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  }