| package addrs |
| |
| import ( |
| "testing" |
| |
| "github.com/go-test/deep" |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/hcl/v2/hclsyntax" |
| "github.com/hashicorp/terraform/internal/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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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.`, |
| }, |
| |
| // each |
| { |
| `each.key`, |
| &Reference{ |
| Subject: ForEachAttr{ |
| Name: "key", |
| }, |
| SourceRange: tfdiags.SourceRange{ |
| Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, |
| End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `each.value.blah`, |
| &Reference{ |
| Subject: ForEachAttr{ |
| Name: "value", |
| }, |
| SourceRange: tfdiags.SourceRange{ |
| Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, |
| End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, |
| }, |
| Remaining: hcl.Traversal{ |
| hcl.TraverseAttr{ |
| Name: "blah", |
| SrcRange: hcl.Range{ |
| Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, |
| End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, |
| }, |
| }, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `each`, |
| nil, |
| `The "each" object cannot be accessed directly. Instead, access one of its attributes.`, |
| }, |
| { |
| `each["hello"]`, |
| nil, |
| `The "each" object does not support this operation.`, |
| }, |
| // data |
| { |
| `data.external.foo`, |
| &Reference{ |
| Subject: 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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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: ModuleCall{ |
| Name: "foo", |
| }, |
| SourceRange: tfdiags.SourceRange{ |
| Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, |
| End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `module.foo.bar`, |
| &Reference{ |
| Subject: ModuleCallInstanceOutput{ |
| 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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `module.foo.bar.baz`, |
| &Reference{ |
| Subject: ModuleCallInstanceOutput{ |
| 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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `module.foo["baz"].bar`, |
| &Reference{ |
| Subject: ModuleCallInstanceOutput{ |
| 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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `module.foo["baz"].bar.boop`, |
| &Reference{ |
| Subject: ModuleCallInstanceOutput{ |
| 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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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.`, |
| }, |
| |
| // the "resource" prefix forces interpreting the next name as a |
| // resource type name. This is an alias for just using a resource |
| // type name at the top level, to be used only if a later edition |
| // of the Terraform language introduces a new reserved word that |
| // overlaps with a resource type name. |
| { |
| `resource.boop_instance.foo`, |
| &Reference{ |
| Subject: 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: 27, Byte: 26}, |
| }, |
| }, |
| ``, |
| }, |
| |
| // We have some names reserved which might be used by a |
| // still-under-discussion proposal for template values or lazy |
| // expressions. |
| { |
| `template.foo`, |
| nil, |
| `The symbol name "template" 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.`, |
| }, |
| { |
| `lazy.foo`, |
| nil, |
| `The symbol name "lazy" 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.`, |
| }, |
| { |
| `arg.foo`, |
| nil, |
| `The symbol name "arg" 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.`, |
| }, |
| |
| // anything else, interpreted as a managed resource reference |
| { |
| `boop_instance.foo`, |
| &Reference{ |
| Subject: 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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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}, |
| }, |
| }, |
| ``, |
| }, |
| { |
| `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) |
| } |
| }) |
| } |
| } |