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