From 344607b83cf289f4846ce59f7b1774aba29fa3e2 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 25 Sep 2023 16:13:49 +0100 Subject: [PATCH 1/5] decoder: Implement completion for binary & unary operators --- decoder/candidates.go | 4 +- decoder/expr_any_completion.go | 86 +++++- decoder/expr_any_completion_test.go | 390 ++++++++++++++++++++++++++++ 3 files changed, 477 insertions(+), 3 deletions(-) diff --git a/decoder/candidates.go b/decoder/candidates.go index a3bf2da0..0680fc7e 100644 --- a/decoder/candidates.go +++ b/decoder/candidates.go @@ -172,9 +172,9 @@ func (d *PathDecoder) isPosInsideAttrExpr(attr *hclsyntax.Attribute, pos hcl.Pos return true } - // edge case: end of incomplete traversal with '.' (which parser ignores) + // edge case: end of incomplete expression with trailing '.' (which parser ignores) endByte := attr.Expr.Range().End.Byte - if _, ok := attr.Expr.(*hclsyntax.ScopeTraversalExpr); ok && pos.Byte-endByte == 1 { + if pos.Byte-endByte == 1 { suspectedDotRng := hcl.Range{ Filename: attr.Expr.Range().Filename, Start: attr.Expr.Range().End, diff --git a/decoder/expr_any_completion.go b/decoder/expr_any_completion.go index a1e27bf9..3674c9eb 100644 --- a/decoder/expr_any_completion.go +++ b/decoder/expr_any_completion.go @@ -10,6 +10,8 @@ import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" ) func (a Any) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { @@ -100,10 +102,15 @@ func (a Any) completeNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) []lan // 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 conditional expression https://github.com/hashicorp/terraform-ls/issues/528 - // TODO: Support operator expresssions https://github.com/hashicorp/terraform-ls/issues/529 // TODO: Support complex index expressions https://github.com/hashicorp/terraform-ls/issues/531 // TODO: Support relative traversals https://github.com/hashicorp/terraform-ls/issues/532 + opCandidates, ok := a.completeOperatorExprAtPos(ctx, pos) + if !ok { + return candidates + } + candidates = append(candidates, opCandidates...) + ref := Reference{ expr: a.expr, cons: schema.Reference{OfType: a.cons.OfType}, @@ -130,3 +137,80 @@ func (a Any) completeNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) []lan return candidates } + +func (a Any) completeOperatorExprAtPos(ctx context.Context, pos hcl.Pos) ([]lang.Candidate, bool) { + candidates := make([]lang.Candidate, 0) + + switch eType := a.expr.(type) { + case *hclsyntax.BinaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + // This could illustrate a situation such as `list_attr = 42 +` + // which is invalid syntax as add (+) op will never produce a list + return candidates, false + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 2 { + // This should never happen if HCL implementation is correct + return candidates, false + } + + if eType.LHS.Range().ContainsPos(pos) { + cons := schema.AnyExpression{ + OfType: opFuncParams[0].Type, + } + return newExpression(a.pathCtx, eType.LHS, cons).CompletionAtPos(ctx, pos), true + } + if eType.RHS.Range().ContainsPos(pos) || eType.RHS.Range().End.Byte == pos.Byte { + cons := schema.AnyExpression{ + OfType: opFuncParams[1].Type, + } + return newExpression(a.pathCtx, eType.RHS, cons).CompletionAtPos(ctx, pos), true + } + + return candidates, false + + case *hclsyntax.UnaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + // This could illustrate a situation such as `list_attr = !` + // which is invalid syntax as negation (!) op will never produce a list + return candidates, false + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 1 { + // This should never happen if HCL implementation is correct + return candidates, false + } + + if eType.Val.Range().ContainsPos(pos) || eType.Val.Range().End.Byte == pos.Byte { + cons := schema.AnyExpression{ + OfType: opFuncParams[0].Type, + } + return newExpression(a.pathCtx, eType.Val, cons).CompletionAtPos(ctx, pos), true + } + + // Trailing dot may be ignored by the parser so we attempt to recover it + if pos.Byte-eType.Val.Range().End.Byte == 1 { + fileBytes := a.pathCtx.Files[eType.Range().Filename].Bytes + trailingRune := fileBytes[eType.Val.Range().End.Byte:pos.Byte][0] + + if trailingRune == '.' { + cons := schema.AnyExpression{ + OfType: opFuncParams[0].Type, + } + return newExpression(a.pathCtx, eType.Val, cons).CompletionAtPos(ctx, pos), true + } + } + + return candidates, false + } + + return candidates, true +} diff --git a/decoder/expr_any_completion_test.go b/decoder/expr_any_completion_test.go index 3fb9b448..e03a4da8 100644 --- a/decoder/expr_any_completion_test.go +++ b/decoder/expr_any_completion_test.go @@ -2826,3 +2826,393 @@ func TestCompletionAtPos_exprAny_skipComplex(t *testing.T) { }) } } + +func TestCompletionAtPos_exprAny_operators(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refTargets reference.Targets + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + { + "logical OR", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + reference.Targets{}, + `attr = true || f +`, + hcl.Pos{Line: 1, Column: 17, Byte: 16}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + }, + }, + }), + }, + { + "logical OR mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + reference.Targets{}, + `attr = true || f +`, + hcl.Pos{Line: 1, Column: 17, Byte: 16}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "adding", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.Number, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "list"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = 42 + v +`, + hcl.Pos{Line: 1, Column: 14, Byte: 13}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.bar", + Detail: "string", + Kind: lang.TraversalCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.bar", + Snippet: "var.bar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + { + Label: "var.foo", + Detail: "number", + Kind: lang.TraversalCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + }), + }, + { + "negation", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + reference.Targets{}, + `attr = !f +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }), + }, + { + "unterminated expression with unary op", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + reference.Targets{}, + `attr = ! +`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + { + Label: "true", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + }), + }, + { + "unterminated expression with unary op and trailing dot", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.Bool, + }, + }, + `attr = !var. +`, + hcl.Pos{Line: 1, Column: 13, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.bar", + Detail: "bool", + Kind: lang.TraversalCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.bar", + Snippet: "var.bar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + }, + }, + }), + }, + { + "unterminated expression with binary op", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.Number, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "list"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = 42 + +`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + // TODO: See https://github.com/hashicorp/hcl-lang/issues/321 + }), + }, + { + "unterminated expression with binary op and trailing dot", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.Number, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "list"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = 42 + var. +`, + hcl.Pos{Line: 1, Column: 17, Byte: 16}, + lang.CompleteCandidates([]lang.Candidate{ + // TODO: See https://github.com/hashicorp/hcl-lang/issues/321 + }), + }, + } + + 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, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + ReferenceTargets: tc.refTargets, + }) + + ctx := context.Background() + candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" { + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} From 8a8f584475d2ed0afd25bc88de2b6011adf6c0ae Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 25 Sep 2023 16:31:21 +0100 Subject: [PATCH 2/5] decoder: Implement hover for binary & unary expressions --- decoder/expr_any_hover.go | 65 ++++++++++++++++- decoder/expr_any_hover_test.go | 127 +++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 1 deletion(-) diff --git a/decoder/expr_any_hover.go b/decoder/expr_any_hover.go index 922ed4ff..fd2b563a 100644 --- a/decoder/expr_any_hover.go +++ b/decoder/expr_any_hover.go @@ -10,6 +10,8 @@ import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" ) func (a Any) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { @@ -98,10 +100,13 @@ func (a Any) hoverNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) *lang.Ho // 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 conditional expression https://github.com/hashicorp/terraform-ls/issues/528 - // TODO: Support operator expresssions https://github.com/hashicorp/terraform-ls/issues/529 // TODO: Support complex index expressions https://github.com/hashicorp/terraform-ls/issues/531 // TODO: Support relative traversals https://github.com/hashicorp/terraform-ls/issues/532 + if hoverData, ok := a.hoverOperatorExprAtPos(ctx, pos); ok { + return hoverData + } + ref := Reference{ expr: a.expr, cons: schema.Reference{OfType: a.cons.OfType}, @@ -129,3 +134,61 @@ func (a Any) hoverNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) *lang.Ho } return lt.HoverAtPos(ctx, pos) } + +func (a Any) hoverOperatorExprAtPos(ctx context.Context, pos hcl.Pos) (*lang.HoverData, bool) { + switch eType := a.expr.(type) { + case *hclsyntax.BinaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + return nil, true + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 2 { + // This should never happen if HCL implementation is correct + return nil, true + } + + if eType.LHS.Range().ContainsPos(pos) { + cons := schema.AnyExpression{ + OfType: opFuncParams[0].Type, + } + return newExpression(a.pathCtx, eType.LHS, cons).HoverAtPos(ctx, pos), true + } + if eType.RHS.Range().ContainsPos(pos) { + cons := schema.AnyExpression{ + OfType: opFuncParams[1].Type, + } + return newExpression(a.pathCtx, eType.RHS, cons).HoverAtPos(ctx, pos), true + } + + return nil, true + + case *hclsyntax.UnaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + return nil, true + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 1 { + // This should never happen if HCL implementation is correct + return nil, true + } + + if eType.Val.Range().ContainsPos(pos) { + cons := schema.AnyExpression{ + OfType: opFuncParams[0].Type, + } + return newExpression(a.pathCtx, eType.Val, cons).HoverAtPos(ctx, pos), true + } + + return nil, true + } + + return nil, false +} diff --git a/decoder/expr_any_hover_test.go b/decoder/expr_any_hover_test.go index c8dd9404..eaff214d 100644 --- a/decoder/expr_any_hover_test.go +++ b/decoder/expr_any_hover_test.go @@ -1478,3 +1478,130 @@ foo = "noot" }) } } + +func TestHoverAtPos_exprAny_operators(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedHoverData *lang.HoverData + }{ + { + "binary operator LHS", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `attr = 42 + 43 +`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("_number_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + "binary operator RHS", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `attr = 42 + 43 +`, + hcl.Pos{Line: 1, Column: 14, Byte: 13}, + &lang.HoverData{ + Content: lang.Markdown("_number_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + { + "binary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `attr = true || false +`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + nil, + }, + { + "unary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + `attr = !true +`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + }, + }, + { + "unary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `attr = !true +`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + nil, + }, + } + 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) + } + }) + } +} From 013d67aadebe1f5c0f0ad621722455e8020d76ef Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 27 Sep 2023 09:16:44 +0100 Subject: [PATCH 3/5] decoder: Implement semantic tokens for binary & unary expressions --- decoder/expr_any_semtok.go | 58 ++++++++++++ decoder/expr_any_semtok_test.go | 154 ++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) diff --git a/decoder/expr_any_semtok.go b/decoder/expr_any_semtok.go index 151aa906..7faaac8a 100644 --- a/decoder/expr_any_semtok.go +++ b/decoder/expr_any_semtok.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" ) func (a Any) SemanticTokens(ctx context.Context) []lang.SemanticToken { @@ -101,6 +103,10 @@ func (a Any) semanticTokensForNonComplexExpr(ctx context.Context) []lang.Semanti // TODO: Support complex index expressions https://github.com/hashicorp/terraform-ls/issues/531 // TODO: Support relative traversals https://github.com/hashicorp/terraform-ls/issues/532 + if tokens, ok := a.semanticTokensForOperatorExpr(ctx); ok { + return tokens + } + ref := Reference{ expr: a.expr, cons: schema.Reference{OfType: a.cons.OfType}, @@ -128,3 +134,55 @@ func (a Any) semanticTokensForNonComplexExpr(ctx context.Context) []lang.Semanti } return lt.SemanticTokens(ctx) } + +func (a Any) semanticTokensForOperatorExpr(ctx context.Context) ([]lang.SemanticToken, bool) { + tokens := make([]lang.SemanticToken, 0) + + switch eType := a.expr.(type) { + case *hclsyntax.BinaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + return tokens, true + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 2 { + // This should never happen if HCL implementation is correct + return tokens, true + } + + tokens = append(tokens, newExpression(a.pathCtx, eType.LHS, schema.AnyExpression{ + OfType: opFuncParams[0].Type, + }).SemanticTokens(ctx)...) + + tokens = append(tokens, newExpression(a.pathCtx, eType.RHS, schema.AnyExpression{ + OfType: opFuncParams[1].Type, + }).SemanticTokens(ctx)...) + + return tokens, true + + case *hclsyntax.UnaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + return tokens, true + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 1 { + // This should never happen if HCL implementation is correct + return tokens, true + } + + tokens = append(tokens, newExpression(a.pathCtx, eType.Val, schema.AnyExpression{ + OfType: opFuncParams[0].Type, + }).SemanticTokens(ctx)...) + + return tokens, true + } + + return tokens, false +} diff --git a/decoder/expr_any_semtok_test.go b/decoder/expr_any_semtok_test.go index 9e4d67e2..8dfbc116 100644 --- a/decoder/expr_any_semtok_test.go +++ b/decoder/expr_any_semtok_test.go @@ -2503,3 +2503,157 @@ foo = "noot" }) } } + +func TestSemanticTokens_exprAny_operators(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedSemanticTokens []lang.SemanticToken + }{ + { + "binary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `attr = 42 + 43 +`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: lang.TokenNumber, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + { + Type: lang.TokenNumber, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }, + { + "binary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `attr = 42 + 43 +`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "unary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + `attr = !true +`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: lang.TokenBool, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + }, + }, + }, + { + "unary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `attr = !true +`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + } + 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() + tokens, err := d.SemanticTokensInFile(ctx, "test.tf") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedSemanticTokens, tokens); diff != "" { + t.Fatalf("unexpected tokens: %s", diff) + } + }) + } +} From 4e771387f56a144a405ba137338cf41d5637f868 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 26 Sep 2023 08:44:51 +0100 Subject: [PATCH 4/5] decoder: Implement reference origins for binary & unary expressions --- decoder/expr_any_ref_origins.go | 72 +++++- decoder/expr_any_ref_origins_test.go | 326 +++++++++++++++++++++++++++ 2 files changed, 397 insertions(+), 1 deletion(-) diff --git a/decoder/expr_any_ref_origins.go b/decoder/expr_any_ref_origins.go index bb14f249..e31433da 100644 --- a/decoder/expr_any_ref_origins.go +++ b/decoder/expr_any_ref_origins.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" ) func (a Any) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { @@ -118,10 +119,13 @@ func (a Any) refOriginsForNonComplexExpr(ctx context.Context, allowSelfRefs bool // 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 conditional expression https://github.com/hashicorp/terraform-ls/issues/528 - // TODO: Support operator expresssions https://github.com/hashicorp/terraform-ls/issues/529 // TODO: Support complex index expressions https://github.com/hashicorp/terraform-ls/issues/531 // TODO: Support relative traversals https://github.com/hashicorp/terraform-ls/issues/532 + if origins, ok := a.refOriginsForOperatorExpr(ctx, allowSelfRefs); ok { + return origins + } + // attempt to get accurate constraint for the origins // if we recognise the given expression funcExpr := functionExpr{ @@ -170,3 +174,69 @@ func (a Any) refOriginsForNonComplexExpr(ctx context.Context, allowSelfRefs bool } return origins } + +func (a Any) refOriginsForOperatorExpr(ctx context.Context, allowSelfRefs bool) (reference.Origins, bool) { + origins := make(reference.Origins, 0) + + // There is currently no way of decoding operator 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.BinaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + return origins, true + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 2 { + // This should never happen if HCL implementation is correct + return origins, true + } + + leftExpr := newExpression(a.pathCtx, eType.LHS, schema.AnyExpression{ + OfType: opFuncParams[0].Type, + }) + if expr, ok := leftExpr.(ReferenceOriginsExpression); ok { + origins = append(origins, expr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + + rightExpr := newExpression(a.pathCtx, eType.RHS, schema.AnyExpression{ + OfType: opFuncParams[1].Type, + }) + if expr, ok := rightExpr.(ReferenceOriginsExpression); ok { + origins = append(origins, expr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + + return origins, true + + case *hclsyntax.UnaryOpExpr: + opReturnType := eType.Op.Type + + // Check if such an operation is even allowed within the constraint + if _, err := convert.Convert(cty.UnknownVal(opReturnType), a.cons.OfType); err != nil { + return origins, true + } + + opFuncParams := eType.Op.Impl.Params() + if len(opFuncParams) != 1 { + // This should never happen if HCL implementation is correct + return origins, true + } + + expr := newExpression(a.pathCtx, eType.Val, schema.AnyExpression{ + OfType: opFuncParams[0].Type, + }) + if expr, ok := expr.(ReferenceOriginsExpression); ok { + origins = append(origins, expr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + + return origins, true + } + + return origins, false +} diff --git a/decoder/expr_any_ref_origins_test.go b/decoder/expr_any_ref_origins_test.go index 9709d689..cb3881bb 100644 --- a/decoder/expr_any_ref_origins_test.go +++ b/decoder/expr_any_ref_origins_test.go @@ -728,3 +728,329 @@ func TestCollectRefOrigins_exprAny_functions_json(t *testing.T) { }) } } + +func TestCollectRefOrigins_exprAny_operators_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefOrigins reference.Origins + }{ + { + "binary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `attr = var.foo + var.bar +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.Number, + }, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.Number, + }, + }, + }, + }, + }, + { + "binary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `attr = var.foo + var.bar +`, + reference.Origins{}, + }, + { + "unary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + `attr = !var.foo +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.Bool, + }, + }, + }, + }, + }, + { + "unary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `attr = !var.foo +`, + reference.Origins{}, + }, + } + + 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, + }, + }) + + 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 + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefOrigins reference.Origins + }{ + { + "binary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Number, + }, + }, + }, + `{"attr": "${var.foo + var.bar}"}`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.DynamicPseudoType, + }, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Range: hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 30, Byte: 29}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.DynamicPseudoType, + }, + }, + }, + }, + }, + { + "binary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `{"attr": "${var.foo + var.bar}"}`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + End: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.DynamicPseudoType, + }, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Range: hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 30, Byte: 29}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.DynamicPseudoType, + }, + }, + }, + }, + }, + { + "unary operator", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + `{"attr": "${!var.foo}"}`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.DynamicPseudoType, + }, + }, + }, + }, + }, + { + "unary operator mismatching constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `{"attr": "${!var.foo}"}`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf.json", + Start: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.DynamicPseudoType, + }, + }, + }, + }, + }, + } + + 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, + }, + }) + + 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) + } + }) + } +} From d2aaea92ea76dcdce4a22b938e8ea3121f8724be Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 29 Sep 2023 08:17:31 +0100 Subject: [PATCH 5/5] add tests with functions --- decoder/expr_any_completion_test.go | 209 ++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/decoder/expr_any_completion_test.go b/decoder/expr_any_completion_test.go index e03a4da8..07dee668 100644 --- a/decoder/expr_any_completion_test.go +++ b/decoder/expr_any_completion_test.go @@ -3006,6 +3006,66 @@ func TestCompletionAtPos_exprAny_operators(t *testing.T) { `, hcl.Pos{Line: 1, Column: 9, Byte: 8}, lang.CompleteCandidates([]lang.Candidate{ + { + Label: "element", + Detail: "element(list dynamic, index number) dynamic", + Description: lang.Markdown("`element` retrieves a single element from a list."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "element()", + Snippet: "element(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + { + Label: "join", + Detail: "join(separator string, …lists list of string) string", + Description: lang.Markdown("`join` produces a string by concatenating together all elements of a given list of strings with the given delimiter."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "join()", + Snippet: "join(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + { + Label: "keys", + Detail: "keys(inputMap dynamic) dynamic", + Description: lang.Markdown("`keys` takes a map and returns a list containing the keys from that map."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "keys()", + Snippet: "keys(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + { + Label: "lower", + Detail: "lower(str string) string", + Description: lang.Markdown("`lower` converts all cased letters in the given string to lowercase."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "lower()", + Snippet: "lower(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, { Label: "false", Detail: "bool", @@ -3187,6 +3247,154 @@ func TestCompletionAtPos_exprAny_operators(t *testing.T) { // TODO: See https://github.com/hashicorp/hcl-lang/issues/321 }), }, + { + "unary expression with function", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + }, + `attr = !lower() +`, + hcl.Pos{Line: 1, Column: 15, Byte: 14}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "string", + Kind: lang.TraversalCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + { + Label: "element", + Detail: "element(list dynamic, index number) dynamic", + Description: lang.Markdown("`element` retrieves a single element from a list."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "element()", + Snippet: "element(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + { + Label: "join", + Detail: "join(separator string, …lists list of string) string", + Description: lang.Markdown("`join` produces a string by concatenating together all elements of a given list of strings with the given delimiter."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "join()", + Snippet: "join(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + { + Label: "keys", + Detail: "keys(inputMap dynamic) dynamic", + Description: lang.Markdown("`keys` takes a map and returns a list containing the keys from that map."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "keys()", + Snippet: "keys(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + { + Label: "log", + Detail: "log(num number, base number) number", + Description: lang.Markdown("`log` returns the logarithm of a given number in a given base."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "log()", + Snippet: "log(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + { + Label: "lower", + Detail: "lower(str string) string", + Description: lang.Markdown("`lower` converts all cased letters in the given string to lowercase."), + Kind: lang.FunctionCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "lower()", + Snippet: "lower(${0})", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }), + }, + { + "unary expression with unterminated function", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Bool, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + }, + `attr = !lower( +`, + hcl.Pos{Line: 1, Column: 15, Byte: 14}, + lang.CompleteCandidates([]lang.Candidate{ + // TODO: See https://github.com/hashicorp/hcl-lang/issues/325 + }), + }, } for i, tc := range testCases { @@ -3202,6 +3410,7 @@ func TestCompletionAtPos_exprAny_operators(t *testing.T) { "test.tf": f, }, ReferenceTargets: tc.refTargets, + Functions: testFunctionSignatures(), }) ctx := context.Background()