From 680c316fe1b4e9bf5ad8ffe01ed6996ab749aa72 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 15 Mar 2023 14:17:04 +0000 Subject: [PATCH] decoder: Implement hover for LiteralType --- decoder/expr_literal_type.go | 5 - decoder/expr_literal_type_hover.go | 111 ++++++ decoder/expr_literal_type_hover_test.go | 441 ++++++++++++++++++++++++ 3 files changed, 552 insertions(+), 5 deletions(-) create mode 100644 decoder/expr_literal_type_hover.go create mode 100644 decoder/expr_literal_type_hover_test.go diff --git a/decoder/expr_literal_type.go b/decoder/expr_literal_type.go index af3fb135..f6303c7a 100644 --- a/decoder/expr_literal_type.go +++ b/decoder/expr_literal_type.go @@ -16,11 +16,6 @@ type LiteralType struct { pathCtx *PathContext } -func (lt LiteralType) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { - // TODO - return nil -} - func (lt LiteralType) SemanticTokens(ctx context.Context) []lang.SemanticToken { // TODO return nil diff --git a/decoder/expr_literal_type_hover.go b/decoder/expr_literal_type_hover.go new file mode 100644 index 00000000..7ba04f16 --- /dev/null +++ b/decoder/expr_literal_type_hover.go @@ -0,0 +1,111 @@ +package decoder + +import ( + "context" + + "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/zclconf/go-cty/cty" +) + +func (lt LiteralType) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { + typ := lt.cons.Type + + if typ == cty.Bool { + return lt.hoverBoolAtPos(ctx, pos) + } + + if typ.IsListType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return nil + } + + cons := schema.List{ + Elem: schema.LiteralType{ + Type: typ.ElementType(), + }, + } + + return newExpression(lt.pathCtx, expr, cons).HoverAtPos(ctx, pos) + } + + if typ.IsSetType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return nil + } + + cons := schema.Set{ + Elem: schema.LiteralType{ + Type: typ.ElementType(), + }, + } + + return newExpression(lt.pathCtx, expr, cons).HoverAtPos(ctx, pos) + } + + if typ.IsTupleType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return nil + } + + elemTypes := typ.TupleElementTypes() + cons := schema.Tuple{ + Elems: make([]schema.Constraint, len(elemTypes)), + } + for i, elemType := range elemTypes { + cons.Elems[i] = schema.LiteralType{ + Type: elemType, + } + } + + return newExpression(lt.pathCtx, expr, cons).HoverAtPos(ctx, pos) + } + + if typ.IsMapType() { + expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return nil + } + + cons := schema.Map{ + Elem: schema.LiteralType{ + Type: typ.ElementType(), + }, + } + return newExpression(lt.pathCtx, expr, cons).HoverAtPos(ctx, pos) + } + + if typ.IsObjectType() { + expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return nil + } + + cons := schema.Object{ + Attributes: ctyObjectToObjectAttributes(typ), + } + return newExpression(lt.pathCtx, expr, cons).HoverAtPos(ctx, pos) + } + + return nil +} + +func (lt LiteralType) hoverBoolAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { + expr, ok := lt.expr.(*hclsyntax.LiteralValueExpr) + if !ok { + return nil + } + if expr.Val.Type() != cty.Bool { + return nil + } + + return &lang.HoverData{ + Content: lang.Markdown(`_bool_`), + Range: expr.Range(), + } +} diff --git a/decoder/expr_literal_type_hover_test.go b/decoder/expr_literal_type_hover_test.go new file mode 100644 index 00000000..6d3dee6d --- /dev/null +++ b/decoder/expr_literal_type_hover_test.go @@ -0,0 +1,441 @@ +package decoder + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "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/zclconf/go-cty/cty" +) + +func TestHoverAtPos_exprLiteralType(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedHoverData *lang.HoverData + }{ + { + "boolean", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Bool, + }, + }, + }, + `attr = false`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + }, + }, + { + "empty single-line object without attributes", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{}), + }, + }, + }, + `attr = {}`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("_object_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + "empty multi-line object without attributes", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{}), + }, + }, + }, + `attr = { + +}`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + &lang.HoverData{ + Content: lang.Markdown("_object_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 13}, + }, + }, + }, + { + "empty single-line object with attributes", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + }, + }, + }, + `attr = {}`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("```\n{\n foo = string # optional\n}\n```\n_object_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + "empty multi-line object with attributes", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + }, + }, + }, + `attr = { + +}`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + &lang.HoverData{ + Content: lang.Markdown("```\n{\n foo = string # optional\n}\n```\n_object_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 13}, + }, + }, + }, + { + "single item object on valid attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + }, + }, + }, + `attr = { + foo = "fooba" +}`, + hcl.Pos{Line: 2, Column: 5, Byte: 13}, + &lang.HoverData{ + Content: lang.Markdown("**foo** _optional, string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 16, Byte: 24}, + }, + }, + }, + { + "single item object on invalid attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + }, + }, + }, + `attr = { + bar = "fooba" +}`, + hcl.Pos{Line: 2, Column: 5, Byte: 13}, + &lang.HoverData{ + Content: lang.Markdown("```" + ` +{ + foo = string +} +` + "```\n" + `_object_`), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 26}, + }, + }, + }, + // { + // "multi item object on valid attribute name", + // map[string]*schema.AttributeSchema{ + // "attr": { + // Constraint: schema.LiteralType{ + // Attributes: schema.ObjectAttributes{ + // "foo": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordfoo", + // }, + // }, + // "bar": { + // IsRequired: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordbar", + // }, + // }, + // "baz": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordbaz", + // }, + // }, + // }, + // }, + // }, + // }, + // `attr = { + // foo = keywordfoo + // bar = keywordbar + // baz = keywordbaz + // }`, + // hcl.Pos{Line: 3, Column: 5, Byte: 32}, + // &lang.HoverData{ + // Content: lang.Markdown("**bar** _required, keyword_"), + // Range: hcl.Range{ + // Filename: "test.tf", + // Start: hcl.Pos{Line: 3, Column: 3, Byte: 30}, + // End: hcl.Pos{Line: 3, Column: 19, Byte: 46}, + // }, + // }, + // }, + // { + // "multi item object on matching value", + // map[string]*schema.AttributeSchema{ + // "attr": { + // Constraint: schema.LiteralType{ + // Attributes: schema.ObjectAttributes{ + // "foo": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordfoo", + // }, + // }, + // "bar": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordbar", + // }, + // }, + // "baz": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordbaz", + // }, + // }, + // }, + // }, + // }, + // }, + // `attr = { + // foo = invalid + // bar = keywordbar + // baz = keywordbaz + // }`, + // hcl.Pos{Line: 3, Column: 16, Byte: 40}, + // &lang.HoverData{ + // Content: lang.Markdown("`keywordbar` _keyword_"), + // Range: hcl.Range{ + // Filename: "test.tf", + // Start: hcl.Pos{Line: 3, Column: 9, Byte: 33}, + // End: hcl.Pos{Line: 3, Column: 19, Byte: 43}, + // }, + // }, + // }, + // { + // "multi item object on mismatching value", + // map[string]*schema.AttributeSchema{ + // "attr": { + // Constraint: schema.LiteralType{ + // Attributes: schema.ObjectAttributes{ + // "foo": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordfoo", + // }, + // }, + // "bar": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordbar", + // }, + // }, + // "baz": { + // IsOptional: true, + // Constraint: schema.Keyword{ + // Keyword: "keywordbaz", + // }, + // }, + // }, + // }, + // }, + // }, + // `attr = { + // foo = invalid + // bar = keywordbar + // baz = keywordbaz + // }`, + // hcl.Pos{Line: 2, Column: 13, Byte: 21}, + // nil, + // }, + // { + // "multi item object in empty space", + // map[string]*schema.AttributeSchema{ + // "attr": { + // Constraint: schema.LiteralType{ + // Attributes: schema.ObjectAttributes{ + // "foo": { + // IsOptional: true, + // Constraint: schema.LiteralType{ + // Type: cty.Number, + // }, + // }, + // "bar": { + // IsOptional: true, + // Constraint: schema.LiteralType{ + // Type: cty.String, + // }, + // }, + // "baz": { + // IsOptional: true, + // Constraint: schema.LiteralType{ + // Type: cty.String, + // }, + // }, + // }, + // }, + // }, + // }, + // `attr = { + // bar = "bar" + // baz = "baz" + // }`, + // hcl.Pos{Line: 2, Column: 2, Byte: 10}, + // &lang.HoverData{ + // Content: lang.Markdown("```" + ` + // { + // bar = string # optional + // baz = string # optional + // foo = number # optional + // } + // ` + "```\n_object_"), + // Range: hcl.Range{ + // Filename: "test.tf", + // Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + // End: hcl.Pos{Line: 4, Column: 2, Byte: 38}, + // }, + // }, + // }, + // { + // "multi item nested object", + // map[string]*schema.AttributeSchema{ + // "attr": { + // Constraint: schema.LiteralType{ + // Attributes: schema.ObjectAttributes{ + // "foo": { + // IsOptional: true, + // Constraint: schema.LiteralType{ + // Type: cty.Number, + // }, + // }, + // "bar": { + // IsOptional: true, + // Constraint: schema.LiteralType{ + // Attributes: schema.ObjectAttributes{ + // "noot": { + // IsRequired: true, + // Constraint: schema.LiteralType{Type: cty.Bool}, + // }, + // "animal": { + // IsOptional: true, + // Constraint: schema.LiteralType{Type: cty.String}, + // }, + // }, + // }, + // }, + // "baz": { + // IsOptional: true, + // Constraint: schema.LiteralType{ + // Type: cty.String, + // }, + // }, + // }, + // }, + // }, + // }, + // `attr = { + // bar = {} + // baz = "baz" + // }`, + // hcl.Pos{Line: 2, Column: 2, Byte: 10}, + // &lang.HoverData{ + // Content: lang.Markdown("```" + ` + // { + // bar = { + // animal = string # optional + // noot = bool + // } # optional + // baz = string # optional + // foo = number # optional + // } + // ` + "```\n_object_"), + // Range: hcl.Range{ + // Filename: "test.tf", + // Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + // End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + // }, + // }, + // }, + } + + 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, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + ctx := context.Background() + hoverData, err := d.HoverAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedHoverData, hoverData); diff != "" { + t.Fatalf("unexpected hover data: %s", diff) + } + }) + } +}