From 2b1ae4035d7797e1077d15469c1475231f00e8b9 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Wed, 30 Nov 2022 11:51:43 +0100 Subject: [PATCH] Fix missing hover for `count` and `for_each` expression (#166) * Add test for missing hover on for_each expression * Add test for missing hover on count expression * Enable reference collection for count and for_each --- decoder/hover_test.go | 199 ++++++++++++++++++++++++++++++++++- decoder/reference_origins.go | 20 ++-- 2 files changed, 210 insertions(+), 9 deletions(-) diff --git a/decoder/hover_test.go b/decoder/hover_test.go index ce44d322..ea1b2acb 100644 --- a/decoder/hover_test.go +++ b/decoder/hover_test.go @@ -935,7 +935,7 @@ func TestDecoder_HoverAtPos_typeDeclaration(t *testing.T) { } } -func TestDecoder_HoverAtPos_extension(t *testing.T) { +func TestDecoder_HoverAtPos_extensions_count(t *testing.T) { testCases := []struct { name string bodySchema *schema.BodySchema @@ -1106,7 +1106,7 @@ func TestDecoder_HoverAtPos_extension(t *testing.T) { } } -func TestDecoder_HoverAtPos_foreach_extension(t *testing.T) { +func TestDecoder_HoverAtPos_extension_for_each(t *testing.T) { testCases := []struct { name string bodySchema *schema.BodySchema @@ -1395,7 +1395,7 @@ func TestDecoder_HoverAtPos_foreach_extension(t *testing.T) { } } -func TestDecoder_HoverAtPos_dynamic_extension(t *testing.T) { +func TestDecoder_HoverAtPos_extensions_dynamic(t *testing.T) { testCases := []struct { name string bodySchema *schema.BodySchema @@ -1540,3 +1540,196 @@ func TestDecoder_HoverAtPos_dynamic_extension(t *testing.T) { }) } } + +func TestDecoder_HoverAtPos_extensions_references(t *testing.T) { + testCases := []struct { + name string + bodySchema *schema.BodySchema + config string + pos hcl.Pos + expectedData *lang.HoverData + }{ + { + "for_each var reference", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "myblock": { + Labels: []*schema.LabelSchema{ + {Name: "type", IsDepKey: true}, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + ForEach: true, + }, + Attributes: map[string]*schema.AttributeSchema{ + "foo": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.String, + }, + }, + }, + }, + }, + }, + "variable": { + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.StaticStep{Name: "var"}, + schema.LabelStep{Index: 0}, + }, + ScopeId: lang.ScopeId("variable"), + AsReference: true, + AsTypeOf: &schema.BlockAsTypeOf{ + AttributeExpr: "type", + AttributeValue: "default", + }, + }, + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "type": { + Expr: schema.ExprConstraints{schema.TypeDeclarationExpr{}}, + IsOptional: true, + }, + }, + }, + }, + }, + }, + `myblock "foo" "bar" { + foo = each.value + for_each = var.name +} +variable "name" { + value = { key = "value" } +} +`, + hcl.Pos{Line: 3, Column: 19, Byte: 59}, + &lang.HoverData{ + Content: lang.Markdown("`var.name`\n_dynamic_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 14, Byte: 54}, + End: hcl.Pos{Line: 3, Column: 22, Byte: 62}, + }, + }, + }, + { + "count var reference", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "myblock": { + Labels: []*schema.LabelSchema{ + {Name: "type", IsDepKey: true}, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + Count: true, + }, + Attributes: map[string]*schema.AttributeSchema{ + "foo": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + }, + }, + }, + }, + }, + "variable": { + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.StaticStep{Name: "var"}, + schema.LabelStep{Index: 0}, + }, + ScopeId: lang.ScopeId("variable"), + AsReference: true, + AsTypeOf: &schema.BlockAsTypeOf{ + AttributeExpr: "type", + AttributeValue: "default", + }, + }, + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "type": { + Expr: schema.ExprConstraints{schema.TypeDeclarationExpr{}}, + IsOptional: true, + }, + }, + }, + }, + }, + }, + `myblock "foo" "bar" { + foo = count.index + count = var.name +} +variable "name" { + value = 4 +} +`, + hcl.Pos{Line: 3, Column: 16, Byte: 57}, + &lang.HoverData{ + Content: lang.Markdown("`var.name`\n_dynamic_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 11, Byte: 52}, + End: hcl.Pos{Line: 3, Column: 19, Byte: 60}, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + ctx := context.Background() + + f, diags := hclsyntax.ParseConfig([]byte(tc.config), "test.tf", hcl.InitialPos) + if diags != nil { + t.Fatal(diags) + } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + origins, err := d.CollectReferenceOrigins() + if err != nil { + t.Fatal(err) + } + d = testPathDecoder(t, &PathContext{ + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + ReferenceTargets: targets, + ReferenceOrigins: origins, + }) + + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tc.expectedData, data, ctydebug.CmpOptions); diff != "" { + t.Fatalf("hover data mismatch: %s", diff) + } + }) + } +} diff --git a/decoder/reference_origins.go b/decoder/reference_origins.go index 9e9bbf7a..3ae3454d 100644 --- a/decoder/reference_origins.go +++ b/decoder/reference_origins.go @@ -122,13 +122,21 @@ func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.B content := decodeBody(body, bodySchema) for _, attr := range content.Attributes { - aSchema, ok := bodySchema.Attributes[attr.Name] - if !ok { - if bodySchema.AnyAttribute == nil { - // skip unknown attribute - continue + var aSchema *schema.AttributeSchema + if bodySchema.Extensions != nil && bodySchema.Extensions.Count && attr.Name == "count" { + aSchema = countAttributeSchema() + } else if bodySchema.Extensions != nil && bodySchema.Extensions.ForEach && attr.Name == "for_each" { + aSchema = forEachAttributeSchema() + } else { + var ok bool + aSchema, ok = bodySchema.Attributes[attr.Name] + if !ok { + if bodySchema.AnyAttribute == nil { + // skip unknown attribute + continue + } + aSchema = bodySchema.AnyAttribute } - aSchema = bodySchema.AnyAttribute } if aSchema.OriginForTarget != nil {