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