diff --git a/decoder/hover.go b/decoder/hover.go index 7cbc5ad6..c1b706e7 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -1,19 +1,22 @@ package decoder import ( + "context" "fmt" "sort" "strings" + "github.com/zclconf/go-cty/cty" + + icontext "github.com/hashicorp/hcl-lang/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" - "github.com/zclconf/go-cty/cty" ) -func (d *PathDecoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) HoverAtPos(ctx context.Context, filename string, pos hcl.Pos) (*lang.HoverData, error) { f, err := d.fileByName(filename) if err != nil { return nil, err @@ -28,7 +31,7 @@ func (d *PathDecoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, return nil, &NoSchemaError{} } - data, err := d.hoverAtPos(rootBody, d.pathCtx.Schema, pos) + data, err := d.hoverAtPos(ctx, rootBody, d.pathCtx.Schema, pos) if err != nil { return nil, err } @@ -36,15 +39,38 @@ func (d *PathDecoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, return data, nil } -func (d *PathDecoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) hoverAtPos(ctx context.Context, body *hclsyntax.Body, bodySchema *schema.BodySchema, pos hcl.Pos) (*lang.HoverData, error) { if bodySchema == nil { return nil, nil } filename := body.Range().Filename + if bodySchema.Extensions != nil { + ctx = icontext.WithExtensions(ctx, bodySchema.Extensions) + if bodySchema.Extensions.Count { + if _, ok := body.Attributes["count"]; ok { + // append to context we need count provided + ctx = icontext.WithActiveCount(ctx) + } + } + } + for name, attr := range body.Attributes { if attr.Range().ContainsPos(pos) { + + if bodySchema.Extensions != nil { + if name == "count" && bodySchema.Extensions.Count { + return &lang.HoverData{ + Content: lang.MarkupContent{ + Kind: lang.MarkdownKind, + Value: "**count** _optional, number_\n\nThe distinct index number (starting with 0) corresponding to the instance", + }, + Range: attr.Range(), + }, nil + } + } + aSchema, ok := bodySchema.Attributes[attr.Name] if !ok { if bodySchema.AnyAttribute == nil { @@ -66,7 +92,7 @@ func (d *PathDecoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySc if attr.Expr.Range().ContainsPos(pos) { exprCons := ExprConstraints(aSchema.Expr) - data, err := d.hoverDataForExpr(attr.Expr, exprCons, 0, pos) + data, err := d.hoverDataForExpr(ctx, attr.Expr, exprCons, 0, pos) if err != nil { return nil, &PositionalError{ Filename: filename, @@ -128,7 +154,7 @@ func (d *PathDecoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySc return nil, err } - return d.hoverAtPos(block.Body, mergedSchema, pos) + return d.hoverAtPos(ctx, block.Body, mergedSchema, pos) } } } @@ -215,7 +241,7 @@ func (d *PathDecoder) hoverContentForBlock(bType string, schema *schema.BlockSch } } -func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprConstraints, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) hoverDataForExpr(ctx context.Context, expr hcl.Expression, constraints ExprConstraints, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { switch e := expr.(type) { case *hclsyntax.ScopeTraversalExpr: kw, ok := constraints.KeywordExpr() @@ -232,6 +258,21 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons }, nil } + address, _ := lang.TraversalToAddress(e.AsTraversal()) + if address.Equals(lang.Address{ + lang.RootStep{ + Name: "count", + }, + lang.AttrStep{ + Name: "index", + }, + }) && icontext.ActiveCountFromContext(ctx) { + return &lang.HoverData{ + Content: lang.Markdown("**count.index** fooooo"), + Range: expr.Range(), + }, nil + } + tes, ok := constraints.TraversalExprs() if ok { content, err := d.hoverContentForTraversalExpr(e.AsTraversal(), tes) @@ -261,7 +302,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons } case *hclsyntax.TemplateExpr: if e.IsStringLiteral() { - data, err := d.hoverDataForExpr(e.Parts[0], constraints, nestingLvl, pos) + data, err := d.hoverDataForExpr(ctx, e.Parts[0], constraints, nestingLvl, pos) if err != nil { return nil, err } @@ -295,7 +336,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons } } case *hclsyntax.TemplateWrapExpr: - data, err := d.hoverDataForExpr(e.Wrapped, constraints, nestingLvl, pos) + data, err := d.hoverDataForExpr(ctx, e.Wrapped, constraints, nestingLvl, pos) if err != nil { return nil, err } @@ -320,7 +361,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons if ok { for _, elemExpr := range e.Exprs { if elemExpr.Range().ContainsPos(pos) { - return d.hoverDataForExpr(elemExpr, ExprConstraints(se.Elem), nestingLvl, pos) + return d.hoverDataForExpr(ctx, elemExpr, ExprConstraints(se.Elem), nestingLvl, pos) } } content := fmt.Sprintf("_%s_", se.FriendlyName()) @@ -336,7 +377,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons if ok { for _, elemExpr := range e.Exprs { if elemExpr.Range().ContainsPos(pos) { - return d.hoverDataForExpr(elemExpr, ExprConstraints(le.Elem), nestingLvl, pos) + return d.hoverDataForExpr(ctx, elemExpr, ExprConstraints(le.Elem), nestingLvl, pos) } } content := fmt.Sprintf("_%s_", le.FriendlyName()) @@ -356,7 +397,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons return nil, &ConstraintMismatch{elemExpr} } ec := ExprConstraints(te.Elems[i]) - return d.hoverDataForExpr(elemExpr, ec, nestingLvl, pos) + return d.hoverDataForExpr(ctx, elemExpr, ec, nestingLvl, pos) } } content := fmt.Sprintf("_%s_", te.FriendlyName()) @@ -393,7 +434,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons case *hclsyntax.ObjectConsExpr: objExpr, ok := constraints.ObjectExpr() if ok { - return d.hoverDataForObjectExpr(e, objExpr, nestingLvl, pos) + return d.hoverDataForObjectExpr(ctx, e, objExpr, nestingLvl, pos) } mapExpr, ok := constraints.MapExpr() if ok { @@ -469,7 +510,7 @@ func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprCons return nil, fmt.Errorf("unsupported expression (%T)", expr) } -func (d *PathDecoder) hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, oe schema.ObjectExpr, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) hoverDataForObjectExpr(ctx context.Context, objExpr *hclsyntax.ObjectConsExpr, oe schema.ObjectExpr, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { declaredAttributes := make(map[string]hclsyntax.Expression, 0) for _, item := range objExpr.Items { key, _ := item.KeyExpr.Value(nil) @@ -485,7 +526,7 @@ func (d *PathDecoder) hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, } if item.ValueExpr.Range().ContainsPos(pos) { - return d.hoverDataForExpr(item.ValueExpr, ExprConstraints(attr.Expr), nestingLvl+1, pos) + return d.hoverDataForExpr(ctx, item.ValueExpr, ExprConstraints(attr.Expr), nestingLvl+1, pos) } itemRng := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) @@ -526,7 +567,7 @@ func (d *PathDecoder) hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, attrData := ec.FriendlyName() if attrExpr, ok := declaredAttributes[name]; ok { - data, err := d.hoverDataForExpr(attrExpr, ExprConstraints(ec), nestingLvl+1, pos) + data, err := d.hoverDataForExpr(ctx, attrExpr, ExprConstraints(ec), nestingLvl+1, pos) if err == nil && data.Content.Value != "" { attrData = data.Content.Value } diff --git a/decoder/hover_expressions_test.go b/decoder/hover_expressions_test.go index c05c6c44..1ff634b6 100644 --- a/decoder/hover_expressions_test.go +++ b/decoder/hover_expressions_test.go @@ -1,18 +1,20 @@ package decoder import ( + "context" "errors" "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "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/zclconf/go-cty-debug/ctydebug" - "github.com/zclconf/go-cty/cty" ) func TestDecoder_HoverAtPos_expressions(t *testing.T) { @@ -1282,7 +1284,8 @@ _object_`), }, }) - data, err := d.HoverAtPos("test.tf", tc.pos) + ctx := context.Background() + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err != nil { if tc.expectedErr != nil && !errors.As(err, &tc.expectedErr) { @@ -1483,7 +1486,8 @@ func TestDecoder_HoverAtPos_traversalExpressions(t *testing.T) { }, }) - data, err := d.HoverAtPos("test.tf", tc.pos) + ctx := context.Background() + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err != nil { if tc.expectedErr != nil && !errors.As(err, &tc.expectedErr) { t.Fatalf("unexpected error: %s\nexpected: %s\n", diff --git a/decoder/hover_test.go b/decoder/hover_test.go index cd03d914..5dd43adc 100644 --- a/decoder/hover_test.go +++ b/decoder/hover_test.go @@ -1,19 +1,21 @@ package decoder import ( + "context" "errors" "fmt" "strings" "testing" "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/hcl-lang/lang" "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 TestDecoder_HoverAtPos_noSchema(t *testing.T) { @@ -28,7 +30,8 @@ func TestDecoder_HoverAtPos_noSchema(t *testing.T) { }, }) - _, err := d.HoverAtPos("test.tf", hcl.InitialPos) + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf", hcl.InitialPos) noSchemaErr := &NoSchemaError{} if !errors.As(err, &noSchemaErr) { t.Fatal("expected NoSchemaError for no schema") @@ -46,7 +49,8 @@ func TestDecoder_HoverAtPos_emptyBody(t *testing.T) { }, }) - _, err := d.HoverAtPos("test.tf", hcl.InitialPos) + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for empty body") @@ -69,7 +73,8 @@ func TestDecoder_HoverAtPos_json(t *testing.T) { }, }) - _, err := d.HoverAtPos("test.tf.json", hcl.InitialPos) + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf.json", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for JSON body") @@ -232,6 +237,8 @@ func TestDecoder_HoverAtPos_nilBodySchema(t *testing.T) { }, } + ctx := context.Background() + for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { f, pDiags := hclsyntax.ParseConfig([]byte(tc.config), "test.tf", hcl.InitialPos) @@ -245,8 +252,7 @@ func TestDecoder_HoverAtPos_nilBodySchema(t *testing.T) { "test.tf": f, }, }) - - data, err := d.HoverAtPos("test.tf", tc.pos) + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err != nil { t.Fatal(err) } @@ -292,7 +298,8 @@ func TestDecoder_HoverAtPos_unknownAttribute(t *testing.T) { }, }) - _, err := d.HoverAtPos("test.tf", hcl.Pos{ + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf", hcl.Pos{ Line: 2, Column: 6, Byte: 32, @@ -339,7 +346,8 @@ func TestDecoder_HoverAtPos_unknownBlock(t *testing.T) { }, }) - _, err := d.HoverAtPos("test.tf", hcl.Pos{ + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf", hcl.Pos{ Line: 2, Column: 1, Byte: 23, @@ -417,7 +425,8 @@ func TestDecoder_HoverAtPos_invalidBlockPositions(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - _, err := d.HoverAtPos("test.tf", tc.pos) + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err == nil { t.Fatal("expected error") } @@ -460,7 +469,8 @@ func TestDecoder_HoverAtPos_rightHandSide(t *testing.T) { }, }) - data, err := d.HoverAtPos("test.tf", hcl.Pos{ + ctx := context.Background() + data, err := d.HoverAtPos(ctx, "test.tf", hcl.Pos{ Line: 2, Column: 17, Byte: 32, @@ -636,7 +646,8 @@ func TestDecoder_HoverAtPos_basic(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - data, err := d.HoverAtPos("test.tf", tc.pos) + ctx := context.Background() + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err != nil { t.Fatal(err) } @@ -810,7 +821,8 @@ My food block }, }) - data, err := d.HoverAtPos("test.tf", tc.pos) + ctx := context.Background() + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err != nil { t.Fatal(err) } @@ -910,8 +922,91 @@ func TestDecoder_HoverAtPos_typeDeclaration(t *testing.T) { }, }) + ctx := context.Background() pos := hcl.Pos{Line: 2, Column: 6, Byte: 32} - data, err := d.HoverAtPos("test.tf", pos) + data, err := d.HoverAtPos(ctx, "test.tf", pos) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tc.expectedData, data, ctydebug.CmpOptions); diff != "" { + t.Fatalf("hover data mismatch: %s", diff) + } + }) + } +} + +func TestDecoder_HoverAtPos_expression_extension(t *testing.T) { + resourceLabelSchema := []*schema.LabelSchema{ + {Name: "type", IsDepKey: true}, + {Name: "name"}, + } + blockSchema := &schema.BlockSchema{ + Labels: resourceLabelSchema, + Description: lang.Markdown("My special block"), + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + Count: true, + }, + Attributes: map[string]*schema.AttributeSchema{ + "num_attr": {Expr: schema.LiteralTypeOnly(cty.Number)}, + "str_attr": { + Expr: schema.LiteralTypeOnly(cty.String), + IsOptional: true, + Description: lang.PlainText("Special attribute"), + }, + "bool_attr": { + Expr: schema.LiteralTypeOnly(cty.Bool), + IsSensitive: true, + Description: lang.PlainText("Flag attribute"), + }, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{}, + } + bodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "myblock": blockSchema, + }, + } + testConfig := []byte(`myblock "foo" "bar" { + count = 1 + num_attr = 4 + bool_attr = true +} +`) + + f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + testCases := []struct { + name string + pos hcl.Pos + expectedData *lang.HoverData + }{ + { + "optional attribute name", + hcl.Pos{Line: 2, Column: 5, Byte: 24}, + &lang.HoverData{ + Content: lang.Markdown("**count** _optional, number_\n\nThe distinct index number (starting with 0) corresponding to the instance"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 24}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 33}, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + ctx := context.Background() + data, err := d.HoverAtPos(ctx, "test.tf", tc.pos) if err != nil { t.Fatal(err) }