From aaf4211e5c82fa3f0e5fe62086635df659a3f57d Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Mon, 15 Jan 2024 17:41:03 +0100 Subject: [PATCH 1/7] decoder: Support complex index expr completion --- decoder/expr_any_completion.go | 2 ++ decoder/expr_any_index.go | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 decoder/expr_any_index.go diff --git a/decoder/expr_any_completion.go b/decoder/expr_any_completion.go index 7e42f97c..716ecffc 100644 --- a/decoder/expr_any_completion.go +++ b/decoder/expr_any_completion.go @@ -143,5 +143,7 @@ func (a Any) completeNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) []lan } candidates = append(candidates, lt.CompletionAtPos(ctx, pos)...) + candidates = append(candidates, a.complexIndex(ctx, pos)...) + return candidates } diff --git a/decoder/expr_any_index.go b/decoder/expr_any_index.go new file mode 100644 index 00000000..dbf6fd9d --- /dev/null +++ b/decoder/expr_any_index.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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 (a Any) complexIndex(ctx context.Context, pos hcl.Pos) []lang.Candidate { + var candidates []lang.Candidate + + cons := schema.AnyExpression{ + OfType: cty.String, // TODO improve type (could be int) + } + + switch eType := a.expr.(type) { + // An empty expression, e.g. `tags[]`, is a scope traversal expression + // with an empty index step. + case *hclsyntax.ScopeTraversalExpr: + if len(eType.Traversal) < 2 { + return candidates + } + // If the last part of the traversal is an index step, + // we start a new completion to enable completion of + // references and functions. + lastTraversal := eType.Traversal[len(eType.Traversal)-1] + if _, ok := lastTraversal.(hcl.TraverseIndex); ok { + editRange := hcl.Range{ + Filename: eType.SrcRange.Filename, + Start: pos, + End: pos, + } + expr := &hclsyntax.LiteralValueExpr{ + SrcRange: editRange, + Val: cty.UnknownVal(cty.DynamicPseudoType), + } + return newExpression(a.pathCtx, expr, cons).CompletionAtPos(ctx, pos) + } + // If there is a prefix or valid expression within the index step, + // we're dealing an index expression and can defer completion for the key. + case *hclsyntax.IndexExpr: + return newExpression(a.pathCtx, eType.Key, cons).CompletionAtPos(ctx, pos) + } + + return candidates +} From cbb6b4706f0bdb30eddafabb3a8a72521c7ee294 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Mon, 15 Jan 2024 17:45:27 +0100 Subject: [PATCH 2/7] decoder: Test completion for complex index expr --- decoder/expr_any_completion_test.go | 91 ++++++++++- decoder/expr_any_index_test.go | 231 ++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 decoder/expr_any_index_test.go diff --git a/decoder/expr_any_completion_test.go b/decoder/expr_any_completion_test.go index 58abebf7..04bb5bd4 100644 --- a/decoder/expr_any_completion_test.go +++ b/decoder/expr_any_completion_test.go @@ -524,7 +524,7 @@ func TestCompletionAtPos_exprAny_functions(t *testing.T) { Start: hcl.Pos{Line: 2, Column: 5, Byte: 27}, End: hcl.Pos{Line: 2, Column: 15, Byte: 37}, }, - Type: cty.List(cty.String), + Type: cty.Map(cty.String), NestedTargets: reference.Targets{ { Addr: lang.Address{ @@ -560,6 +560,95 @@ func TestCompletionAtPos_exprAny_functions(t *testing.T) { Snippet: `var.map["foo"]`, }, }, + { + Label: `var.map`, + Detail: "map of string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + NewText: `var.map`, + Snippet: `var.map`, + }, + }, + { + 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: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, + { + 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: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, + { + 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: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, + { + 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: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, + { + 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: 22, Byte: 21}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + }, + }, }), }, { diff --git a/decoder/expr_any_index_test.go b/decoder/expr_any_index_test.go new file mode 100644 index 00000000..3af8c342 --- /dev/null +++ b/decoder/expr_any_index_test.go @@ -0,0 +1,231 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package decoder + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "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 TestCompletionAtPos_exprAny_index(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refTargets reference.Targets + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + { + "empty complex index", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "name"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "tags": cty.List(cty.String), + }, []string{"tags"}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "name"}, + lang.AttrStep{Name: "tags"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "name"}, + lang.AttrStep{Name: "tags"}, + lang.IndexStep{Key: cty.StringVal("name")}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.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: "local"}, + lang.AttrStep{Name: "name"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + }, + `attr = aws_instance.name.tags[] +`, + hcl.Pos{Line: 1, Column: 31, Byte: 30}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `aws_instance.name.tags["name"]`, + Detail: "string", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + NewText: `aws_instance.name.tags["name"]`, + Snippet: `aws_instance.name.tags["name"]`, + }, + Kind: lang.ReferenceCandidateKind, + }, + { + Label: `aws_instance.name`, + Detail: "object", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + }, + NewText: `aws_instance.name`, + Snippet: `aws_instance.name`, + }, + Kind: lang.ReferenceCandidateKind, + }, + { + Label: `local.name`, + Detail: "string", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + }, + NewText: `local.name`, + Snippet: `local.name`, + }, + Kind: lang.ReferenceCandidateKind, + }, + }), + }, + { + "prefix complex index", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "name"}, + lang.AttrStep{Name: "tags"}, + lang.IndexStep{Key: cty.StringVal("name")}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.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: "local"}, + lang.AttrStep{Name: "name"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + }, + `attr = aws_instance.name.tags[l] +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `local.name`, + Detail: "string", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + NewText: `local.name`, + Snippet: `local.name`, + }, + Kind: lang.ReferenceCandidateKind, + }, + }), + }, + } + + 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, + // Functions: testFunctionSignatures(), + }) + + ctx := context.Background() + candidates, err := d.CompletionAtPos(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 af65c8d8c7ac3308eec56b7f04f895d01f05002d Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 16 Jan 2024 17:08:58 +0100 Subject: [PATCH 3/7] decoder: Support complex index expr hover --- decoder/expr_any_hover.go | 4 ++++ decoder/expr_any_index.go | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/decoder/expr_any_hover.go b/decoder/expr_any_hover.go index 53e0e1d9..96586268 100644 --- a/decoder/expr_any_hover.go +++ b/decoder/expr_any_hover.go @@ -111,6 +111,10 @@ func (a Any) hoverNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) *lang.Ho return hoverData } + if hoverData, ok := a.hoverIndexExprAtPos(ctx, pos); ok { + return hoverData + } + ref := Reference{ expr: a.expr, cons: schema.Reference{OfType: a.cons.OfType}, diff --git a/decoder/expr_any_index.go b/decoder/expr_any_index.go index dbf6fd9d..89c8f49a 100644 --- a/decoder/expr_any_index.go +++ b/decoder/expr_any_index.go @@ -51,3 +51,16 @@ func (a Any) complexIndex(ctx context.Context, pos hcl.Pos) []lang.Candidate { return candidates } + +func (a Any) hoverIndexExprAtPos(ctx context.Context, pos hcl.Pos) (*lang.HoverData, bool) { + if eType, ok := a.expr.(*hclsyntax.IndexExpr); ok { + if eType.Key.Range().ContainsPos(pos) { + cons := schema.AnyExpression{ + OfType: cty.String, // TODO improve type (could be int) + } + return newExpression(a.pathCtx, eType.Key, cons).HoverAtPos(ctx, pos), true + } + } + + return nil, false +} From 74b6b0feb10f5d31e82781f633b3da1841705d31 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 16 Jan 2024 17:09:32 +0100 Subject: [PATCH 4/7] decoder: Test hover for complex index expr --- decoder/expr_any_index_test.go | 147 ++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 3 deletions(-) diff --git a/decoder/expr_any_index_test.go b/decoder/expr_any_index_test.go index 3af8c342..ee0d445f 100644 --- a/decoder/expr_any_index_test.go +++ b/decoder/expr_any_index_test.go @@ -156,15 +156,46 @@ func TestCompletionAtPos_exprAny_index(t *testing.T) { Addr: lang.Address{ lang.RootStep{Name: "aws_instance"}, lang.AttrStep{Name: "name"}, - lang.AttrStep{Name: "tags"}, - lang.IndexStep{Key: cty.StringVal("name")}, }, RangePtr: &hcl.Range{ Filename: "variables.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, }, - Type: cty.String, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "tags": cty.List(cty.String), + }, []string{"tags"}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "name"}, + lang.AttrStep{Name: "tags"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "name"}, + lang.AttrStep{Name: "tags"}, + lang.IndexStep{Key: cty.StringVal("name")}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.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{ @@ -229,3 +260,113 @@ func TestCompletionAtPos_exprAny_index(t *testing.T) { }) } } + +func TestHoverAtPos_exprAny_index(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refOrigins reference.Origins + refTargets reference.Targets + cfg string + pos hcl.Pos + expectedHoverData *lang.HoverData + }{ + { + "empty index", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Origins{}, + reference.Targets{}, + `attr = aws_instance.name.tags[] +`, + hcl.Pos{Line: 1, Column: 31, Byte: 30}, + nil, + }, + { + "local reference in index", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "local"}, + lang.AttrStep{Name: "name"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 41, Byte: 40}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.String, + }, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "local"}, + lang.AttrStep{Name: "name"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + }, + `attr = aws_instance.name.tags[local.name] +`, + hcl.Pos{Line: 1, Column: 35, Byte: 34}, + &lang.HoverData{ + Content: lang.Markdown("`local.name`\n_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 41, Byte: 40}, + }, + }, + }, + } + + 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, + }, + ReferenceOrigins: tc.refOrigins, + ReferenceTargets: tc.refTargets, + }) + + 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 6e9cceaf9f8ea368352d45d0ec10300d05a573b7 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 16 Jan 2024 17:30:17 +0100 Subject: [PATCH 5/7] decoder: Support complex index expr semtok --- decoder/expr_any_index.go | 11 +++++++++++ decoder/expr_any_semtok.go | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/decoder/expr_any_index.go b/decoder/expr_any_index.go index 89c8f49a..5564aa8e 100644 --- a/decoder/expr_any_index.go +++ b/decoder/expr_any_index.go @@ -64,3 +64,14 @@ func (a Any) hoverIndexExprAtPos(ctx context.Context, pos hcl.Pos) (*lang.HoverD return nil, false } + +func (a Any) semanticTokensForIndexExpr(ctx context.Context) ([]lang.SemanticToken, bool) { + if eType, ok := a.expr.(*hclsyntax.IndexExpr); ok { + cons := schema.AnyExpression{ + OfType: cty.String, // TODO improve type (could be int) + } + return newExpression(a.pathCtx, eType.Key, cons).SemanticTokens(ctx), true + } + + return nil, false +} diff --git a/decoder/expr_any_semtok.go b/decoder/expr_any_semtok.go index f530796c..08912dea 100644 --- a/decoder/expr_any_semtok.go +++ b/decoder/expr_any_semtok.go @@ -110,6 +110,10 @@ func (a Any) semanticTokensForNonComplexExpr(ctx context.Context) []lang.Semanti return tokens } + if tokens, ok := a.semanticTokensForIndexExpr(ctx); ok { + return tokens + } + ref := Reference{ expr: a.expr, cons: schema.Reference{OfType: a.cons.OfType}, From 13836c45ae98a09a515c0ba53ddcabcc3695baba Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 16 Jan 2024 17:30:33 +0100 Subject: [PATCH 6/7] decoder: Test semtok for complex index expr --- decoder/expr_any_index_test.go | 190 +++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/decoder/expr_any_index_test.go b/decoder/expr_any_index_test.go index ee0d445f..2357e3bd 100644 --- a/decoder/expr_any_index_test.go +++ b/decoder/expr_any_index_test.go @@ -370,3 +370,193 @@ func TestHoverAtPos_exprAny_index(t *testing.T) { }) } } + +func TestSemanticTokens_exprAny_index(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refOrigins reference.Origins + refTargets reference.Targets + cfg string + expectedSemanticTokens []lang.SemanticToken + }{ + { + "simple conditional", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Origins{}, + reference.Targets{}, + `attr = true ? "t" : 422 +`, + []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: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + { + Type: lang.TokenString, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + }, + }, + { + Type: lang.TokenNumber, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + }, + }, + }, + { + "empty index", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Origins{}, + reference.Targets{}, + `attr = aws_instance.name.tags[local.name] +`, + []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}, + }, + }, + }, + }, + { + "local reference in index", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.String, + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "local"}, + lang.AttrStep{Name: "name"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 41, Byte: 40}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.String, + }, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "local"}, + lang.AttrStep{Name: "name"}, + }, + RangePtr: &hcl.Range{ + Filename: "variables.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + }, + Type: cty.String, + }, + }, + `attr = aws_instance.name.tags[local.name] +`, + []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.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 36, Byte: 35}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 37, Byte: 36}, + End: hcl.Pos{Line: 1, Column: 41, Byte: 40}, + }, + }, + }, + }, + } + + 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, + }, + ReferenceOrigins: tc.refOrigins, + ReferenceTargets: tc.refTargets, + }) + + 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 2ad013af2ecb435b6cd962230f3832e0ecd48c7c Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Fri, 19 Jan 2024 11:59:41 +0100 Subject: [PATCH 7/7] Review feedback --- decoder/expr_any_completion.go | 3 +-- decoder/expr_any_hover.go | 1 - decoder/expr_any_index.go | 24 ++++++++++-------------- decoder/expr_any_ref_origins.go | 1 - decoder/expr_any_semtok.go | 1 - 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/decoder/expr_any_completion.go b/decoder/expr_any_completion.go index 716ecffc..d33648ca 100644 --- a/decoder/expr_any_completion.go +++ b/decoder/expr_any_completion.go @@ -98,7 +98,6 @@ 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 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) @@ -143,7 +142,7 @@ func (a Any) completeNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) []lan } candidates = append(candidates, lt.CompletionAtPos(ctx, pos)...) - candidates = append(candidates, a.complexIndex(ctx, pos)...) + candidates = append(candidates, a.completeIndexExprAtPos(ctx, pos)...) return candidates } diff --git a/decoder/expr_any_hover.go b/decoder/expr_any_hover.go index 96586268..3c35da25 100644 --- a/decoder/expr_any_hover.go +++ b/decoder/expr_any_hover.go @@ -96,7 +96,6 @@ func (a Any) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { func (a Any) hoverNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { // 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 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 { diff --git a/decoder/expr_any_index.go b/decoder/expr_any_index.go index 5564aa8e..b7b16c33 100644 --- a/decoder/expr_any_index.go +++ b/decoder/expr_any_index.go @@ -13,11 +13,15 @@ import ( "github.com/zclconf/go-cty/cty" ) -func (a Any) complexIndex(ctx context.Context, pos hcl.Pos) []lang.Candidate { +func (a Any) completeIndexExprAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { var candidates []lang.Candidate cons := schema.AnyExpression{ - OfType: cty.String, // TODO improve type (could be int) + // TODO we could improve this by looking up the type of the + // referenced collection. For example, if it's a list, we + // could use a number type. But since number and strings + // are convertible both ways, it shouldn't matter too much. + OfType: cty.String, } switch eType := a.expr.(type) { @@ -32,19 +36,11 @@ func (a Any) complexIndex(ctx context.Context, pos hcl.Pos) []lang.Candidate { // references and functions. lastTraversal := eType.Traversal[len(eType.Traversal)-1] if _, ok := lastTraversal.(hcl.TraverseIndex); ok { - editRange := hcl.Range{ - Filename: eType.SrcRange.Filename, - Start: pos, - End: pos, - } - expr := &hclsyntax.LiteralValueExpr{ - SrcRange: editRange, - Val: cty.UnknownVal(cty.DynamicPseudoType), - } + expr := newEmptyExpressionAtPos(eType.Range().Filename, pos) return newExpression(a.pathCtx, expr, cons).CompletionAtPos(ctx, pos) } // If there is a prefix or valid expression within the index step, - // we're dealing an index expression and can defer completion for the key. + // we're dealing with an index expression and can defer completion for the key. case *hclsyntax.IndexExpr: return newExpression(a.pathCtx, eType.Key, cons).CompletionAtPos(ctx, pos) } @@ -56,7 +52,7 @@ func (a Any) hoverIndexExprAtPos(ctx context.Context, pos hcl.Pos) (*lang.HoverD if eType, ok := a.expr.(*hclsyntax.IndexExpr); ok { if eType.Key.Range().ContainsPos(pos) { cons := schema.AnyExpression{ - OfType: cty.String, // TODO improve type (could be int) + OfType: cty.String, // TODO improve type (see above) } return newExpression(a.pathCtx, eType.Key, cons).HoverAtPos(ctx, pos), true } @@ -68,7 +64,7 @@ func (a Any) hoverIndexExprAtPos(ctx context.Context, pos hcl.Pos) (*lang.HoverD func (a Any) semanticTokensForIndexExpr(ctx context.Context) ([]lang.SemanticToken, bool) { if eType, ok := a.expr.(*hclsyntax.IndexExpr); ok { cons := schema.AnyExpression{ - OfType: cty.String, // TODO improve type (could be int) + OfType: cty.String, // TODO improve type (see above) } return newExpression(a.pathCtx, eType.Key, cons).SemanticTokens(ctx), true } diff --git a/decoder/expr_any_ref_origins.go b/decoder/expr_any_ref_origins.go index 6ce35576..90c2773f 100644 --- a/decoder/expr_any_ref_origins.go +++ b/decoder/expr_any_ref_origins.go @@ -116,7 +116,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 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 { diff --git a/decoder/expr_any_semtok.go b/decoder/expr_any_semtok.go index 08912dea..1ba89fc2 100644 --- a/decoder/expr_any_semtok.go +++ b/decoder/expr_any_semtok.go @@ -95,7 +95,6 @@ func (a Any) SemanticTokens(ctx context.Context) []lang.SemanticToken { func (a Any) semanticTokensForNonComplexExpr(ctx context.Context) []lang.SemanticToken { // 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 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 {