From a5893ae76abcc6e107d033c8fb03627eec315238 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 30 Mar 2018 20:18:59 -0700 Subject: [PATCH] addrs: ParseRef function, for parsing references in expressions This function corresponds to terraform.NewInterpolatedVariable, but built with HCL2 primitives. It accepts a hcl.Traversal, which is what is returned from the HCL2 API functions to find which variables are referenced in a given expression. --- addrs/parse_ref.go | 296 +++++++++++++++++ addrs/parse_ref_test.go | 689 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 985 insertions(+) create mode 100644 addrs/parse_ref.go create mode 100644 addrs/parse_ref_test.go diff --git a/addrs/parse_ref.go b/addrs/parse_ref.go new file mode 100644 index 0000000000..04c450a009 --- /dev/null +++ b/addrs/parse_ref.go @@ -0,0 +1,296 @@ +package addrs + +import ( + "fmt" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/terraform/tfdiags" +) + +// Reference describes a reference to an address with source location +// information. +type Reference struct { + Subject Referenceable + SourceRange tfdiags.SourceRange + Remaining hcl.Traversal +} + +// ParseRef attempts to extract a referencable address from the prefix of the +// given traversal, which must be an absolute traversal or this function +// will panic. +// +// If no error diagnostics are returned, the returned reference includes the +// address that was extracted, the source range it was extracted from, and any +// remaining relative traversal that was not consumed as part of the +// reference. +// +// If error diagnostics are returned then the Reference value is invalid and +// must not be used. +func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + root := traversal.RootName() + rootRange := traversal[0].SourceRange() + + switch root { + + case "count": + name, rng, remain, diags := parseSingleAttrRef(traversal) + return &Reference{ + Subject: CountAttr{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags + + case "data": + if len(traversal) < 3 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "data" object must be followed by two attribute names: the data source type and the resource name.`, + Subject: traversal.SourceRange().Ptr(), + }) + return nil, diags + } + remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser + return parseResourceRef(DataResourceMode, rootRange, remain) + + case "local": + name, rng, remain, diags := parseSingleAttrRef(traversal) + return &Reference{ + Subject: LocalValue{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags + + case "module": + callName, callRange, remain, diags := parseSingleAttrRef(traversal) + if diags.HasErrors() { + return nil, diags + } + + // A traversal starting with "module" can either be a reference to + // an entire module instance or to a single output from a module + // instance, depending on what we find after this introducer. + + callInstance := ModuleCallInstance{ + Call: ModuleCall{ + Name: callName, + }, + Key: NoKey, + } + + if len(remain) == 0 { + // Reference to an entire module instance. Might alternatively + // be a reference to a collection of instances of a particular + // module, but the caller will need to deal with that ambiguity + // since we don't have enough context here. + return &Reference{ + Subject: callInstance, + SourceRange: tfdiags.SourceRangeFromHCL(callRange), + Remaining: remain, + }, diags + } + + if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { + var err error + callInstance.Key, err = ParseInstanceKey(idxTrav.Key) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid index key", + Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), + Subject: &idxTrav.SrcRange, + }) + return nil, diags + } + remain = remain[1:] + + if len(remain) == 0 { + // Also a reference to an entire module instance, but we have a key + // now. + return &Reference{ + Subject: callInstance, + SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)), + Remaining: remain, + }, diags + } + } + + if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { + remain = remain[1:] + return &Reference{ + Subject: ModuleCallOutput{ + Name: attrTrav.Name, + Call: callInstance, + }, + SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)), + Remaining: remain, + }, diags + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: "Module instance objects do not support this operation.", + Subject: remain[0].SourceRange().Ptr(), + }) + return nil, diags + + case "path": + name, rng, remain, diags := parseSingleAttrRef(traversal) + return &Reference{ + Subject: PathAttr{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags + + case "self": + return &Reference{ + Subject: Self, + SourceRange: tfdiags.SourceRangeFromHCL(rootRange), + Remaining: traversal[1:], + }, diags + + case "terraform": + name, rng, remain, diags := parseSingleAttrRef(traversal) + return &Reference{ + Subject: TerraformAttr{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags + + case "var": + name, rng, remain, diags := parseSingleAttrRef(traversal) + return &Reference{ + Subject: InputVariable{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags + + default: + return parseResourceRef(ManagedResourceMode, rootRange, traversal) + } +} + +func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if len(traversal) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, + Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), + }) + return nil, diags + } + + var typeName, name string + switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode + case hcl.TraverseRoot: + typeName = tt.Name + case hcl.TraverseAttr: + typeName = tt.Name + default: + // If it isn't a TraverseRoot then it must be a "data" reference. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "data" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + return nil, diags + } + + attrTrav, ok := traversal[1].(hcl.TraverseAttr) + if !ok { + var what string + switch mode { + case DataResourceMode: + what = "data source" + default: + what = "resource type" + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), + Subject: traversal[1].SourceRange().Ptr(), + }) + return nil, diags + } + name = attrTrav.Name + rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) + remain := traversal[2:] + + resourceAddr := Resource{ + Mode: mode, + Type: typeName, + Name: name, + } + resourceInstAddr := ResourceInstance{ + Resource: resourceAddr, + Key: NoKey, + } + + if len(remain) == 0 { + // This might actually be a reference to the collection of all instances + // of the resource, but we don't have enough context here to decide + // so we'll let the caller resolve that ambiguity. + return &Reference{ + Subject: resourceInstAddr, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags + } + + if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { + var err error + resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid index key", + Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), + Subject: &idxTrav.SrcRange, + }) + return nil, diags + } + remain = remain[1:] + rng = hcl.RangeBetween(rng, idxTrav.SrcRange) + } + + return &Reference{ + Subject: resourceInstAddr, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags +} + +func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + root := traversal.RootName() + rootRange := traversal[0].SourceRange() + + if len(traversal) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), + Subject: &rootRange, + }) + return "", hcl.Range{}, nil, diags + } + if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { + return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The %q object does not support this operation.", root), + Subject: traversal[1].SourceRange().Ptr(), + }) + return "", hcl.Range{}, nil, diags +} diff --git a/addrs/parse_ref_test.go b/addrs/parse_ref_test.go new file mode 100644 index 0000000000..7b0f2c4fb0 --- /dev/null +++ b/addrs/parse_ref_test.go @@ -0,0 +1,689 @@ +package addrs + +import ( + "testing" + + "github.com/go-test/deep" + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func TestParseRef(t *testing.T) { + tests := []struct { + Input string + Want *Reference + WantErr string + }{ + + // count + { + `count.index`, + &Reference{ + Subject: CountAttr{ + Name: "index", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `count.index.blah`, + &Reference{ + Subject: CountAttr{ + Name: "index", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "blah", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + }, + }, + }, + ``, // valid at this layer, but will fail during eval because "index" is a number + }, + { + `count`, + nil, + `The "count" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `count["hello"]`, + nil, + `The "count" object does not support this operation.`, + }, + + // data + { + `data.external.foo`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: DataResourceMode, + Type: "external", + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `data.external.foo.bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: DataResourceMode, + Type: "external", + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, + }, + ``, + }, + { + `data.external.foo["baz"].bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: DataResourceMode, + Type: "external", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + }, + }, + ``, + }, + { + `data.external.foo["baz"]`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: DataResourceMode, + Type: "external", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `data`, + nil, + `The "data" object must be followed by two attribute names: the data source type and the resource name.`, + }, + { + `data.external`, + nil, + `The "data" object must be followed by two attribute names: the data source type and the resource name.`, + }, + + // local + { + `local.foo`, + &Reference{ + Subject: LocalValue{ + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `local.foo.blah`, + &Reference{ + Subject: LocalValue{ + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "blah", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }, + ``, + }, + { + `local.foo["blah"]`, + &Reference{ + Subject: LocalValue{ + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseIndex{ + Key: cty.StringVal("blah"), + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + }, + }, + }, + }, + ``, + }, + { + `local`, + nil, + `The "local" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `local["foo"]`, + nil, + `The "local" object does not support this operation.`, + }, + + // module + { + `module.foo`, + &Reference{ + Subject: ModuleCallInstance{ + Call: ModuleCall{ + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `module.foo.bar`, + &Reference{ + Subject: ModuleCallOutput{ + Call: ModuleCallInstance{ + Call: ModuleCall{ + Name: "foo", + }, + }, + Name: "bar", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `module.foo.bar.baz`, + &Reference{ + Subject: ModuleCallOutput{ + Call: ModuleCallInstance{ + Call: ModuleCall{ + Name: "foo", + }, + }, + Name: "bar", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "baz", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + }, + }, + }, + ``, + }, + { + `module.foo["baz"]`, + &Reference{ + Subject: ModuleCallInstance{ + Call: ModuleCall{ + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `module.foo["baz"].bar`, + &Reference{ + Subject: ModuleCallOutput{ + Call: ModuleCallInstance{ + Call: ModuleCall{ + Name: "foo", + }, + Key: StringKey("baz"), + }, + Name: "bar", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `module.foo["baz"].bar.boop`, + &Reference{ + Subject: ModuleCallOutput{ + Call: ModuleCallInstance{ + Call: ModuleCall{ + Name: "foo", + }, + Key: StringKey("baz"), + }, + Name: "bar", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "boop", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }, + ``, + }, + { + `module`, + nil, + `The "module" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `module["foo"]`, + nil, + `The "module" object does not support this operation.`, + }, + + // path + { + `path.module`, + &Reference{ + Subject: PathAttr{ + Name: "module", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `path.module.blah`, + &Reference{ + Subject: PathAttr{ + Name: "module", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "blah", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + }, + }, + }, + ``, // valid at this layer, but will fail during eval because "module" is a string + }, + { + `path`, + nil, + `The "path" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `path["module"]`, + nil, + `The "path" object does not support this operation.`, + }, + + // self + { + `self`, + &Reference{ + Subject: Self, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `self.blah`, + &Reference{ + Subject: Self, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "blah", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + ``, + }, + + // terraform + { + `terraform.workspace`, + &Reference{ + Subject: TerraformAttr{ + Name: "workspace", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `terraform.workspace.blah`, + &Reference{ + Subject: TerraformAttr{ + Name: "workspace", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "blah", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + }, + ``, // valid at this layer, but will fail during eval because "workspace" is a string + }, + { + `terraform`, + nil, + `The "terraform" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `terraform["workspace"]`, + nil, + `The "terraform" object does not support this operation.`, + }, + + // var + { + `var.foo`, + &Reference{ + Subject: InputVariable{ + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `var.foo.blah`, + &Reference{ + Subject: InputVariable{ + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "blah", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + }, + }, + }, + ``, // valid at this layer, but will fail during eval because "module" is a string + }, + { + `var`, + nil, + `The "var" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `var["foo"]`, + nil, + `The "var" object does not support this operation.`, + }, + + // anything else, interpreted as a managed resource reference + { + `boop_instance.foo`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "boop_instance", + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `boop_instance.foo.bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "boop_instance", + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, + }, + ``, + }, + { + `boop_instance.foo["baz"].bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "boop_instance", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + }, + }, + ``, + }, + { + `boop_instance.foo["baz"]`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "boop_instance", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, + }, + Remaining: hcl.Traversal{}, + }, + ``, + }, + { + `boop_instance`, + nil, + `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1}) + if travDiags.HasErrors() { + t.Fatal(travDiags.Error()) + } + + got, diags := ParseRef(traversal) + + switch len(diags) { + case 0: + if test.WantErr != "" { + t.Fatalf("succeeded; want error: %s", test.WantErr) + } + case 1: + if test.WantErr == "" { + t.Fatalf("unexpected diagnostics: %s", diags.Err()) + } + if got, want := diags[0].Description().Detail, test.WantErr; got != want { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) + } + default: + t.Fatalf("too many diagnostics: %s", diags.Err()) + } + + if diags.HasErrors() { + return + } + + for _, problem := range deep.Equal(got, test.Want) { + t.Errorf(problem) + } + }) + } +}