github.com/kevinklinger/open_terraform@v1.3.6/noninternal/configs/configschema/validate_traversal.go (about) 1 package configschema 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/kevinklinger/open_terraform/noninternal/didyoumean" 12 "github.com/kevinklinger/open_terraform/noninternal/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 a 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 // Check for Deprecated status of this attribute. 78 // We currently can't provide the user with any useful guidance because 79 // the deprecation string is not part of the schema, but we can at 80 // least warn them. 81 // 82 // This purposely does not attempt to recurse into nested attribute 83 // types. Because nested attribute values are often not accessed via a 84 // direct traversal to the leaf attributes, we cannot reliably detect 85 // if a nested, deprecated attribute value is actually used from the 86 // traversal alone. More precise detection of deprecated attributes 87 // would require adding metadata like marks to the cty value itself, to 88 // be caught during evaluation. 89 if attrS.Deprecated { 90 diags = diags.Append(&hcl.Diagnostic{ 91 Severity: hcl.DiagWarning, 92 Summary: `Deprecated attribute`, 93 Detail: fmt.Sprintf(`The attribute %q is deprecated. Refer to the provider documentation for details.`, name), 94 Subject: next.SourceRange().Ptr(), 95 }) 96 } 97 98 // For attribute validation we will just apply the rest of the 99 // traversal to an unknown value of the attribute type and pass 100 // through HCL's own errors, since we don't want to replicate all 101 // of HCL's type checking rules here. 102 val := cty.UnknownVal(attrS.ImpliedType()) 103 _, hclDiags := after.TraverseRel(val) 104 return diags.Append(hclDiags) 105 } 106 107 if blockS, exists := b.BlockTypes[name]; exists { 108 moreDiags := blockS.staticValidateTraversal(name, after) 109 diags = diags.Append(moreDiags) 110 return diags 111 } 112 113 // If we get here then the name isn't valid at all. We'll collect up 114 // all of the names that _are_ valid to use as suggestions. 115 var suggestions []string 116 for name := range b.Attributes { 117 suggestions = append(suggestions, name) 118 } 119 for name := range b.BlockTypes { 120 suggestions = append(suggestions, name) 121 } 122 sort.Strings(suggestions) 123 suggestion := didyoumean.NameSuggestion(name, suggestions) 124 if suggestion != "" { 125 suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) 126 } 127 diags = diags.Append(&hcl.Diagnostic{ 128 Severity: hcl.DiagError, 129 Summary: `Unsupported attribute`, 130 Detail: fmt.Sprintf(`This object has no argument, nested block, or exported attribute named %q.%s`, name, suggestion), 131 Subject: next.SourceRange().Ptr(), 132 }) 133 134 return diags 135 } 136 137 func (b *NestedBlock) staticValidateTraversal(typeName string, traversal hcl.Traversal) tfdiags.Diagnostics { 138 if b.Nesting == NestingSingle || b.Nesting == NestingGroup { 139 // Single blocks are easy: just pass right through. 140 return b.Block.StaticValidateTraversal(traversal) 141 } 142 143 if len(traversal) == 0 { 144 // It's always valid to access a nested block's attribute directly. 145 return nil 146 } 147 148 var diags tfdiags.Diagnostics 149 next := traversal[0] 150 after := traversal[1:] 151 152 switch b.Nesting { 153 154 case NestingSet: 155 // Can't traverse into a set at all, since it does not have any keys 156 // to index with. 157 diags = diags.Append(&hcl.Diagnostic{ 158 Severity: hcl.DiagError, 159 Summary: `Cannot index a set value`, 160 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), 161 Subject: next.SourceRange().Ptr(), 162 }) 163 return diags 164 165 case NestingList: 166 if _, ok := next.(hcl.TraverseIndex); ok { 167 moreDiags := b.Block.StaticValidateTraversal(after) 168 diags = diags.Append(moreDiags) 169 } else { 170 diags = diags.Append(&hcl.Diagnostic{ 171 Severity: hcl.DiagError, 172 Summary: `Invalid operation`, 173 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), 174 Subject: next.SourceRange().Ptr(), 175 }) 176 } 177 return diags 178 179 case NestingMap: 180 // Both attribute and index steps are valid for maps, so we'll just 181 // pass through here and let normal evaluation catch an 182 // incorrectly-typed index key later, if present. 183 moreDiags := b.Block.StaticValidateTraversal(after) 184 diags = diags.Append(moreDiags) 185 return diags 186 187 default: 188 // Invalid nesting type is just ignored. It's checked by 189 // InternalValidate. (Note that we handled NestingSingle separately 190 // back at the start of this function.) 191 return nil 192 } 193 }