kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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 "kubeform.dev/terraform-backend-sdk/didyoumean" 12 "kubeform.dev/terraform-backend-sdk/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 }