github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/addrs/parse_ref.go (about)

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