]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package configschema |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "sort" | |
6 | ||
7 | "github.com/hashicorp/hcl2/hcl" | |
8 | "github.com/hashicorp/hcl2/hcl/hclsyntax" | |
9 | "github.com/zclconf/go-cty/cty" | |
10 | ||
11 | "github.com/hashicorp/terraform/helper/didyoumean" | |
12 | "github.com/hashicorp/terraform/tfdiags" | |
13 | ) | |
14 | ||
15 | // StaticValidateTraversal checks whether the given traversal (which must be | |
16 | // relative) refers to a construct in the receiving schema, returning error | |
17 | // diagnostics if any problems are found. | |
18 | // | |
19 | // This method is "optimistic" in that it will not return errors for possible | |
20 | // problems that cannot be detected statically. It is possible that an | |
21 | // traversal which passed static validation will still fail when evaluated. | |
22 | func (b *Block) StaticValidateTraversal(traversal hcl.Traversal) tfdiags.Diagnostics { | |
23 | if !traversal.IsRelative() { | |
24 | panic("StaticValidateTraversal on absolute traversal") | |
25 | } | |
26 | if len(traversal) == 0 { | |
27 | return nil | |
28 | } | |
29 | ||
30 | var diags tfdiags.Diagnostics | |
31 | ||
32 | next := traversal[0] | |
33 | after := traversal[1:] | |
34 | ||
35 | var name string | |
36 | switch step := next.(type) { | |
37 | case hcl.TraverseAttr: | |
38 | name = step.Name | |
39 | case hcl.TraverseIndex: | |
40 | // No other traversal step types are allowed directly at a block. | |
41 | // If it looks like the user was trying to use index syntax to | |
42 | // access an attribute then we'll produce a specialized message. | |
43 | key := step.Key | |
44 | if key.Type() == cty.String && key.IsKnown() && !key.IsNull() { | |
45 | maybeName := key.AsString() | |
46 | if hclsyntax.ValidIdentifier(maybeName) { | |
47 | diags = diags.Append(&hcl.Diagnostic{ | |
48 | Severity: hcl.DiagError, | |
49 | Summary: `Invalid index operation`, | |
50 | Detail: fmt.Sprintf(`Only attribute access is allowed here. Did you mean to access attribute %q using the dot operator?`, maybeName), | |
51 | Subject: &step.SrcRange, | |
52 | }) | |
53 | return diags | |
54 | } | |
55 | } | |
56 | // If it looks like some other kind of index then we'll use a generic error. | |
57 | diags = diags.Append(&hcl.Diagnostic{ | |
58 | Severity: hcl.DiagError, | |
59 | Summary: `Invalid index operation`, | |
60 | Detail: `Only attribute access is allowed here, using the dot operator.`, | |
61 | Subject: &step.SrcRange, | |
62 | }) | |
63 | return diags | |
64 | default: | |
65 | // No other traversal types should appear in a normal valid traversal, | |
66 | // but we'll handle this with a generic error anyway to be robust. | |
67 | diags = diags.Append(&hcl.Diagnostic{ | |
68 | Severity: hcl.DiagError, | |
69 | Summary: `Invalid operation`, | |
70 | Detail: `Only attribute access is allowed here, using the dot operator.`, | |
71 | Subject: next.SourceRange().Ptr(), | |
72 | }) | |
73 | return diags | |
74 | } | |
75 | ||
76 | if attrS, exists := b.Attributes[name]; exists { | |
77 | // For attribute validation we will just apply the rest of the | |
78 | // traversal to an unknown value of the attribute type and pass | |
79 | // through HCL's own errors, since we don't want to replicate all of | |
80 | // HCL's type checking rules here. | |
81 | val := cty.UnknownVal(attrS.Type) | |
82 | _, hclDiags := after.TraverseRel(val) | |
83 | diags = diags.Append(hclDiags) | |
84 | return diags | |
85 | } | |
86 | ||
87 | if blockS, exists := b.BlockTypes[name]; exists { | |
88 | moreDiags := blockS.staticValidateTraversal(name, after) | |
89 | diags = diags.Append(moreDiags) | |
90 | return diags | |
91 | } | |
92 | ||
93 | // If we get here then the name isn't valid at all. We'll collect up | |
94 | // all of the names that _are_ valid to use as suggestions. | |
95 | var suggestions []string | |
96 | for name := range b.Attributes { | |
97 | suggestions = append(suggestions, name) | |
98 | } | |
99 | for name := range b.BlockTypes { | |
100 | suggestions = append(suggestions, name) | |
101 | } | |
102 | sort.Strings(suggestions) | |
103 | suggestion := didyoumean.NameSuggestion(name, suggestions) | |
104 | if suggestion != "" { | |
105 | suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) | |
106 | } | |
107 | diags = diags.Append(&hcl.Diagnostic{ | |
108 | Severity: hcl.DiagError, | |
109 | Summary: `Unsupported attribute`, | |
110 | Detail: fmt.Sprintf(`This object has no argument, nested block, or exported attribute named %q.%s`, name, suggestion), | |
111 | Subject: next.SourceRange().Ptr(), | |
112 | }) | |
113 | ||
114 | return diags | |
115 | } | |
116 | ||
117 | func (b *NestedBlock) staticValidateTraversal(typeName string, traversal hcl.Traversal) tfdiags.Diagnostics { | |
118 | if b.Nesting == NestingSingle || b.Nesting == NestingGroup { | |
119 | // Single blocks are easy: just pass right through. | |
120 | return b.Block.StaticValidateTraversal(traversal) | |
121 | } | |
122 | ||
123 | if len(traversal) == 0 { | |
124 | // It's always valid to access a nested block's attribute directly. | |
125 | return nil | |
126 | } | |
127 | ||
128 | var diags tfdiags.Diagnostics | |
129 | next := traversal[0] | |
130 | after := traversal[1:] | |
131 | ||
132 | switch b.Nesting { | |
133 | ||
134 | case NestingSet: | |
135 | // Can't traverse into a set at all, since it does not have any keys | |
136 | // to index with. | |
137 | diags = diags.Append(&hcl.Diagnostic{ | |
138 | Severity: hcl.DiagError, | |
139 | Summary: `Cannot index a set value`, | |
140 | Detail: fmt.Sprintf(`Block type %q is represented by a set of objects, and set elements do not have addressable keys. To find elements matching specific criteria, use a "for" expression with an "if" clause.`, typeName), | |
141 | Subject: next.SourceRange().Ptr(), | |
142 | }) | |
143 | return diags | |
144 | ||
145 | case NestingList: | |
146 | if _, ok := next.(hcl.TraverseIndex); ok { | |
147 | moreDiags := b.Block.StaticValidateTraversal(after) | |
148 | diags = diags.Append(moreDiags) | |
149 | } else { | |
150 | diags = diags.Append(&hcl.Diagnostic{ | |
151 | Severity: hcl.DiagError, | |
152 | Summary: `Invalid operation`, | |
153 | Detail: fmt.Sprintf(`Block type %q is represented by a list of objects, so it must be indexed using a numeric key, like .%s[0].`, typeName, typeName), | |
154 | Subject: next.SourceRange().Ptr(), | |
155 | }) | |
156 | } | |
157 | return diags | |
158 | ||
159 | case NestingMap: | |
160 | // Both attribute and index steps are valid for maps, so we'll just | |
161 | // pass through here and let normal evaluation catch an | |
162 | // incorrectly-typed index key later, if present. | |
163 | moreDiags := b.Block.StaticValidateTraversal(after) | |
164 | diags = diags.Append(moreDiags) | |
165 | return diags | |
166 | ||
167 | default: | |
168 | // Invalid nesting type is just ignored. It's checked by | |
169 | // InternalValidate. (Note that we handled NestingSingle separately | |
170 | // back at the start of this function.) | |
171 | return nil | |
172 | } | |
173 | } |