diff --git a/decoder/expr_any_completion.go b/decoder/expr_any_completion.go index 7e42f97c..c65986c5 100644 --- a/decoder/expr_any_completion.go +++ b/decoder/expr_any_completion.go @@ -74,7 +74,9 @@ func (a Any) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate Elem: schema.AnyExpression{ OfType: typ.ElementType(), }, + AllowInterpolatedKeys: true, } + return newExpression(a.pathCtx, expr, cons).CompletionAtPos(ctx, pos) } @@ -85,7 +87,8 @@ func (a Any) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate } cons := schema.Object{ - Attributes: ctyObjectToObjectAttributes(typ), + Attributes: ctyObjectToObjectAttributes(typ), + AllowInterpolatedKeys: true, } return newExpression(a.pathCtx, expr, cons).CompletionAtPos(ctx, pos) } diff --git a/decoder/expr_any_completion_test.go b/decoder/expr_any_completion_test.go index 1e00e558..363ffb3b 100644 --- a/decoder/expr_any_completion_test.go +++ b/decoder/expr_any_completion_test.go @@ -3609,6 +3609,278 @@ func TestCompletionAtPos_exprAny_parentheses(t *testing.T) { }, }), }, + { + "empty parentheses as map key", + 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, + }, + }, + `attr = { + () = "foo" +} +`, + hcl.Pos{Line: 2, Column: 4, Byte: 12}, + 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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + }, + }, + }, + }), + }, + { + "parentheses with prefix as map key", + 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, + }, + }, + `attr = { + (var) = "foo" +} +`, + hcl.Pos{Line: 2, Column: 7, Byte: 15}, + 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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 7, Byte: 15}, + }, + }, + }, + }), + }, + { + "empty parentheses as map key in static map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{Type: cty.String}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + `attr = { + () = "foo" +} +`, + hcl.Pos{Line: 2, Column: 4, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "parentheses with prefix as map key in static map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{Type: cty.String}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + `attr = { + (var) = "foo" +} +`, + hcl.Pos{Line: 2, Column: 7, Byte: 15}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "empty parentheses as object attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Object(map[string]cty.Type{ + "bar": cty.String, + }), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + `attr = { + () = "foo" +} +`, + hcl.Pos{Line: 2, Column: 4, Byte: 12}, + 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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + }, + }, + }, + }), + }, + { + "parentheses with prefix as object attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Object(map[string]cty.Type{ + "bar": cty.String, + }), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + `attr = { + (var) = "foo" +} +`, + hcl.Pos{Line: 2, Column: 7, Byte: 15}, + 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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 7, Byte: 15}, + }, + }, + }, + }), + }, + { + "empty parentheses as object attribute name in static object", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": &schema.AttributeSchema{ + Constraint: schema.LiteralType{Type: cty.String}, + }, + }, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + `attr = { + () = "foo" +} +`, + hcl.Pos{Line: 2, Column: 4, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "parentheses with prefix as map key in static map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": &schema.AttributeSchema{ + Constraint: schema.LiteralType{Type: cty.String}, + }, + }, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + `attr = { + (var) = "foo" +} +`, + hcl.Pos{Line: 2, Column: 7, Byte: 15}, + lang.CompleteCandidates([]lang.Candidate{}), + }, } for i, tc := range testCases { diff --git a/decoder/expr_any_hover.go b/decoder/expr_any_hover.go index 53e0e1d9..5ba23423 100644 --- a/decoder/expr_any_hover.go +++ b/decoder/expr_any_hover.go @@ -74,6 +74,7 @@ func (a Any) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { Elem: schema.AnyExpression{ OfType: typ.ElementType(), }, + AllowInterpolatedKeys: true, } return newExpression(a.pathCtx, expr, cons).HoverAtPos(ctx, pos) } @@ -85,7 +86,8 @@ func (a Any) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { } cons := schema.Object{ - Attributes: ctyObjectToObjectAttributes(typ), + Attributes: ctyObjectToObjectAttributes(typ), + AllowInterpolatedKeys: true, } return newExpression(a.pathCtx, expr, cons).HoverAtPos(ctx, pos) } diff --git a/decoder/expr_any_hover_test.go b/decoder/expr_any_hover_test.go index 224c0f5f..e57f98eb 100644 --- a/decoder/expr_any_hover_test.go +++ b/decoder/expr_any_hover_test.go @@ -1617,6 +1617,8 @@ func TestHoverAtPos_exprAny_parenthesis(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 @@ -1630,6 +1632,8 @@ func TestHoverAtPos_exprAny_parenthesis(t *testing.T) { }, }, }, + reference.Targets{}, + reference.Origins{}, `attr = (42+3)*2 `, hcl.Pos{Line: 1, Column: 10, Byte: 9}, @@ -1651,6 +1655,8 @@ func TestHoverAtPos_exprAny_parenthesis(t *testing.T) { }, }, }, + reference.Targets{}, + reference.Origins{}, `attr = (true || false) && true `, hcl.Pos{Line: 1, Column: 11, Byte: 10}, @@ -1672,11 +1678,111 @@ func TestHoverAtPos_exprAny_parenthesis(t *testing.T) { }, }, }, + reference.Targets{}, + reference.Origins{}, `attr = (true || false) && true `, hcl.Pos{Line: 1, Column: 11, Byte: 10}, nil, }, + { + "reference as map key", + 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, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, + `attr = { + (var.foo) = "foo" +} +`, + hcl.Pos{Line: 2, Column: 7, Byte: 15}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\n_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, + { + "reference as object attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Object(map[string]cty.Type{ + "bar": cty.String, + }), + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + }, + }, + reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, + `attr = { + (var.foo) = "foo" +} +`, + hcl.Pos{Line: 2, Column: 7, Byte: 15}, + &lang.HoverData{ + Content: lang.Markdown("`var.foo`\n_string_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { @@ -1686,7 +1792,9 @@ func TestHoverAtPos_exprAny_parenthesis(t *testing.T) { f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) d := testPathDecoder(t, &PathContext{ - Schema: bodySchema, + Schema: bodySchema, + ReferenceTargets: tc.refTargets, + ReferenceOrigins: tc.refOrigins, Files: map[string]*hcl.File{ "test.tf": f, }, diff --git a/decoder/expr_any_ref_origins.go b/decoder/expr_any_ref_origins.go index 6ce35576..e6ab42f4 100644 --- a/decoder/expr_any_ref_origins.go +++ b/decoder/expr_any_ref_origins.go @@ -89,6 +89,7 @@ func (a Any) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference Elem: schema.AnyExpression{ OfType: typ.ElementType(), }, + AllowInterpolatedKeys: true, }, } return m.ReferenceOrigins(ctx, allowSelfRefs) @@ -104,7 +105,8 @@ func (a Any) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference expr: a.expr, pathCtx: a.pathCtx, cons: schema.Object{ - Attributes: ctyObjectToObjectAttributes(typ), + Attributes: ctyObjectToObjectAttributes(typ), + AllowInterpolatedKeys: true, }, } return obj.ReferenceOrigins(ctx, allowSelfRefs) diff --git a/decoder/expr_any_ref_origins_test.go b/decoder/expr_any_ref_origins_test.go index 77ae8847..bd666c38 100644 --- a/decoder/expr_any_ref_origins_test.go +++ b/decoder/expr_any_ref_origins_test.go @@ -922,6 +922,72 @@ func TestCollectRefOrigins_exprAny_parenthesis_hcl(t *testing.T) { }, }, }, + { + "reference as map key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Map(cty.String), + }, + }, + }, + `attr = { + (var.foo) = "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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.String, + }, + }, + }, + }, + }, + { + "reference as object attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Object(map[string]cty.Type{ + "bar": cty.String, + }), + }, + }, + }, + `attr = { + (var.foo) = "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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + Constraints: reference.OriginConstraints{ + { + OfType: cty.String, + }, + }, + }, + }, + }, } for i, tc := range testCases { diff --git a/decoder/expr_any_semtok.go b/decoder/expr_any_semtok.go index f530796c..cd48fca7 100644 --- a/decoder/expr_any_semtok.go +++ b/decoder/expr_any_semtok.go @@ -73,6 +73,7 @@ func (a Any) SemanticTokens(ctx context.Context) []lang.SemanticToken { Elem: schema.AnyExpression{ OfType: typ.ElementType(), }, + AllowInterpolatedKeys: true, } return newExpression(a.pathCtx, expr, cons).SemanticTokens(ctx) } @@ -84,7 +85,8 @@ func (a Any) SemanticTokens(ctx context.Context) []lang.SemanticToken { } cons := schema.Object{ - Attributes: ctyObjectToObjectAttributes(typ), + Attributes: ctyObjectToObjectAttributes(typ), + AllowInterpolatedKeys: true, } return newExpression(a.pathCtx, expr, cons).SemanticTokens(ctx) } diff --git a/decoder/expr_any_semtok_test.go b/decoder/expr_any_semtok_test.go index 0831ab5e..113b593f 100644 --- a/decoder/expr_any_semtok_test.go +++ b/decoder/expr_any_semtok_test.go @@ -2662,6 +2662,8 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { testCases := []struct { testName string attrSchema map[string]*schema.AttributeSchema + refOrigins reference.Origins + refTargets reference.Targets cfg string expectedSemanticTokens []lang.SemanticToken }{ @@ -2674,6 +2676,8 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { }, }, }, + reference.Origins{}, + reference.Targets{}, `attr = (42 + 43)*2 `, []lang.SemanticToken{ @@ -2724,6 +2728,8 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { }, }, }, + reference.Origins{}, + reference.Targets{}, `attr = (true || false) && true `, []lang.SemanticToken{ @@ -2774,6 +2780,8 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { }, }, }, + reference.Origins{}, + reference.Targets{}, `attr = (true || false) && true `, []lang.SemanticToken{ @@ -2788,6 +2796,163 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { }, }, }, + { + "reference as map key", + 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: "foo"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 1, Byte: 31}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 32}, + }, + }, + }, + `attr = { + (var.foo) = "foo" +} +`, + []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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 7, Byte: 15}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 8, Byte: 16}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + { + Type: lang.TokenString, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 15, Byte: 23}, + End: hcl.Pos{Line: 2, Column: 20, Byte: 28}, + }, + }, + }, + }, + { + "reference as object attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.AnyExpression{ + OfType: cty.Object(map[string]cty.Type{ + "bar": cty.String, + }), + }, + }, + }, + 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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + }, + }, + }, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 1, Byte: 31}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 32}, + }, + }, + }, + `attr = { + (var.foo) = "foo" +} +`, + []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: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 7, Byte: 15}, + }, + }, + { + Type: lang.TokenReferenceStep, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 8, Byte: 16}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, + }, } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { @@ -2801,6 +2966,8 @@ func TestSemanticTokens_exprAny_parenthesis(t *testing.T) { Files: map[string]*hcl.File{ "test.tf": f, }, + ReferenceOrigins: tc.refOrigins, + ReferenceTargets: tc.refTargets, }) ctx := context.Background() diff --git a/decoder/expr_map_completion.go b/decoder/expr_map_completion.go index 4b637956..6b6dd8b9 100644 --- a/decoder/expr_map_completion.go +++ b/decoder/expr_map_completion.go @@ -9,8 +9,10 @@ import ( "fmt" "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 (m Map) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { @@ -135,6 +137,18 @@ func (m Map) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate recoveryPos = item.ValueExpr.Range().End if item.KeyExpr.Range().ContainsPos(pos) { + // handle any interpolation if it is allowed + keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + if ok && m.cons.AllowInterpolatedKeys { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + return newExpression(m.pathCtx, parensExpr, keyCons).CompletionAtPos(ctx, pos) + } + } + return []lang.Candidate{} } if item.ValueExpr.Range().ContainsPos(pos) || item.ValueExpr.Range().End.Byte == pos.Byte { @@ -162,6 +176,15 @@ func (m Map) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate } } + // parenthesis implies interpolated map key + if trimmedBytes[len(trimmedBytes)-1] == '(' && m.cons.AllowInterpolatedKeys { + emptyExpr := newEmptyExpressionAtPos(eType.Range().Filename, pos) + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + return newExpression(m.pathCtx, emptyExpr, keyCons).CompletionAtPos(ctx, pos) + } + // if last byte is =, then it's incomplete attribute if trimmedBytes[len(trimmedBytes)-1] == '=' { emptyExpr := newEmptyExpressionAtPos(eType.Range().Filename, pos) diff --git a/decoder/expr_map_hover.go b/decoder/expr_map_hover.go index 00ee4873..4759aaec 100644 --- a/decoder/expr_map_hover.go +++ b/decoder/expr_map_hover.go @@ -8,8 +8,10 @@ import ( "fmt" "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 (m Map) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { @@ -20,7 +22,17 @@ func (m Map) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { for _, item := range eType.Items { if item.KeyExpr.Range().ContainsPos(pos) { - // no hover for map keys + keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + if ok && m.cons.AllowInterpolatedKeys { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + expr := newExpression(m.pathCtx, parensExpr, keyCons) + return expr.HoverAtPos(ctx, pos) + } + } return nil } diff --git a/decoder/expr_map_ref_origins.go b/decoder/expr_map_ref_origins.go index 05475ed4..d8437c84 100644 --- a/decoder/expr_map_ref_origins.go +++ b/decoder/expr_map_ref_origins.go @@ -7,7 +7,10 @@ import ( "context" "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 (m Map) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { @@ -23,10 +26,23 @@ func (m Map) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference origins := make(reference.Origins, 0) for _, item := range items { - expr := newExpression(m.pathCtx, item.Value, m.cons.Elem) + keyExpr, ok := item.Key.(*hclsyntax.ObjectConsKeyExpr) + if ok { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + kExpr := newExpression(m.pathCtx, parensExpr, keyCons) + if expr, ok := kExpr.(ReferenceOriginsExpression); ok { + origins = append(origins, expr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + } + } - if elemExpr, ok := expr.(ReferenceOriginsExpression); ok { - origins = append(origins, elemExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + valExpr := newExpression(m.pathCtx, item.Value, m.cons.Elem) + if expr, ok := valExpr.(ReferenceOriginsExpression); ok { + origins = append(origins, expr.ReferenceOrigins(ctx, allowSelfRefs)...) } } diff --git a/decoder/expr_map_semtok.go b/decoder/expr_map_semtok.go index ea156e67..b867d1d5 100644 --- a/decoder/expr_map_semtok.go +++ b/decoder/expr_map_semtok.go @@ -7,7 +7,9 @@ import ( "context" "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" ) func (m Map) SemanticTokens(ctx context.Context) []lang.SemanticToken { @@ -23,18 +25,33 @@ func (m Map) SemanticTokens(ctx context.Context) []lang.SemanticToken { tokens := make([]lang.SemanticToken, 0) for _, item := range eType.Items { - _, _, ok := rawObjectKey(item.KeyExpr) - if !ok { + _, _, isRawKey := rawObjectKey(item.KeyExpr) + if isRawKey { + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenMapKey, + Modifiers: lang.SemanticTokenModifiers{}, + Range: item.KeyExpr.Range(), + }) + + vExpr := newExpression(m.pathCtx, item.ValueExpr, m.cons.Elem) + tokens = append(tokens, vExpr.SemanticTokens(ctx)...) continue } - tokens = append(tokens, lang.SemanticToken{ - Type: lang.TokenMapKey, - Modifiers: lang.SemanticTokenModifiers{}, - Range: item.KeyExpr.Range(), - }) - - expr := newExpression(m.pathCtx, item.ValueExpr, m.cons.Elem) - tokens = append(tokens, expr.SemanticTokens(ctx)...) + + keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + if ok && m.cons.AllowInterpolatedKeys { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + kExpr := newExpression(m.pathCtx, parensExpr, keyCons) + tokens = append(tokens, kExpr.SemanticTokens(ctx)...) + + vExpr := newExpression(m.pathCtx, item.ValueExpr, m.cons.Elem) + tokens = append(tokens, vExpr.SemanticTokens(ctx)...) + } + } } return tokens diff --git a/decoder/expr_object_completion.go b/decoder/expr_object_completion.go index cd7c03a8..2a808170 100644 --- a/decoder/expr_object_completion.go +++ b/decoder/expr_object_completion.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" ) type declaredAttributes map[string]hcl.Range @@ -81,14 +82,12 @@ func (obj Object) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candi return []lang.Candidate{} } - attrName, attrRange, ok := rawObjectKey(item.KeyExpr) - if !ok { - continue + attrName, attrRange, isRawName := rawObjectKey(item.KeyExpr) + if isRawName { + // collect all declared attributes + declared[attrName] = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) } - // collect all declared attributes - declared[attrName] = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) - if nextItemRange != nil { continue } @@ -106,18 +105,33 @@ func (obj Object) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candi recoveryPos = item.ValueExpr.Range().End if item.KeyExpr.Range().ContainsPos(pos) { - prefix := "" - - // if we're before start of the attribute - // it means the attribute is likely quoted - if pos.Byte >= attrRange.Start.Byte { - prefixLen := pos.Byte - attrRange.Start.Byte - prefix = attrName[0:prefixLen] + // handle any interpolation if it is allowed + keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + if ok && obj.cons.AllowInterpolatedKeys { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + return newExpression(obj.pathCtx, parensExpr, keyCons).CompletionAtPos(ctx, pos) + } } - editRange := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + if isRawName { + prefix := "" + // if we're before start of the attribute + // it means the attribute is likely quoted + if pos.Byte >= attrRange.Start.Byte { + prefixLen := pos.Byte - attrRange.Start.Byte + prefix = attrName[0:prefixLen] + } + + editRange := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + + return objectAttributesToCandidates(ctx, prefix, obj.cons.Attributes, declared, editRange) + } - return objectAttributesToCandidates(ctx, prefix, obj.cons.Attributes, declared, editRange) + return []lang.Candidate{} } if item.ValueExpr.Range().ContainsPos(pos) || item.ValueExpr.Range().End.Byte == pos.Byte { aSchema, ok := obj.cons.Attributes[attrName] @@ -169,6 +183,15 @@ func (obj Object) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candi return isObjectItemTerminatingRune(r) || unicode.IsSpace(r) }) + // parenthesis implies interpolated attribute name + if trimmedBytes[len(trimmedBytes)-1] == '(' && obj.cons.AllowInterpolatedKeys { + emptyExpr := newEmptyExpressionAtPos(eType.Range().Filename, pos) + attrNameCons := schema.AnyExpression{ + OfType: cty.String, + } + return newExpression(obj.pathCtx, emptyExpr, attrNameCons).CompletionAtPos(ctx, pos) + } + // if last byte is =, then it's incomplete attribute if len(trimmedBytes) > 0 && trimmedBytes[len(trimmedBytes)-1] == '=' { emptyExpr := newEmptyExpressionAtPos(eType.Range().Filename, pos) diff --git a/decoder/expr_object_hover.go b/decoder/expr_object_hover.go index 889d1084..0c1dfaf9 100644 --- a/decoder/expr_object_hover.go +++ b/decoder/expr_object_hover.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" ) func (obj Object) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { @@ -20,28 +21,39 @@ func (obj Object) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { } for _, item := range eType.Items { - attrName, _, ok := rawObjectKey(item.KeyExpr) - if !ok { - continue - } + attrName, _, isRawKey := rawObjectKey(item.KeyExpr) - aSchema, ok := obj.cons.Attributes[attrName] - if !ok { - // unknown attribute - continue + var aSchema *schema.AttributeSchema + var isKnownAttr bool + if isRawKey { + aSchema, isKnownAttr = obj.cons.Attributes[attrName] } if item.KeyExpr.Range().ContainsPos(pos) { - itemRng := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) - content := hoverContentForAttribute(attrName, aSchema) + // handle any interpolation if it is allowed + keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + if ok && obj.cons.AllowInterpolatedKeys { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + return newExpression(obj.pathCtx, parensExpr, keyCons).HoverAtPos(ctx, pos) + } + } + + if isKnownAttr { + itemRng := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + content := hoverContentForAttribute(attrName, aSchema) - return &lang.HoverData{ - Content: content, - Range: itemRng, + return &lang.HoverData{ + Content: content, + Range: itemRng, + } } } - if item.ValueExpr.Range().ContainsPos(pos) { + if isKnownAttr && item.ValueExpr.Range().ContainsPos(pos) { expr := newExpression(obj.pathCtx, item.ValueExpr, aSchema.Constraint) return expr.HoverAtPos(ctx, pos) } diff --git a/decoder/expr_object_ref_origins.go b/decoder/expr_object_ref_origins.go index ada4f1c7..71bab1fd 100644 --- a/decoder/expr_object_ref_origins.go +++ b/decoder/expr_object_ref_origins.go @@ -7,7 +7,10 @@ import ( "context" "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 (obj Object) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { @@ -23,21 +26,33 @@ func (obj Object) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) refe origins := make(reference.Origins, 0) for _, item := range items { - attrName, _, ok := rawObjectKey(item.Key) - if !ok { - continue - } + attrName, _, isRawKey := rawObjectKey(item.Key) - aSchema, ok := obj.cons.Attributes[attrName] - if !ok { - // skip unknown attribute - continue + var aSchema *schema.AttributeSchema + var isKnownAttr bool + if isRawKey { + aSchema, isKnownAttr = obj.cons.Attributes[attrName] } - expr := newExpression(obj.pathCtx, item.Value, aSchema.Constraint) + keyExpr, ok := item.Key.(*hclsyntax.ObjectConsKeyExpr) + if ok { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + kExpr := newExpression(obj.pathCtx, parensExpr, keyCons) + if expr, ok := kExpr.(ReferenceOriginsExpression); ok { + origins = append(origins, expr.ReferenceOrigins(ctx, allowSelfRefs)...) + } + } + } - if elemExpr, ok := expr.(ReferenceOriginsExpression); ok { - origins = append(origins, elemExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + if isKnownAttr { + expr := newExpression(obj.pathCtx, item.Value, aSchema.Constraint) + if elemExpr, ok := expr.(ReferenceOriginsExpression); ok { + origins = append(origins, elemExpr.ReferenceOrigins(ctx, allowSelfRefs)...) + } } } diff --git a/decoder/expr_object_semtok.go b/decoder/expr_object_semtok.go index 39d46450..3732a39d 100644 --- a/decoder/expr_object_semtok.go +++ b/decoder/expr_object_semtok.go @@ -7,7 +7,9 @@ import ( "context" "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" ) func (obj Object) SemanticTokens(ctx context.Context) []lang.SemanticToken { @@ -23,27 +25,37 @@ func (obj Object) SemanticTokens(ctx context.Context) []lang.SemanticToken { tokens := make([]lang.SemanticToken, 0) for _, item := range eType.Items { - attrName, _, ok := rawObjectKey(item.KeyExpr) - if !ok { - // invalid expression - continue + attrName, _, isRawKey := rawObjectKey(item.KeyExpr) + + var aSchema *schema.AttributeSchema + var isKnownAttr bool + if isRawKey { + aSchema, isKnownAttr = obj.cons.Attributes[attrName] } - aSchema, ok := obj.cons.Attributes[attrName] - if !ok { - // skip unknown attribute - continue + keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + if ok && obj.cons.AllowInterpolatedKeys { + parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr) + if ok { + keyCons := schema.AnyExpression{ + OfType: cty.String, + } + kExpr := newExpression(obj.pathCtx, parensExpr, keyCons) + tokens = append(tokens, kExpr.SemanticTokens(ctx)...) + } } - tokens = append(tokens, lang.SemanticToken{ - Type: lang.TokenObjectKey, - Modifiers: lang.SemanticTokenModifiers{}, - // TODO: Consider not reporting the quotes? - Range: item.KeyExpr.Range(), - }) + if isKnownAttr { + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenObjectKey, + Modifiers: lang.SemanticTokenModifiers{}, + // TODO: Consider not reporting the quotes? + Range: item.KeyExpr.Range(), + }) - expr := newExpression(obj.pathCtx, item.ValueExpr, aSchema.Constraint) - tokens = append(tokens, expr.SemanticTokens(ctx)...) + expr := newExpression(obj.pathCtx, item.ValueExpr, aSchema.Constraint) + tokens = append(tokens, expr.SemanticTokens(ctx)...) + } } return tokens diff --git a/decoder/expression.go b/decoder/expression.go index 4c9b908e..8dafbe7b 100644 --- a/decoder/expression.go +++ b/decoder/expression.go @@ -127,8 +127,9 @@ func newExpression(pathContext *PathContext, expr hcl.Expression, cons schema.Co } case schema.LiteralValue: return LiteralValue{ - expr: expr, - cons: c, + expr: expr, + cons: c, + pathCtx: pathContext, } case schema.TypeDeclaration: return TypeDeclaration{ diff --git a/schema/constraint_map.go b/schema/constraint_map.go index 1373cc0d..e0c9c202 100644 --- a/schema/constraint_map.go +++ b/schema/constraint_map.go @@ -30,6 +30,10 @@ type Map struct { // MaxItems defines maximum number of items (affects completion) MaxItems uint64 + + // AllowInterpolatedKeys determines whether the key names can be + // interpolated (true) or static (literal strings only). + AllowInterpolatedKeys bool } func (Map) isConstraintImpl() constraintSigil { @@ -52,11 +56,12 @@ func (m Map) Copy() Constraint { elem = m.Elem.Copy() } return Map{ - Elem: elem, - Name: m.Name, - Description: m.Description, - MinItems: m.MinItems, - MaxItems: m.MaxItems, + Elem: elem, + Name: m.Name, + Description: m.Description, + MinItems: m.MinItems, + MaxItems: m.MaxItems, + AllowInterpolatedKeys: m.AllowInterpolatedKeys, } } diff --git a/schema/constraint_object.go b/schema/constraint_object.go index a887272f..21f5ac2e 100644 --- a/schema/constraint_object.go +++ b/schema/constraint_object.go @@ -25,6 +25,10 @@ type Object struct { // Description defines description of the whole object (affects hover) Description lang.MarkupContent + + // AllowInterpolatedKeys determines whether the attribute names can be + // interpolated (true) or static (literal strings only). + AllowInterpolatedKeys bool } type ObjectAttributes map[string]*AttributeSchema @@ -42,9 +46,10 @@ func (o Object) FriendlyName() string { func (o Object) Copy() Constraint { return Object{ - Attributes: o.Attributes.Copy(), - Name: o.Name, - Description: o.Description, + Attributes: o.Attributes.Copy(), + Name: o.Name, + Description: o.Description, + AllowInterpolatedKeys: o.AllowInterpolatedKeys, } }