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  }