diff --git a/decoder/expr_reference.go b/decoder/expr_reference.go index 0bffe263..4c87a1a7 100644 --- a/decoder/expr_reference.go +++ b/decoder/expr_reference.go @@ -1,9 +1,6 @@ package decoder import ( - "context" - - "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" ) @@ -13,8 +10,3 @@ type Reference struct { cons schema.Reference pathCtx *PathContext } - -func (ref Reference) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - // TODO - return nil -} diff --git a/decoder/expr_reference_ref_targets.go b/decoder/expr_reference_ref_targets.go new file mode 100644 index 00000000..2fdcb559 --- /dev/null +++ b/decoder/expr_reference_ref_targets.go @@ -0,0 +1,80 @@ +package decoder + +import ( + "context" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" +) + +func (ref Reference) ReferenceTargets(ctx context.Context, _ *TargetContext) reference.Targets { + if ref.cons.Address == nil { + return reference.Targets{} + } + + // deal with native HCL syntax first + eType, ok := ref.expr.(*hclsyntax.ScopeTraversalExpr) + if ok { + addr, err := lang.TraversalToAddress(eType.Traversal) + if err != nil { + return reference.Targets{} + } + + return reference.Targets{ + reference.Target{ + Addr: addr, + ScopeId: ref.cons.Address.ScopeId, + RangePtr: eType.SrcRange.Ptr(), + Name: ref.cons.Name, + }, + } + } + + if json.IsJSONExpression(ref.expr) { + // Given the limited AST/API access to JSON we can only + // guess whether the expression has exactly a single traversal + + vars := ref.expr.Variables() + if len(vars) != 1 { + return reference.Targets{} + } + + tRange := vars[0].SourceRange() + expectedExprRange := hcl.Range{ + Filename: tRange.Filename, + Start: hcl.Pos{ + Line: tRange.Start.Line, + // account for "${ + Column: tRange.Start.Column - 3, + Byte: tRange.Start.Byte - 3, + }, + End: hcl.Pos{ + Line: tRange.End.Line, + // account for }" + Column: tRange.End.Column + 2, + Byte: tRange.End.Byte + 2, + }, + } + + if rangesEqual(expectedExprRange, ref.expr.Range()) { + addr, err := lang.TraversalToAddress(vars[0]) + if err != nil { + return reference.Targets{} + } + + return reference.Targets{ + reference.Target{ + Addr: addr, + ScopeId: ref.cons.Address.ScopeId, + RangePtr: vars[0].SourceRange().Ptr(), + Name: ref.cons.Name, + }, + } + } + } + + return reference.Targets{} +} diff --git a/decoder/expr_reference_ref_targets_test.go b/decoder/expr_reference_ref_targets_test.go new file mode 100644 index 00000000..adf56e2a --- /dev/null +++ b/decoder/expr_reference_ref_targets_test.go @@ -0,0 +1,251 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprReference_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "no traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foo"), + }, + }, + IsOptional: true, + }, + }, + `attr = "foo"`, + reference.Targets{}, + }, + { + "wrapped traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foo"), + }, + }, + IsOptional: true, + }, + }, + `attr = "${foo}"`, + reference.Targets{}, + }, + { + "traversal with string", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foo"), + }, + }, + IsOptional: true, + }, + }, + `attr = "${foo}-bar"`, + reference.Targets{}, + }, + { + "non-addressable traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + OfType: cty.String, + }, + IsOptional: true, + }, + }, + `attr = foo`, + reference.Targets{}, + }, + { + "addressable traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + OfType: cty.String, + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foobar"), + }, + Name: "custom name", + }, + IsOptional: true, + }, + }, + `attr = foo`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("foobar"), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + }, + Name: "custom name", + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprReference_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "no traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foo"), + }, + }, + IsOptional: true, + }, + }, + `{"attr": "foo"}`, + reference.Targets{}, + }, + { + "traversal with string", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foo"), + }, + }, + IsOptional: true, + }, + }, + `{"attr": "${foo}-bar"}`, + reference.Targets{}, + }, + { + "non-addressable traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + OfType: cty.String, + }, + IsOptional: true, + }, + }, + `{"attr": "${foo}"}`, + reference.Targets{}, + }, + { + "addressable traversal", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Reference{ + OfType: cty.String, + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("foobar"), + }, + Name: "custom name", + }, + IsOptional: true, + }, + }, + `{"attr": "${foo}"}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("foobar"), + RangePtr: &hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Name: "custom name", + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.tf.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +}