diff --git a/decoder/expr_any_completion.go b/decoder/expr_any_completion.go index 7e3eb0c3..906e0ae4 100644 --- a/decoder/expr_any_completion.go +++ b/decoder/expr_any_completion.go @@ -100,7 +100,6 @@ func (a Any) completeNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) []lan candidates := make([]lang.Candidate, 0) // 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 relative traversals https://github.com/hashicorp/terraform-ls/issues/532 opCandidates, ok := a.completeOperatorExprAtPos(ctx, pos) @@ -121,6 +120,12 @@ func (a Any) completeNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) []lan } candidates = append(candidates, condCandidates...) + forCandidates, ok := a.completeForExprAtPos(ctx, pos) + if !ok { + return candidates + } + candidates = append(candidates, forCandidates...) + ref := Reference{ expr: a.expr, cons: schema.Reference{OfType: a.cons.OfType}, diff --git a/decoder/expr_any_completion_test.go b/decoder/expr_any_completion_test.go index 618f1e0a..ea035240 100644 --- a/decoder/expr_any_completion_test.go +++ b/decoder/expr_any_completion_test.go @@ -4000,6 +4000,802 @@ func TestCompletionAtPos_exprAny_parentheses(t *testing.T) { } } +func TestCompletionAtPos_exprAny_forExpr(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refTargets reference.Targets + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + // list + { + "list on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.List(cty.String), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "mystring"}, + }, + Type: cty.String, + }, + }, + `attr = [for i, v in var: v] +`, + hcl.Pos{Line: 1, Column: 24, Byte: 23}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "list of string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + }, + }, + }), + }, + { + "list on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.List(cty.String), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.String, + }, + }, + `attr = [for i, v in var: v] +`, + hcl.Pos{Line: 1, Column: 27, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.bar", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.bar", + Snippet: "var.bar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }), + }, + { + "list on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Targets{}, + `attr = [for i, v in var: v if t] +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + lang.CompleteCandidates([]lang.Candidate{ + { + 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: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + + // set + { + "set on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.Set(cty.String), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "mystring"}, + }, + Type: cty.String, + }, + }, + `attr = [for i, v in var: v] +`, + hcl.Pos{Line: 1, Column: 24, Byte: 23}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "set of string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + }, + }, + }), + }, + { + "set on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.Set(cty.String), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.String, + }, + }, + `attr = [for i, v in var: v] +`, + hcl.Pos{Line: 1, Column: 27, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.bar", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.bar", + Snippet: "var.bar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }), + }, + { + "set on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.Set(cty.String), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.String, + }, + }, + `attr = [for i, v in var: v if t] +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + lang.CompleteCandidates([]lang.Candidate{ + { + 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: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + + // tuple + { + "tuple on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.EmptyTuple, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "mystring"}, + }, + Type: cty.String, + }, + }, + `attr = [for k, v in var: v] +`, + hcl.Pos{Line: 1, Column: 24, Byte: 23}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "tuple", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + }, + }, + }), + }, + { + "tuple on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.EmptyTuple, + }, + }, + `attr = [for k, v in var: v] +`, + hcl.Pos{Line: 1, Column: 27, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + { + Label: "var.bar", + Detail: "tuple", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.bar", + Snippet: "var.bar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }), + }, + { + "tuple on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.EmptyTuple, + }, + }, + `attr = [for k, v in var: v if f] +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + 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: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + + // map + { + "map on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.Map(cty.String), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "mystring"}, + }, + Type: cty.String, + }, + }, + `attr = {for k, v in var: k => v} +`, + hcl.Pos{Line: 1, Column: 24, Byte: 23}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "map of string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + }, + }, + }), + }, + { + "map on key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.Number), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = {for k, v in var: v => val} +`, + hcl.Pos{Line: 1, Column: 27, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }), + }, + { + "map on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = {for k, v in var: k => v} +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + { + "map on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{}, + `attr = {for k, v in var: v if f} +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + 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: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + + // object + { + "object on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.EmptyObject, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "mystring"}, + }, + Type: cty.String, + }, + }, + `attr = {for k, v in var: k => v} +`, + hcl.Pos{Line: 1, Column: 24, Byte: 23}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "object", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + }, + }, + }), + }, + { + "object on key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = {for k, v in var: v => val} +`, + hcl.Pos{Line: 1, Column: 27, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }), + }, + { + "object on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = {for k, v in var: k => v} +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "var.foo", + Detail: "string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.foo", + Snippet: "var.foo", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + { + Label: "var.bar", + Detail: "list of string", + Kind: lang.ReferenceCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "var.bar", + Snippet: "var.bar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + { + "object on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{}, + `attr = {for k, v in var: v if f} +`, + hcl.Pos{Line: 1, Column: 32, Byte: 31}, + 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: 31, Byte: 30}, + End: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + }, + }, + }, + }), + }, + } + + 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.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) + } + }) + } +} + func TestCompletionAtPos_exprAny_template(t *testing.T) { testCases := []struct { testName string diff --git a/decoder/expr_any_for.go b/decoder/expr_any_for.go new file mode 100644 index 00000000..b1cce89d --- /dev/null +++ b/decoder/expr_any_for.go @@ -0,0 +1,289 @@ +// 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/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 (a Any) completeForExprAtPos(ctx context.Context, pos hcl.Pos) ([]lang.Candidate, bool) { + candidates := make([]lang.Candidate, 0) + + switch eType := a.expr.(type) { + case *hclsyntax.ForExpr: + if !isTypeIterable(a.cons.OfType) { + return candidates, true + } + + if eType.CollExpr.Range().ContainsPos(pos) || eType.CollExpr.Range().End.Byte == pos.Byte { + return newExpression(a.pathCtx, eType.CollExpr, a.cons).CompletionAtPos(ctx, pos), true + } + + if eType.KeyExpr != nil && (eType.KeyExpr.Range().ContainsPos(pos) || eType.KeyExpr.Range().End.Byte == pos.Byte) { + typ, ok := iterableKeyType(a.cons.OfType) + if !ok { + return candidates, true + } + cons := schema.AnyExpression{ + OfType: typ, + } + + return newExpression(a.pathCtx, eType.KeyExpr, cons).CompletionAtPos(ctx, pos), true + } + + if eType.ValExpr.Range().ContainsPos(pos) || eType.ValExpr.Range().End.Byte == pos.Byte { + typ, ok := iterableValueType(a.cons.OfType) + if !ok { + return candidates, true + } + cons := schema.AnyExpression{ + OfType: typ, + } + + return newExpression(a.pathCtx, eType.ValExpr, cons).CompletionAtPos(ctx, pos), true + } + + if eType.CondExpr != nil && (eType.CondExpr.Range().ContainsPos(pos) || eType.CondExpr.Range().End.Byte == pos.Byte) { + cons := schema.AnyExpression{ + OfType: cty.Bool, + } + return newExpression(a.pathCtx, eType.CondExpr, cons).CompletionAtPos(ctx, pos), true + } + return candidates, false + } + + return candidates, true +} + +func (a Any) hoverForExprAtPos(ctx context.Context, pos hcl.Pos) (*lang.HoverData, bool) { + switch eType := a.expr.(type) { + case *hclsyntax.ForExpr: + if !isTypeIterable(a.cons.OfType) { + return nil, false + } + + // TODO: eType.KeyVarExpr.Range() to display key type + + // TODO: eType.ValVarExpr.Range() to display value type + + if eType.CollExpr.Range().ContainsPos(pos) { + return newExpression(a.pathCtx, eType.CollExpr, a.cons).HoverAtPos(ctx, pos), true + } + + if eType.KeyExpr != nil && eType.KeyExpr.Range().ContainsPos(pos) { + typ, ok := iterableKeyType(a.cons.OfType) + if !ok { + return nil, false + } + cons := schema.AnyExpression{ + OfType: typ, + } + return newExpression(a.pathCtx, eType.KeyExpr, cons).HoverAtPos(ctx, pos), true + } + + if eType.ValExpr.Range().ContainsPos(pos) { + typ, ok := iterableValueType(a.cons.OfType) + if !ok { + return nil, false + } + cons := schema.AnyExpression{ + OfType: typ, + } + return newExpression(a.pathCtx, eType.ValExpr, cons).HoverAtPos(ctx, pos), true + } + + if eType.CondExpr != nil && eType.CondExpr.Range().ContainsPos(pos) { + cons := schema.AnyExpression{ + OfType: cty.Bool, + } + return newExpression(a.pathCtx, eType.CondExpr, cons).HoverAtPos(ctx, pos), true + } + } + + return nil, false +} + +func (a Any) semanticTokensForForExpr(ctx context.Context) ([]lang.SemanticToken, bool) { + tokens := make([]lang.SemanticToken, 0) + + switch eType := a.expr.(type) { + case *hclsyntax.ForExpr: + if !isTypeIterable(a.cons.OfType) { + return nil, false + } + + // TODO: eType.KeyVarExpr.Range() to report key as keyword + // TODO: eType.ValVarExpr.Range() to display value as keyword + + tokens = append(tokens, newExpression(a.pathCtx, eType.CollExpr, a.cons).SemanticTokens(ctx)...) + + if eType.KeyExpr != nil { + typ, ok := iterableKeyType(a.cons.OfType) + if !ok { + return nil, false + } + cons := schema.AnyExpression{ + OfType: typ, + } + tokens = append(tokens, newExpression(a.pathCtx, eType.KeyExpr, cons).SemanticTokens(ctx)...) + } + + typ, ok := iterableValueType(a.cons.OfType) + if !ok { + return nil, false + } + cons := schema.AnyExpression{ + OfType: typ, + } + tokens = append(tokens, newExpression(a.pathCtx, eType.ValExpr, cons).SemanticTokens(ctx)...) + + if eType.CondExpr != nil { + cons := schema.AnyExpression{ + OfType: cty.Bool, + } + tokens = append(tokens, newExpression(a.pathCtx, eType.CondExpr, cons).SemanticTokens(ctx)...) + } + + return tokens, true + } + + return tokens, false +} + +func (a Any) refOriginsForForExpr(ctx context.Context, allowSelfRefs bool) (reference.Origins, bool) { + origins := make(reference.Origins, 0) + + // There is currently no way of decoding for 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.ForExpr: + if !isTypeIterable(a.cons.OfType) { + return nil, false + } + + // TODO: eType.KeyVarExpr.Range() to collect key as origin + // TODO: eType.ValVarExpr.Range() to collect value as origin + + if collExpr, ok := newExpression(a.pathCtx, eType.CollExpr, a.cons).(ReferenceOriginsExpression); ok { + origins = append(origins, collExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + + if eType.KeyExpr != nil { + typ, ok := iterableKeyType(a.cons.OfType) + if !ok { + return nil, false + } + cons := schema.AnyExpression{ + OfType: typ, + } + if keyExpr, ok := newExpression(a.pathCtx, eType.KeyExpr, cons).(ReferenceOriginsExpression); ok { + origins = append(origins, keyExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + } + + typ, ok := iterableValueType(a.cons.OfType) + if !ok { + return nil, false + } + cons := schema.AnyExpression{ + OfType: typ, + } + if valExpr, ok := newExpression(a.pathCtx, eType.ValExpr, cons).(ReferenceOriginsExpression); ok { + origins = append(origins, valExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + + if eType.CondExpr != nil { + cons := schema.AnyExpression{ + OfType: cty.Bool, + } + + if condExpr, ok := newExpression(a.pathCtx, eType.CondExpr, cons).(ReferenceOriginsExpression); ok { + origins = append(origins, condExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + } + + return origins, true + } + + return origins, false +} + +func isTypeIterable(typ cty.Type) bool { + if typ == cty.DynamicPseudoType { + return true + } + if typ.IsListType() { + return true + } + if typ.IsMapType() { + return true + } + if typ.IsSetType() { + return true + } + if typ.IsTupleType() { + return true + } + if typ.IsObjectType() { + return true + } + return false +} + +func iterableKeyType(typ cty.Type) (cty.Type, bool) { + if typ == cty.DynamicPseudoType { + return cty.DynamicPseudoType, true + } + if typ.IsListType() { + return cty.Number, true + } + if typ.IsSetType() { + // This looks awkward but we just mimic Terraform's behaviour + return *typ.SetElementType(), true + } + if typ.IsTupleType() { + return cty.Number, true + } + if typ.IsMapType() { + return cty.String, true + } + if typ.IsObjectType() { + return cty.String, true + } + return cty.NilType, false +} + +func iterableValueType(typ cty.Type) (cty.Type, bool) { + if typ == cty.DynamicPseudoType { + return cty.DynamicPseudoType, true + } + if typ.IsListType() { + return *typ.ListElementType(), true + } + if typ.IsSetType() { + return *typ.SetElementType(), true + } + if typ.IsTupleType() { + // This is not accurate but pragmatic + return cty.DynamicPseudoType, true + } + if typ.IsMapType() { + return *typ.MapElementType(), true + } + if typ.IsObjectType() { + // This is not accurate but pragmatic + return cty.DynamicPseudoType, true + } + return cty.NilType, false +} diff --git a/decoder/expr_any_hover.go b/decoder/expr_any_hover.go index 0a2ee22c..3ecf6d0b 100644 --- a/decoder/expr_any_hover.go +++ b/decoder/expr_any_hover.go @@ -97,7 +97,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 relative traversals https://github.com/hashicorp/terraform-ls/issues/532 if hoverData, ok := a.hoverOperatorExprAtPos(ctx, pos); ok { @@ -112,6 +111,10 @@ func (a Any) hoverNonComplexExprAtPos(ctx context.Context, pos hcl.Pos) *lang.Ho return hoverData } + if hoverData, ok := a.hoverForExprAtPos(ctx, pos); ok { + return hoverData + } + if hoverData, ok := a.hoverIndexExprAtPos(ctx, pos); ok { return hoverData } diff --git a/decoder/expr_any_hover_test.go b/decoder/expr_any_hover_test.go index e57f98eb..cdc41e20 100644 --- a/decoder/expr_any_hover_test.go +++ b/decoder/expr_any_hover_test.go @@ -1813,6 +1813,571 @@ func TestHoverAtPos_exprAny_parenthesis(t *testing.T) { } } +func TestHoverAtPos_exprAny_forExpr(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refTargets reference.Targets + refOrigins reference.Origins + cfg string + pos hcl.Pos + expectedHoverData *lang.HoverData + }{ + // list + { + "list on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.List(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 2, Column: 26, Byte: 25}, + }, + }, + }, + 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: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + `attr = [for v in var.foo: v] +`, + hcl.Pos{Line: 1, Column: 20, Byte: 19}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\n_list of string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + { + "list on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = [for v in col: "foo"] +`, + hcl.Pos{Line: 1, Column: 25, Byte: 24}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + { + "list on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = [for v in col: "foo" if true] +`, + hcl.Pos{Line: 1, Column: 34, Byte: 33}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + End: hcl.Pos{Line: 1, Column: 36, Byte: 35}, + }, + }, + }, + + // set + { + "set on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.Set(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 2, Column: 26, Byte: 25}, + }, + }, + }, + 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: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + `attr = [for v in var.foo: v] +`, + hcl.Pos{Line: 1, Column: 20, Byte: 19}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\n_set of string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + { + "set on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = [for v in col: "foo"] +`, + hcl.Pos{Line: 1, Column: 25, Byte: 24}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + { + "set on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = [for v in col: "foo" if true] +`, + hcl.Pos{Line: 1, Column: 34, Byte: 33}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + End: hcl.Pos{Line: 1, Column: 36, Byte: 35}, + }, + }, + }, + + // tuple + { + "tuple on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.EmptyTuple, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 26, Byte: 25}, + End: hcl.Pos{Line: 2, Column: 26, Byte: 25}, + }, + }, + }, + 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: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + `attr = [for v in var.foo: v] +`, + hcl.Pos{Line: 1, Column: 20, Byte: 19}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\n_tuple_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + }, + }, + { + "tuple on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = [for v in col: "foo"] +`, + hcl.Pos{Line: 1, Column: 25, Byte: 24}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + { + "tuple on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = [for v in col: "foo" if true] +`, + hcl.Pos{Line: 1, Column: 34, Byte: 33}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 32, Byte: 31}, + End: hcl.Pos{Line: 1, Column: 36, Byte: 35}, + }, + }, + }, + + // map + { + "map on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 29}, + }, + }, + }, + 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: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + `attr = {for k, v in var.foo: "key" => "val"] +`, + hcl.Pos{Line: 1, Column: 23, Byte: 22}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\n_map of string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + { + "map on key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = {for k, v in var.foo: "key" => "val"] +`, + hcl.Pos{Line: 1, Column: 33, Byte: 32}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 30, Byte: 29}, + End: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + }, + }, + }, + { + "map on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = {for k, v in var.foo: "key" => "val"] +`, + hcl.Pos{Line: 1, Column: 41, Byte: 40}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 39, Byte: 38}, + End: hcl.Pos{Line: 1, Column: 44, Byte: 43}, + }, + }, + }, + { + "map on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = {for k, v in var.foo: "key" => "val" if true] +`, + hcl.Pos{Line: 1, Column: 50, Byte: 49}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 48, Byte: 47}, + End: hcl.Pos{Line: 1, Column: 52, Byte: 51}, + }, + }, + }, + + // object + { + "object on collection", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.EmptyObject, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 29}, + }, + }, + }, + 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: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + `attr = {for k, v in var.foo: "key" => "val"] +`, + hcl.Pos{Line: 1, Column: 23, Byte: 22}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\nobject"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }, + }, + { + "object on key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = {for k, v in var.foo: "key" => "val"] +`, + hcl.Pos{Line: 1, Column: 33, Byte: 32}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 30, Byte: 29}, + End: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + }, + }, + }, + { + "object on value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = {for k, v in var.foo: "key" => "val"] +`, + hcl.Pos{Line: 1, Column: 41, Byte: 40}, + &lang.HoverData{ + Content: lang.Markdown("_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 39, Byte: 38}, + End: hcl.Pos{Line: 1, Column: 44, Byte: 43}, + }, + }, + }, + { + "object on condition", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Targets{}, + reference.Origins{}, + `attr = {for k, v in var.foo: "key" => "val" if true] +`, + hcl.Pos{Line: 1, Column: 50, Byte: 49}, + &lang.HoverData{ + Content: lang.Markdown("_bool_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 48, Byte: 47}, + End: hcl.Pos{Line: 1, Column: 52, Byte: 51}, + }, + }, + }, + } + 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, + ReferenceTargets: tc.refTargets, + ReferenceOrigins: tc.refOrigins, + 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) + } + }) + } +} + func TestHoverAtPos_exprAny_templates(t *testing.T) { testCases := []struct { testName string diff --git a/decoder/expr_any_ref_origins.go b/decoder/expr_any_ref_origins.go index ff561c29..bb8bfbdd 100644 --- a/decoder/expr_any_ref_origins.go +++ b/decoder/expr_any_ref_origins.go @@ -117,7 +117,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 relative traversals https://github.com/hashicorp/terraform-ls/issues/532 if origins, ok := a.refOriginsForOperatorExpr(ctx, allowSelfRefs); ok { @@ -132,6 +131,10 @@ func (a Any) refOriginsForNonComplexExpr(ctx context.Context, allowSelfRefs bool return origins } + if origins, ok := a.refOriginsForForExpr(ctx, allowSelfRefs); ok { + return origins + } + // attempt to get accurate constraint for the origins // if we recognise the given expression funcExpr := functionExpr{ diff --git a/decoder/expr_any_ref_origins_test.go b/decoder/expr_any_ref_origins_test.go index bd666c38..5eea5f3d 100644 --- a/decoder/expr_any_ref_origins_test.go +++ b/decoder/expr_any_ref_origins_test.go @@ -1019,6 +1019,275 @@ func TestCollectRefOrigins_exprAny_parenthesis_hcl(t *testing.T) { } } +func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefOrigins reference.Origins + }{ + { + "list", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + `attr = [for key, val in var.coll: val] +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.List(cty.String)}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "val"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + End: hcl.Pos{Line: 1, Column: 38, Byte: 37}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + }, + }, + { + "set", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + `attr = [for key, val in var.coll: val] +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.Set(cty.String)}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "val"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + End: hcl.Pos{Line: 1, Column: 38, Byte: 37}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + }, + }, + { + "tuple", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + `attr = [for key, val in var.coll: val] +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.EmptyTuple}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "val"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + End: hcl.Pos{Line: 1, Column: 38, Byte: 37}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.DynamicPseudoType}, + }, + }, + }, + }, + { + "map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + `attr = {for key, val in var.coll: key => val} +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.Map(cty.String)}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "key"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + End: hcl.Pos{Line: 1, Column: 38, Byte: 37}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "val"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 42, Byte: 41}, + End: hcl.Pos{Line: 1, Column: 45, Byte: 44}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + }, + }, + { + "object", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + `attr = {for key, val in var.coll: key => val} +`, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.EmptyObject}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "key"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 35, Byte: 34}, + End: hcl.Pos{Line: 1, Column: 38, Byte: 37}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "val"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 42, Byte: 41}, + End: hcl.Pos{Line: 1, Column: 45, Byte: 44}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.DynamicPseudoType}, + }, + }, + }, + }, + } + + 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, 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 diff --git a/decoder/expr_any_semtok.go b/decoder/expr_any_semtok.go index 0bfd1dbc..67adfa79 100644 --- a/decoder/expr_any_semtok.go +++ b/decoder/expr_any_semtok.go @@ -96,7 +96,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 relative traversals https://github.com/hashicorp/terraform-ls/issues/532 if tokens, ok := a.semanticTokensForOperatorExpr(ctx); ok { @@ -111,6 +110,10 @@ func (a Any) semanticTokensForNonComplexExpr(ctx context.Context) []lang.Semanti return tokens } + if tokens, ok := a.semanticTokensForForExpr(ctx); ok { + return tokens + } + if tokens, ok := a.semanticTokensForIndexExpr(ctx); ok { return tokens } diff --git a/decoder/expr_any_semtok_test.go b/decoder/expr_any_semtok_test.go index 113b593f..a2d26584 100644 --- a/decoder/expr_any_semtok_test.go +++ b/decoder/expr_any_semtok_test.go @@ -2983,6 +2983,483 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { } } +func TestSemanticTokens_exprAny_forExpr(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + refOrigins reference.Origins + refTargets reference.Targets + cfg string + expectedSemanticTokens []lang.SemanticToken + }{ + { + "list", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.List(cty.String)}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Type: cty.List(cty.String), + }, + }, + `attr = [for k, v in var.coll: "foo" if 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.TokenReferenceStep, + 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}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + { + Type: lang.TokenString, + 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.TokenBool, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, + End: hcl.Pos{Line: 1, Column: 44, Byte: 43}, + }, + }, + }, + }, + { + "set", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Set(cty.String), + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.Set(cty.String)}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Type: cty.Set(cty.String), + }, + }, + `attr = [for k, v in var.coll: "foo" if 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.TokenReferenceStep, + 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}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + { + Type: lang.TokenString, + 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.TokenBool, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, + End: hcl.Pos{Line: 1, Column: 44, Byte: 43}, + }, + }, + }, + }, + { + "tuple", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyTuple, + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.EmptyTuple}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Type: cty.EmptyTuple, + }, + }, + `attr = [for k, v in var.coll: "foo" if 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.TokenReferenceStep, + 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}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + { + Type: lang.TokenString, + 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.TokenBool, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, + End: hcl.Pos{Line: 1, Column: 44, Byte: 43}, + }, + }, + }, + }, + { + "map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.Map(cty.String)}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Type: cty.Map(cty.String), + }, + }, + `attr = {for k, v in var.coll: "key" => "val" if 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.TokenReferenceStep, + 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}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + { + Type: lang.TokenString, + 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.TokenString, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, + End: hcl.Pos{Line: 1, Column: 45, Byte: 44}, + }, + }, + { + Type: lang.TokenBool, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 49, Byte: 48}, + End: hcl.Pos{Line: 1, Column: 53, Byte: 52}, + }, + }, + }, + }, + { + "object", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.EmptyObject, + }, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.EmptyObject}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Type: cty.EmptyObject, + }, + }, + `attr = {for k, v in var.coll: "key" => "val" if 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.TokenReferenceStep, + 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}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + }, + { + Type: lang.TokenString, + 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.TokenString, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, + End: hcl.Pos{Line: 1, Column: 45, Byte: 44}, + }, + }, + { + Type: lang.TokenBool, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 49, Byte: 48}, + End: hcl.Pos{Line: 1, Column: 53, Byte: 52}, + }, + }, + }, + }, + } + 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, + }, + 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) + } + }) + } +} + func TestSemanticTokens_exprAny_templates(t *testing.T) { testCases := []struct { testName string