| package configschema |
| |
| import ( |
| "fmt" |
| "sort" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/hcl/v2/hclsyntax" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/didyoumean" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| // StaticValidateTraversal checks whether the given traversal (which must be |
| // relative) refers to a construct in the receiving schema, returning error |
| // diagnostics if any problems are found. |
| // |
| // This method is "optimistic" in that it will not return errors for possible |
| // problems that cannot be detected statically. It is possible that a |
| // traversal which passed static validation will still fail when evaluated. |
| func (b *Block) StaticValidateTraversal(traversal hcl.Traversal) tfdiags.Diagnostics { |
| if !traversal.IsRelative() { |
| panic("StaticValidateTraversal on absolute traversal") |
| } |
| if len(traversal) == 0 { |
| return nil |
| } |
| |
| var diags tfdiags.Diagnostics |
| |
| next := traversal[0] |
| after := traversal[1:] |
| |
| var name string |
| switch step := next.(type) { |
| case hcl.TraverseAttr: |
| name = step.Name |
| case hcl.TraverseIndex: |
| // No other traversal step types are allowed directly at a block. |
| // If it looks like the user was trying to use index syntax to |
| // access an attribute then we'll produce a specialized message. |
| key := step.Key |
| if key.Type() == cty.String && key.IsKnown() && !key.IsNull() { |
| maybeName := key.AsString() |
| if hclsyntax.ValidIdentifier(maybeName) { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Invalid index operation`, |
| Detail: fmt.Sprintf(`Only attribute access is allowed here. Did you mean to access attribute %q using the dot operator?`, maybeName), |
| Subject: &step.SrcRange, |
| }) |
| return diags |
| } |
| } |
| // If it looks like some other kind of index then we'll use a generic error. |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Invalid index operation`, |
| Detail: `Only attribute access is allowed here, using the dot operator.`, |
| Subject: &step.SrcRange, |
| }) |
| return diags |
| default: |
| // No other traversal types should appear in a normal valid traversal, |
| // but we'll handle this with a generic error anyway to be robust. |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Invalid operation`, |
| Detail: `Only attribute access is allowed here, using the dot operator.`, |
| Subject: next.SourceRange().Ptr(), |
| }) |
| return diags |
| } |
| |
| if attrS, exists := b.Attributes[name]; exists { |
| // Check for Deprecated status of this attribute. |
| // We currently can't provide the user with any useful guidance because |
| // the deprecation string is not part of the schema, but we can at |
| // least warn them. |
| // |
| // This purposely does not attempt to recurse into nested attribute |
| // types. Because nested attribute values are often not accessed via a |
| // direct traversal to the leaf attributes, we cannot reliably detect |
| // if a nested, deprecated attribute value is actually used from the |
| // traversal alone. More precise detection of deprecated attributes |
| // would require adding metadata like marks to the cty value itself, to |
| // be caught during evaluation. |
| if attrS.Deprecated { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagWarning, |
| Summary: `Deprecated attribute`, |
| Detail: fmt.Sprintf(`The attribute %q is deprecated. Refer to the provider documentation for details.`, name), |
| Subject: next.SourceRange().Ptr(), |
| }) |
| } |
| |
| // For attribute validation we will just apply the rest of the |
| // traversal to an unknown value of the attribute type and pass |
| // through HCL's own errors, since we don't want to replicate all |
| // of HCL's type checking rules here. |
| val := cty.UnknownVal(attrS.ImpliedType()) |
| _, hclDiags := after.TraverseRel(val) |
| return diags.Append(hclDiags) |
| } |
| |
| if blockS, exists := b.BlockTypes[name]; exists { |
| moreDiags := blockS.staticValidateTraversal(name, after) |
| diags = diags.Append(moreDiags) |
| return diags |
| } |
| |
| // If we get here then the name isn't valid at all. We'll collect up |
| // all of the names that _are_ valid to use as suggestions. |
| var suggestions []string |
| for name := range b.Attributes { |
| suggestions = append(suggestions, name) |
| } |
| for name := range b.BlockTypes { |
| suggestions = append(suggestions, name) |
| } |
| sort.Strings(suggestions) |
| suggestion := didyoumean.NameSuggestion(name, suggestions) |
| if suggestion != "" { |
| suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) |
| } |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Unsupported attribute`, |
| Detail: fmt.Sprintf(`This object has no argument, nested block, or exported attribute named %q.%s`, name, suggestion), |
| Subject: next.SourceRange().Ptr(), |
| }) |
| |
| return diags |
| } |
| |
| func (b *NestedBlock) staticValidateTraversal(typeName string, traversal hcl.Traversal) tfdiags.Diagnostics { |
| if b.Nesting == NestingSingle || b.Nesting == NestingGroup { |
| // Single blocks are easy: just pass right through. |
| return b.Block.StaticValidateTraversal(traversal) |
| } |
| |
| if len(traversal) == 0 { |
| // It's always valid to access a nested block's attribute directly. |
| return nil |
| } |
| |
| var diags tfdiags.Diagnostics |
| next := traversal[0] |
| after := traversal[1:] |
| |
| switch b.Nesting { |
| |
| case NestingSet: |
| // Can't traverse into a set at all, since it does not have any keys |
| // to index with. |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Cannot index a set value`, |
| 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), |
| Subject: next.SourceRange().Ptr(), |
| }) |
| return diags |
| |
| case NestingList: |
| if _, ok := next.(hcl.TraverseIndex); ok { |
| moreDiags := b.Block.StaticValidateTraversal(after) |
| diags = diags.Append(moreDiags) |
| } else { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Invalid operation`, |
| 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), |
| Subject: next.SourceRange().Ptr(), |
| }) |
| } |
| return diags |
| |
| case NestingMap: |
| // Both attribute and index steps are valid for maps, so we'll just |
| // pass through here and let normal evaluation catch an |
| // incorrectly-typed index key later, if present. |
| moreDiags := b.Block.StaticValidateTraversal(after) |
| diags = diags.Append(moreDiags) |
| return diags |
| |
| default: |
| // Invalid nesting type is just ignored. It's checked by |
| // InternalValidate. (Note that we handled NestingSingle separately |
| // back at the start of this function.) |
| return nil |
| } |
| } |