github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/parse_ref.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package addrs
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     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/tfdiags"
    15  )
    16  
    17  // Reference describes a reference to an address with source location
    18  // information.
    19  type Reference struct {
    20  	Subject     Referenceable
    21  	SourceRange tfdiags.SourceRange
    22  	Remaining   hcl.Traversal
    23  }
    24  
    25  // DisplayString returns a string that approximates the subject and remaining
    26  // traversal of the reciever in a way that resembles the Terraform language
    27  // syntax that could've produced it.
    28  //
    29  // It's not guaranteed to actually be a valid Terraform language expression,
    30  // since the intended use here is primarily for UI messages such as
    31  // diagnostics.
    32  func (r *Reference) DisplayString() string {
    33  	if len(r.Remaining) == 0 {
    34  		// Easy case: we can just return the subject's string.
    35  		return r.Subject.String()
    36  	}
    37  
    38  	var ret strings.Builder
    39  	ret.WriteString(r.Subject.String())
    40  	for _, step := range r.Remaining {
    41  		switch tStep := step.(type) {
    42  		case hcl.TraverseRoot:
    43  			ret.WriteString(tStep.Name)
    44  		case hcl.TraverseAttr:
    45  			ret.WriteByte('.')
    46  			ret.WriteString(tStep.Name)
    47  		case hcl.TraverseIndex:
    48  			ret.WriteByte('[')
    49  			switch tStep.Key.Type() {
    50  			case cty.String:
    51  				ret.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
    52  			case cty.Number:
    53  				bf := tStep.Key.AsBigFloat()
    54  				ret.WriteString(bf.Text('g', 10))
    55  			}
    56  			ret.WriteByte(']')
    57  		}
    58  	}
    59  	return ret.String()
    60  }
    61  
    62  // ParseRef attempts to extract a referencable address from the prefix of the
    63  // given traversal, which must be an absolute traversal or this function
    64  // will panic.
    65  //
    66  // If no error diagnostics are returned, the returned reference includes the
    67  // address that was extracted, the source range it was extracted from, and any
    68  // remaining relative traversal that was not consumed as part of the
    69  // reference.
    70  //
    71  // If error diagnostics are returned then the Reference value is invalid and
    72  // must not be used.
    73  func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
    74  	ref, diags := parseRef(traversal)
    75  
    76  	// Normalize a little to make life easier for callers.
    77  	if ref != nil {
    78  		if len(ref.Remaining) == 0 {
    79  			ref.Remaining = nil
    80  		}
    81  	}
    82  
    83  	return ref, diags
    84  }
    85  
    86  // ParseRefFromTestingScope adds check blocks and outputs into the available
    87  // references returned by ParseRef.
    88  //
    89  // The testing files and functionality have a slightly expanded referencing
    90  // scope and so should use this function to retrieve references.
    91  func ParseRefFromTestingScope(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
    92  	root := traversal.RootName()
    93  
    94  	var diags tfdiags.Diagnostics
    95  	var reference *Reference
    96  
    97  	switch root {
    98  	case "output":
    99  		name, rng, remain, outputDiags := parseSingleAttrRef(traversal)
   100  		reference = &Reference{
   101  			Subject:     OutputValue{Name: name},
   102  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   103  			Remaining:   remain,
   104  		}
   105  		diags = outputDiags
   106  	case "check":
   107  		name, rng, remain, checkDiags := parseSingleAttrRef(traversal)
   108  		reference = &Reference{
   109  			Subject:     Check{Name: name},
   110  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   111  			Remaining:   remain,
   112  		}
   113  		diags = checkDiags
   114  	}
   115  
   116  	if reference != nil {
   117  		if len(reference.Remaining) == 0 {
   118  			reference.Remaining = nil
   119  		}
   120  		return reference, diags
   121  	}
   122  
   123  	// If it's not an output or a check block, then just parse it as normal.
   124  	return ParseRef(traversal)
   125  }
   126  
   127  // ParseRefStr is a helper wrapper around ParseRef that takes a string
   128  // and parses it with the HCL native syntax traversal parser before
   129  // interpreting it.
   130  //
   131  // This should be used only in specialized situations since it will cause the
   132  // created references to not have any meaningful source location information.
   133  // If a reference string is coming from a source that should be identified in
   134  // error messages then the caller should instead parse it directly using a
   135  // suitable function from the HCL API and pass the traversal itself to
   136  // ParseRef.
   137  //
   138  // Error diagnostics are returned if either the parsing fails or the analysis
   139  // of the traversal fails. There is no way for the caller to distinguish the
   140  // two kinds of diagnostics programmatically. If error diagnostics are returned
   141  // the returned reference may be nil or incomplete.
   142  func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) {
   143  	var diags tfdiags.Diagnostics
   144  
   145  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
   146  	diags = diags.Append(parseDiags)
   147  	if parseDiags.HasErrors() {
   148  		return nil, diags
   149  	}
   150  
   151  	ref, targetDiags := ParseRef(traversal)
   152  	diags = diags.Append(targetDiags)
   153  	return ref, diags
   154  }
   155  
   156  // ParseRefStrFromTestingScope matches ParseRefStr except it supports the
   157  // references supported by ParseRefFromTestingScope.
   158  func ParseRefStrFromTestingScope(str string) (*Reference, tfdiags.Diagnostics) {
   159  	var diags tfdiags.Diagnostics
   160  
   161  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
   162  	diags = diags.Append(parseDiags)
   163  	if parseDiags.HasErrors() {
   164  		return nil, diags
   165  	}
   166  
   167  	ref, targetDiags := ParseRefFromTestingScope(traversal)
   168  	diags = diags.Append(targetDiags)
   169  	return ref, diags
   170  }
   171  
   172  func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
   173  	var diags tfdiags.Diagnostics
   174  
   175  	root := traversal.RootName()
   176  	rootRange := traversal[0].SourceRange()
   177  
   178  	switch root {
   179  
   180  	case "count":
   181  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   182  		return &Reference{
   183  			Subject:     CountAttr{Name: name},
   184  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   185  			Remaining:   remain,
   186  		}, diags
   187  
   188  	case "each":
   189  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   190  		return &Reference{
   191  			Subject:     ForEachAttr{Name: name},
   192  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   193  			Remaining:   remain,
   194  		}, diags
   195  
   196  	case "data":
   197  		if len(traversal) < 3 {
   198  			diags = diags.Append(&hcl.Diagnostic{
   199  				Severity: hcl.DiagError,
   200  				Summary:  "Invalid reference",
   201  				Detail:   `The "data" object must be followed by two attribute names: the data source type and the resource name.`,
   202  				Subject:  traversal.SourceRange().Ptr(),
   203  			})
   204  			return nil, diags
   205  		}
   206  		remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser
   207  		return parseResourceRef(DataResourceMode, rootRange, remain)
   208  
   209  	case "resource":
   210  		// This is an alias for the normal case of just using a managed resource
   211  		// type as a top-level symbol, which will serve as an escape mechanism
   212  		// if a later edition of the Terraform language introduces a new
   213  		// reference prefix that conflicts with a resource type name in an
   214  		// existing provider. In that case, the edition upgrade tool can
   215  		// rewrite foo.bar into resource.foo.bar to ensure that "foo" remains
   216  		// interpreted as a resource type name rather than as the new reserved
   217  		// word.
   218  		if len(traversal) < 3 {
   219  			diags = diags.Append(&hcl.Diagnostic{
   220  				Severity: hcl.DiagError,
   221  				Summary:  "Invalid reference",
   222  				Detail:   `The "resource" object must be followed by two attribute names: the resource type and the resource name.`,
   223  				Subject:  traversal.SourceRange().Ptr(),
   224  			})
   225  			return nil, diags
   226  		}
   227  		remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser
   228  		return parseResourceRef(ManagedResourceMode, rootRange, remain)
   229  
   230  	case "local":
   231  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   232  		return &Reference{
   233  			Subject:     LocalValue{Name: name},
   234  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   235  			Remaining:   remain,
   236  		}, diags
   237  
   238  	case "module":
   239  		callName, callRange, remain, diags := parseSingleAttrRef(traversal)
   240  		if diags.HasErrors() {
   241  			return nil, diags
   242  		}
   243  
   244  		// A traversal starting with "module" can either be a reference to an
   245  		// entire module, or to a single output from a module instance,
   246  		// depending on what we find after this introducer.
   247  		callInstance := ModuleCallInstance{
   248  			Call: ModuleCall{
   249  				Name: callName,
   250  			},
   251  			Key: NoKey,
   252  		}
   253  
   254  		if len(remain) == 0 {
   255  			// Reference to an entire module. Might alternatively be a
   256  			// reference to a single instance of a particular module, but the
   257  			// caller will need to deal with that ambiguity since we don't have
   258  			// enough context here.
   259  			return &Reference{
   260  				Subject:     callInstance.Call,
   261  				SourceRange: tfdiags.SourceRangeFromHCL(callRange),
   262  				Remaining:   remain,
   263  			}, diags
   264  		}
   265  
   266  		if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok {
   267  			var err error
   268  			callInstance.Key, err = ParseInstanceKey(idxTrav.Key)
   269  			if err != nil {
   270  				diags = diags.Append(&hcl.Diagnostic{
   271  					Severity: hcl.DiagError,
   272  					Summary:  "Invalid index key",
   273  					Detail:   fmt.Sprintf("Invalid index for module instance: %s.", err),
   274  					Subject:  &idxTrav.SrcRange,
   275  				})
   276  				return nil, diags
   277  			}
   278  			remain = remain[1:]
   279  
   280  			if len(remain) == 0 {
   281  				// Also a reference to an entire module instance, but we have a key
   282  				// now.
   283  				return &Reference{
   284  					Subject:     callInstance,
   285  					SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)),
   286  					Remaining:   remain,
   287  				}, diags
   288  			}
   289  		}
   290  
   291  		if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok {
   292  			remain = remain[1:]
   293  			return &Reference{
   294  				Subject: ModuleCallInstanceOutput{
   295  					Name: attrTrav.Name,
   296  					Call: callInstance,
   297  				},
   298  				SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)),
   299  				Remaining:   remain,
   300  			}, diags
   301  		}
   302  
   303  		diags = diags.Append(&hcl.Diagnostic{
   304  			Severity: hcl.DiagError,
   305  			Summary:  "Invalid reference",
   306  			Detail:   "Module instance objects do not support this operation.",
   307  			Subject:  remain[0].SourceRange().Ptr(),
   308  		})
   309  		return nil, diags
   310  
   311  	case "path":
   312  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   313  		return &Reference{
   314  			Subject:     PathAttr{Name: name},
   315  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   316  			Remaining:   remain,
   317  		}, diags
   318  
   319  	case "self":
   320  		return &Reference{
   321  			Subject:     Self,
   322  			SourceRange: tfdiags.SourceRangeFromHCL(rootRange),
   323  			Remaining:   traversal[1:],
   324  		}, diags
   325  
   326  	case "terraform":
   327  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   328  		return &Reference{
   329  			Subject:     TerraformAttr{Name: name},
   330  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   331  			Remaining:   remain,
   332  		}, diags
   333  
   334  	case "var":
   335  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   336  		return &Reference{
   337  			Subject:     InputVariable{Name: name},
   338  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   339  			Remaining:   remain,
   340  		}, diags
   341  
   342  	case "template", "lazy", "arg":
   343  		// These names are all pre-emptively reserved in the hope of landing
   344  		// some version of "template values" or "lazy expressions" feature
   345  		// before the next opt-in language edition, but don't yet do anything.
   346  		diags = diags.Append(&hcl.Diagnostic{
   347  			Severity: hcl.DiagError,
   348  			Summary:  "Reserved symbol name",
   349  			Detail:   fmt.Sprintf("The symbol name %q is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name.", root),
   350  			Subject:  rootRange.Ptr(),
   351  		})
   352  		return nil, diags
   353  
   354  	default:
   355  		return parseResourceRef(ManagedResourceMode, rootRange, traversal)
   356  	}
   357  }
   358  
   359  func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
   360  	var diags tfdiags.Diagnostics
   361  
   362  	if len(traversal) < 2 {
   363  		diags = diags.Append(&hcl.Diagnostic{
   364  			Severity: hcl.DiagError,
   365  			Summary:  "Invalid reference",
   366  			Detail:   `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`,
   367  			Subject:  hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(),
   368  		})
   369  		return nil, diags
   370  	}
   371  
   372  	var typeName, name string
   373  	switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode
   374  	case hcl.TraverseRoot:
   375  		typeName = tt.Name
   376  	case hcl.TraverseAttr:
   377  		typeName = tt.Name
   378  	default:
   379  		// If it isn't a TraverseRoot then it must be a "data" reference.
   380  		diags = diags.Append(&hcl.Diagnostic{
   381  			Severity: hcl.DiagError,
   382  			Summary:  "Invalid reference",
   383  			Detail:   `The "data" object does not support this operation.`,
   384  			Subject:  traversal[0].SourceRange().Ptr(),
   385  		})
   386  		return nil, diags
   387  	}
   388  
   389  	attrTrav, ok := traversal[1].(hcl.TraverseAttr)
   390  	if !ok {
   391  		var what string
   392  		switch mode {
   393  		case DataResourceMode:
   394  			what = "data source"
   395  		default:
   396  			what = "resource type"
   397  		}
   398  		diags = diags.Append(&hcl.Diagnostic{
   399  			Severity: hcl.DiagError,
   400  			Summary:  "Invalid reference",
   401  			Detail:   fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what),
   402  			Subject:  traversal[1].SourceRange().Ptr(),
   403  		})
   404  		return nil, diags
   405  	}
   406  	name = attrTrav.Name
   407  	rng := hcl.RangeBetween(startRange, attrTrav.SrcRange)
   408  	remain := traversal[2:]
   409  
   410  	resourceAddr := Resource{
   411  		Mode: mode,
   412  		Type: typeName,
   413  		Name: name,
   414  	}
   415  	resourceInstAddr := ResourceInstance{
   416  		Resource: resourceAddr,
   417  		Key:      NoKey,
   418  	}
   419  
   420  	if len(remain) == 0 {
   421  		// This might actually be a reference to the collection of all instances
   422  		// of the resource, but we don't have enough context here to decide
   423  		// so we'll let the caller resolve that ambiguity.
   424  		return &Reference{
   425  			Subject:     resourceAddr,
   426  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   427  		}, diags
   428  	}
   429  
   430  	if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok {
   431  		var err error
   432  		resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key)
   433  		if err != nil {
   434  			diags = diags.Append(&hcl.Diagnostic{
   435  				Severity: hcl.DiagError,
   436  				Summary:  "Invalid index key",
   437  				Detail:   fmt.Sprintf("Invalid index for resource instance: %s.", err),
   438  				Subject:  &idxTrav.SrcRange,
   439  			})
   440  			return nil, diags
   441  		}
   442  		remain = remain[1:]
   443  		rng = hcl.RangeBetween(rng, idxTrav.SrcRange)
   444  	}
   445  
   446  	return &Reference{
   447  		Subject:     resourceInstAddr,
   448  		SourceRange: tfdiags.SourceRangeFromHCL(rng),
   449  		Remaining:   remain,
   450  	}, diags
   451  }
   452  
   453  func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
   454  	var diags tfdiags.Diagnostics
   455  
   456  	root := traversal.RootName()
   457  	rootRange := traversal[0].SourceRange()
   458  
   459  	if len(traversal) < 2 {
   460  		diags = diags.Append(&hcl.Diagnostic{
   461  			Severity: hcl.DiagError,
   462  			Summary:  "Invalid reference",
   463  			Detail:   fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root),
   464  			Subject:  &rootRange,
   465  		})
   466  		return "", hcl.Range{}, nil, diags
   467  	}
   468  	if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
   469  		return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags
   470  	}
   471  	diags = diags.Append(&hcl.Diagnostic{
   472  		Severity: hcl.DiagError,
   473  		Summary:  "Invalid reference",
   474  		Detail:   fmt.Sprintf("The %q object does not support this operation.", root),
   475  		Subject:  traversal[1].SourceRange().Ptr(),
   476  	})
   477  	return "", hcl.Range{}, nil, diags
   478  }