Skip to content

Commit

Permalink
decoder: Provide reference origins for ForExpr
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Jan 26, 2024
1 parent e9d9df1 commit 58d40c5
Show file tree
Hide file tree
Showing 3 changed files with 335 additions and 1 deletion.
62 changes: 62 additions & 0 deletions decoder/expr_any_for.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"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"
Expand Down Expand Up @@ -157,6 +158,67 @@ func (a Any) semanticTokensForForExpr(ctx context.Context) ([]lang.SemanticToken
return tokens, false
}

func (a Any) refOriginsForForExpr(ctx context.Context, allowSelfRefs bool) (reference.Origins, bool) {
origins := make(reference.Origins, 0)

// There is currently no way of decoding for expressions in JSON
// so we just collect them using the fallback logic assuming "any"
// constraint and focus on collecting expressions in HCL with more
// accurate constraints below.

switch eType := a.expr.(type) {
case *hclsyntax.ForExpr:
if !isTypeIterable(a.cons.OfType) {
return nil, false
}

// TODO: eType.KeyVarExpr.Range() to collect key as origin
// TODO: eType.ValVarExpr.Range() to collect value as origin

if collExpr, ok := newExpression(a.pathCtx, eType.CollExpr, a.cons).(ReferenceOriginsExpression); ok {
origins = append(origins, collExpr.ReferenceOrigins(ctx, allowSelfRefs)...)
}

if eType.KeyExpr != nil {
typ, ok := iterableKeyType(a.cons.OfType)
if !ok {
return nil, false
}
cons := schema.AnyExpression{
OfType: typ,
}
if keyExpr, ok := newExpression(a.pathCtx, eType.KeyExpr, cons).(ReferenceOriginsExpression); ok {
origins = append(origins, keyExpr.ReferenceOrigins(ctx, allowSelfRefs)...)
}
}

typ, ok := iterableValueType(a.cons.OfType)
if !ok {
return nil, false
}
cons := schema.AnyExpression{
OfType: typ,
}
if valExpr, ok := newExpression(a.pathCtx, eType.ValExpr, cons).(ReferenceOriginsExpression); ok {
origins = append(origins, valExpr.ReferenceOrigins(ctx, allowSelfRefs)...)
}

if eType.CondExpr != nil {
cons := schema.AnyExpression{
OfType: cty.Bool,
}

if condExpr, ok := newExpression(a.pathCtx, eType.CondExpr, cons).(ReferenceOriginsExpression); ok {
origins = append(origins, condExpr.ReferenceOrigins(ctx, allowSelfRefs)...)
}
}

return origins, true
}

return origins, false
}

