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  }