1package configschema
2
3import (
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.
22func (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
117func (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}
174