github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/configschema/validate_traversal.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package configschema 5 6 import ( 7 "fmt" 8 "sort" 9 10 "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/terramate-io/tf/didyoumean" 15 "github.com/terramate-io/tf/tfdiags" 16 ) 17 18 // StaticValidateTraversal checks whether the given traversal (which must be 19 // relative) refers to a construct in the receiving schema, returning error 20 // diagnostics if any problems are found. 21 // 22 // This method is "optimistic" in that it will not return errors for possible 23 // problems that cannot be detected statically. It is possible that a 24 // traversal which passed static validation will still fail when evaluated. 25 func (b *Block) StaticValidateTraversal(traversal hcl.Traversal) tfdiags.Diagnostics { 26 if !traversal.IsRelative() { 27 panic("StaticValidateTraversal on absolute traversal") 28 } 29 if len(traversal) == 0 { 30 return nil 31 } 32 33 var diags tfdiags.Diagnostics 34 35 next := traversal[0] 36 after := traversal[1:] 37 38 var name string 39 switch step := next.(type) { 40 case hcl.TraverseAttr: 41 name = step.Name 42 case hcl.TraverseIndex: 43 // No other traversal step types are allowed directly at a block. 44 // If it looks like the user was trying to use index syntax to 45 // access an attribute then we'll produce a specialized message. 46 key := step.Key 47 if key.Type() == cty.String && key.IsKnown() && !key.IsNull() { 48 maybeName := key.AsString() 49 if hclsyntax.ValidIdentifier(maybeName) { 50 diags = diags.Append(&hcl.Diagnostic{ 51 Severity: hcl.DiagError, 52 Summary: `Invalid index operation`, 53 Detail: fmt.Sprintf(`Only attribute access is allowed here. Did you mean to access attribute %q using the dot operator?`, maybeName), 54 Subject: &step.SrcRange, 55 }) 56 return diags 57 } 58 } 59 // If it looks like some other kind of index then we'll use a generic error. 60 diags = diags.Append(&hcl.Diagnostic{ 61 Severity: hcl.DiagError, 62 Summary: `Invalid index operation`, 63 Detail: `Only attribute access is allowed here, using the dot operator.`, 64 Subject: &step.SrcRange, 65 }) 66 return diags 67 default: 68 // No other traversal types should appear in a normal valid traversal, 69 // but we'll handle this with a generic error anyway to be robust. 70 diags = diags.Append(&hcl.Diagnostic{ 71 Severity: hcl.DiagError, 72 Summary: `Invalid operation`, 73 Detail: `Only attribute access is allowed here, using the dot operator.`, 74 Subject: next.SourceRange().Ptr(), 75 }) 76 return diags 77 } 78 79 if attrS, exists := b.Attributes[name]; exists { 80 // Check for Deprecated status of this attribute. 81 // We currently can't provide the user with any useful guidance because 82 // the deprecation string is not part of the schema, but we can at 83 // least warn them. 84 // 85 // This purposely does not attempt to recurse into nested attribute 86 // types. Because nested attribute values are often not accessed via a 87 // direct traversal to the leaf attributes, we cannot reliably detect 88 // if a nested, deprecated attribute value is actually used from the 89 // traversal alone. More precise detection of deprecated attributes 90 // would require adding metadata like marks to the cty value itself, to 91 // be caught during evaluation. 92 if attrS.Deprecated { 93 diags = diags.Append(&hcl.Diagnostic{ 94 Severity: hcl.DiagWarning, 95 Summary: `Deprecated attribute`, 96 Detail: fmt.Sprintf(`The attribute %q is deprecated. Refer to the provider documentation for details.`, name), 97 Subject: next.SourceRange().Ptr(), 98 }) 99 } 100 101 // For attribute validation we will just apply the rest of the 102 // traversal to an unknown value of the attribute type and pass 103 // through HCL's own errors, since we don't want to replicate all 104 // of HCL's type checking rules here. 105 val := cty.UnknownVal(attrS.ImpliedType()) 106 _, hclDiags := after.TraverseRel(val) 107 return diags.Append(hclDiags) 108 } 109 110 if blockS, exists := b.BlockTypes[name]; exists { 111 moreDiags := blockS.staticValidateTraversal(name, after) 112 diags = diags.Append(moreDiags) 113 return diags 114 } 115 116 // If we get here then the name isn't valid at all. We'll collect up 117 // all of the names that _are_ valid to use as suggestions. 118 var suggestions []string 119 for name := range b.Attributes { 120 suggestions = append(suggestions, name) 121 } 122 for name := range b.BlockTypes { 123 suggestions = append(suggestions, name) 124 } 125 sort.Strings(suggestions) 126 suggestion := didyoumean.NameSuggestion(name, suggestions) 127 if suggestion != "" { 128 suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) 129 } 130 diags = diags.Append(&hcl.Diagnostic{ 131 Severity: hcl.DiagError, 132 Summary: `Unsupported attribute`, 133 Detail: fmt.Sprintf(`This object has no argument, nested block, or exported attribute named %q.%s`, name, suggestion), 134 Subject: next.SourceRange().Ptr(), 135 }) 136 137 return diags 138 } 139 140 func (b *NestedBlock) staticValidateTraversal(typeName string, traversal hcl.Traversal) tfdiags.Diagnostics { 141 if b.Nesting == NestingSingle || b.Nesting == NestingGroup { 142 // Single blocks are easy: just pass right through. 143 return b.Block.StaticValidateTraversal(traversal) 144 } 145 146 if len(traversal) == 0 { 147 // It's always valid to access a nested block's attribute directly. 148 return nil 149 } 150 151 var diags tfdiags.Diagnostics 152 next := traversal[0] 153 after := traversal[1:] 154 155 switch b.Nesting { 156 157 case NestingSet: 158 // Can't traverse into a set at all, since it does not have any keys 159 // to index with. 160 diags = diags.Append(&hcl.Diagnostic{ 161 Severity: hcl.DiagError, 162 Summary: `Cannot index a set value`, 163 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), 164 Subject: next.SourceRange().Ptr(), 165 }) 166 return diags 167 168 case NestingList: 169 if _, ok := next.(hcl.TraverseIndex); ok { 170 moreDiags := b.Block.StaticValidateTraversal(after) 171 diags = diags.Append(moreDiags) 172 } else { 173 diags = diags.Append(&hcl.Diagnostic{ 174 Severity: hcl.DiagError, 175 Summary: `Invalid operation`, 176 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), 177 Subject: next.SourceRange().Ptr(), 178 }) 179 } 180 return diags 181 182 case NestingMap: 183 // Both attribute and index steps are valid for maps, so we'll just 184 // pass through here and let normal evaluation catch an 185 // incorrectly-typed index key later, if present. 186 moreDiags := b.Block.StaticValidateTraversal(after) 187 diags = diags.Append(moreDiags) 188 return diags 189 190 default: 191 // Invalid nesting type is just ignored. It's checked by 192 // InternalValidate. (Note that we handled NestingSingle separately 193 // back at the start of this function.) 194 return nil 195 } 196 }