Skip to content

Commit

Permalink
decoder: Implement reference targets for Reference
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Mar 22, 2023
1 parent 363e3a2 commit 871393e
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 8 deletions.
8 changes: 0 additions & 8 deletions decoder/expr_reference.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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
}
80 changes: 80 additions & 0 deletions decoder/expr_reference_ref_targets.go
Original file line number Diff line number Diff line change
@@ -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{}
}
251 changes: 251 additions & 0 deletions decoder/expr_reference_ref_targets_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 871393e

Please sign in to comment.