func isTypeIterable(typ cty.Type) bool {
if typ == cty.DynamicPseudoType {
return true
Expand Down
5 changes: 4 additions & 1 deletion decoder/expr_any_ref_origins.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ func (a Any) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference

func (a Any) refOriginsForNonComplexExpr(ctx context.Context, allowSelfRefs bool) reference.Origins {
// TODO: Support splat expression https://github.com/hashicorp/terraform-ls/issues/526
// TODO: Support for-in-if expression https://github.com/hashicorp/terraform-ls/issues/527
// TODO: Support relative traversals https://github.com/hashicorp/terraform-ls/issues/532

if origins, ok := a.refOriginsForOperatorExpr(ctx, allowSelfRefs); ok {
Expand All @@ -132,6 +131,10 @@ func (a Any) refOriginsForNonComplexExpr(ctx context.Context, allowSelfRefs bool
return origins
}

if origins, ok := a.refOriginsForForExpr(ctx, allowSelfRefs); ok {
return origins
}

// attempt to get accurate constraint for the origins
// if we recognise the given expression
funcExpr := functionExpr{
Expand Down
269 changes: 269 additions & 0 deletions decoder/expr_any_ref_origins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,275 @@ func TestCollectRefOrigins_exprAny_parenthesis_hcl(t *testing.T) {
}
}

func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) {
testCases := []struct {
testName string
attrSchema map[string]*schema.AttributeSchema
cfg string
expectedRefOrigins reference.Origins
}{
{
"list",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.AnyExpression{
OfType: cty.List(cty.String),
},
},
},
`attr = [for key, val in var.coll: val]
`,
reference.Origins{
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "coll"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
End: hcl.Pos{Line: 1, Column: 33, Byte: 32},
},
Constraints: reference.OriginConstraints{
{OfType: cty.List(cty.String)},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "val"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 35, Byte: 34},
End: hcl.Pos{Line: 1, Column: 38, Byte: 37},
},
Constraints: reference.OriginConstraints{
{OfType: cty.String},
},
},
},
},
{
"set",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.AnyExpression{
OfType: cty.Set(cty.String),
},
},
},
`attr = [for key, val in var.coll: val]
`,
reference.Origins{
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "coll"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
End: hcl.Pos{Line: 1, Column: 33, Byte: 32},
},
Constraints: reference.OriginConstraints{
{OfType: cty.Set(cty.String)},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "val"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 35, Byte: 34},
End: hcl.Pos{Line: 1, Column: 38, Byte: 37},
},
Constraints: reference.OriginConstraints{
{OfType: cty.String},
},
},
},
},
{
"tuple",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.AnyExpression{
OfType: cty.EmptyTuple,
},
},
},
`attr = [for key, val in var.coll: val]
`,
reference.Origins{
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "coll"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
End: hcl.Pos{Line: 1, Column: 33, Byte: 32},
},
Constraints: reference.OriginConstraints{
{OfType: cty.EmptyTuple},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "val"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 35, Byte: 34},
End: hcl.Pos{Line: 1, Column: 38, Byte: 37},
},
Constraints: reference.OriginConstraints{
{OfType: cty.DynamicPseudoType},
},
},
},
},
{
"map",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.AnyExpression{
OfType: cty.Map(cty.String),
},
},
},
`attr = {for key, val in var.coll: key => val}
`,
reference.Origins{
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "coll"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
End: hcl.Pos{Line: 1, Column: 33, Byte: 32},
},
Constraints: reference.OriginConstraints{
{OfType: cty.Map(cty.String)},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "key"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 35, Byte: 34},
End: hcl.Pos{Line: 1, Column: 38, Byte: 37},
},
Constraints: reference.OriginConstraints{
{OfType: cty.String},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "val"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 42, Byte: 41},
End: hcl.Pos{Line: 1, Column: 45, Byte: 44},
},
Constraints: reference.OriginConstraints{
{OfType: cty.String},
},
},
},
},
{
"object",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.AnyExpression{
OfType: cty.EmptyObject,
},
},
},
`attr = {for key, val in var.coll: key => val}
`,
reference.Origins{
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "coll"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
End: hcl.Pos{Line: 1, Column: 33, Byte: 32},
},
Constraints: reference.OriginConstraints{
{OfType: cty.EmptyObject},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "key"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 35, Byte: 34},
End: hcl.Pos{Line: 1, Column: 38, Byte: 37},
},
Constraints: reference.OriginConstraints{
{OfType: cty.String},
},
},
reference.LocalOrigin{
Addr: lang.Address{
lang.RootStep{Name: "val"},
},
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 42, Byte: 41},
End: hcl.Pos{Line: 1, Column: 45, Byte: 44},
},
Constraints: reference.OriginConstraints{
{OfType: cty.DynamicPseudoType},
},
},
},
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%2d-%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,
},
})

origins, err := d.CollectReferenceOrigins()
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expectedRefOrigins, origins, ctydebug.CmpOptions); diff != "" {
t.Fatalf("unexpected origins: %s", diff)
}
})
}
}

func TestCollectRefOrigins_exprAny_operators_json(t *testing.T) {
testCases := []struct {
testName string
Expand Down

0 comments on commit 58d40c5

Please sign in to comment.