diff --git a/decoder/expression_candidates.go b/decoder/expression_candidates.go index f2824a94..abc590d1 100644 --- a/decoder/expression_candidates.go +++ b/decoder/expression_candidates.go @@ -62,7 +62,9 @@ func constraintsAtPos(expr hcl.Expression, constraints ExprConstraints, pos hcl. undeclaredAttributes := oe.Attributes for _, item := range eType.Items { key, _ := item.KeyExpr.Value(nil) - if !key.IsWhollyKnown() || key.Type() != cty.String { + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { + // skip items keys that can't be interpolated + // without further context continue } attr, ok := oe.Attributes[key.AsString()] diff --git a/decoder/expression_candidates_test.go b/decoder/expression_candidates_test.go index 7ae63dd2..b7cadd9e 100644 --- a/decoder/expression_candidates_test.go +++ b/decoder/expression_candidates_test.go @@ -290,6 +290,57 @@ func TestDecoder_CandidateAtPos_expressions(t *testing.T) { }, }), }, + { + "object as expression - attribute key unknown", + map[string]*schema.AttributeSchema{ + "attr": { + Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "first": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "second": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Number), + }, + }, + }, + }, + }, + }, + `attr = { + first = "blah" + var.test = "foo" + "${var.env}.${another}" = "prod" + +} +`, + hcl.Pos{Line: 5, Column: 3, Byte: 82}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "second", + Detail: "number", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 5, + Column: 3, + Byte: 82, + }, + End: hcl.Pos{ + Line: 5, + Column: 3, + Byte: 82, + }, + }, + NewText: "second = 1", + Snippet: "second = ${1:1}", + }, + Kind: lang.AttributeCandidateKind, + }, + }), + }, { "list as value", map[string]*schema.AttributeSchema{ diff --git a/decoder/expression_constraints.go b/decoder/expression_constraints.go index 25f87819..d1d7819c 100644 --- a/decoder/expression_constraints.go +++ b/decoder/expression_constraints.go @@ -146,7 +146,9 @@ func (ec ExprConstraints) LiteralValueOfObjectConsExpr(expr *hclsyntax.ObjectCon exprValues := make(map[string]cty.Value) for _, item := range expr.Items { key, _ := item.KeyExpr.Value(nil) - if !key.IsWhollyKnown() || key.Type() != cty.String { + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { + // Avoid building incomplete object with keys + // that can't be interpolated without further context return schema.LiteralValue{}, false } diff --git a/decoder/hover.go b/decoder/hover.go index c9c685f0..8d21533e 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -369,7 +369,9 @@ func hoverDataForExpr(expr hcl.Expression, constraints ExprConstraints, pos hcl. func hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, oe schema.ObjectExpr, pos hcl.Pos) (*lang.HoverData, error) { for _, item := range objExpr.Items { key, _ := item.KeyExpr.Value(nil) - if !key.IsWhollyKnown() || key.Type() != cty.String { + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { + // skip items keys that can't be interpolated + // without further context continue } attr, ok := oe.Attributes[key.AsString()] diff --git a/decoder/hover_expressions_test.go b/decoder/hover_expressions_test.go index 50ea8382..0158dfa4 100644 --- a/decoder/hover_expressions_test.go +++ b/decoder/hover_expressions_test.go @@ -253,6 +253,52 @@ _object_`), }, nil, }, + { + "object as type with unknown key", + map[string]*schema.AttributeSchema{ + "litobj": {Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{ + "source": cty.String, + "bool": cty.Bool, + "notbool": cty.String, + "nested_map": cty.Map(cty.String), + "nested_obj": cty.Object(map[string]cty.Type{}), + }))}, + }, + `litobj = { + "${var.src}" = "blah" + "${var.env}.${another}" = "prod" + "different" = 42 + "bool" = true + "notbool" = "test" + }`, + hcl.Pos{Line: 4, Column: 12, Byte: 65}, + &lang.HoverData{ + Content: lang.Markdown("```" + ` +{ + bool = bool + nested_map = map of string + nested_obj = object + notbool = string + source = string +} +` + "```" + ` +_object_`), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 10, + Byte: 9, + }, + End: hcl.Pos{ + Line: 7, + Column: 4, + Byte: 139, + }, + }, + }, + nil, + }, { "object as expression", map[string]*schema.AttributeSchema{ @@ -303,6 +349,57 @@ _object_`), }, nil, }, + { + "object as expression with unknown key", + map[string]*schema.AttributeSchema{ + "obj": {Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "source": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "bool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Bool), + }, + "notbool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "nested_map": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Map(cty.String)), + }, + "nested_obj": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{})), + }, + }, + }, + }}, + }, + `obj = { + var.src = "blah" + "${var.env}.${another}" = "prod" + different = 42 + bool = true + notbool = "test" +}`, + hcl.Pos{Line: 1, Column: 3, Byte: 2}, + &lang.HoverData{ + Content: lang.Markdown(`**obj** _object_`), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 7, + Column: 2, + Byte: 123, + }, + }, + }, + nil, + }, { "object as expression - expression", map[string]*schema.AttributeSchema{ diff --git a/decoder/semantic_tokens.go b/decoder/semantic_tokens.go index d8e78b24..567aed71 100644 --- a/decoder/semantic_tokens.go +++ b/decoder/semantic_tokens.go @@ -178,7 +178,9 @@ func tokensForExpression(expr hclsyntax.Expression, constraints ExprConstraints) if ok { for _, item := range eType.Items { key, _ := item.KeyExpr.Value(nil) - if !key.IsWhollyKnown() || key.Type() != cty.String { + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { + // skip items keys that can't be interpolated + // without further context continue } attr, ok := oe.Attributes[key.AsString()] @@ -299,19 +301,23 @@ func tokensForObjectConsExpr(expr *hclsyntax.ObjectConsExpr, exprType cty.Type) attrTypes := exprType.AttributeTypes() for _, item := range expr.Items { key, _ := item.KeyExpr.Value(nil) - if key.IsWhollyKnown() && key.Type() == cty.String { - valType, ok := attrTypes[key.AsString()] - if !ok { - // unknown attribute - continue - } - tokens = append(tokens, lang.SemanticToken{ - Type: lang.TokenObjectKey, - Modifiers: []lang.SemanticTokenModifier{}, - Range: item.KeyExpr.Range(), - }) - tokens = append(tokens, tokenForTypedExpression(item.ValueExpr, valType)...) + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { + // skip items keys that can't be interpolated + // without further context + continue } + + valType, ok := attrTypes[key.AsString()] + if !ok { + // unknown attribute + continue + } + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenObjectKey, + Modifiers: []lang.SemanticTokenModifier{}, + Range: item.KeyExpr.Range(), + }) + tokens = append(tokens, tokenForTypedExpression(item.ValueExpr, valType)...) } } if exprType.IsMapType() { diff --git a/decoder/semantic_tokens_expr_test.go b/decoder/semantic_tokens_expr_test.go index dea13b6f..8c4d04e9 100644 --- a/decoder/semantic_tokens_expr_test.go +++ b/decoder/semantic_tokens_expr_test.go @@ -304,6 +304,74 @@ EOT `obj = { knownkey = 42 unknownkey = "boo" +}`, + []lang.SemanticToken{ + { // obj + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 4, + Byte: 3, + }, + }, + }, + { // knownkey + Type: lang.TokenObjectKey, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 10, + }, + End: hcl.Pos{ + Line: 2, + Column: 11, + Byte: 18, + }, + }, + }, + { // 42 + Type: lang.TokenNumber, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 14, + Byte: 21, + }, + End: hcl.Pos{ + Line: 2, + Column: 16, + Byte: 23, + }, + }, + }, + }, + }, + { + "object as type with unknown key", + map[string]*schema.AttributeSchema{ + "obj": { + Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{ + "knownkey": cty.Number, + })), + }, + }, + `obj = { + knownkey = 42 + "${var.env}.${another}" = "prod" + var.test = "boo" }`, []lang.SemanticToken{ { // obj @@ -377,6 +445,80 @@ EOT `obj = { knownkey = 42 unknownkey = "boo" +}`, + []lang.SemanticToken{ + { // obj + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 4, + Byte: 3, + }, + }, + }, + { // knownkey + Type: lang.TokenObjectKey, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 10, + }, + End: hcl.Pos{ + Line: 2, + Column: 11, + Byte: 18, + }, + }, + }, + { // 42 + Type: lang.TokenNumber, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 14, + Byte: 21, + }, + End: hcl.Pos{ + Line: 2, + Column: 16, + Byte: 23, + }, + }, + }, + }, + }, + { + "object as expression with unknown key", + map[string]*schema.AttributeSchema{ + "obj": { + Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "knownkey": { + Expr: schema.LiteralTypeOnly(cty.Number), + }, + }, + }, + }, + }, + }, + `obj = { + knownkey = 42 + var.test = 32 + "${var.env}.${another}" = "prod" }`, []lang.SemanticToken{ { // obj diff --git a/decoder/symbols.go b/decoder/symbols.go index 70684359..24c5c9f8 100644 --- a/decoder/symbols.go +++ b/decoder/symbols.go @@ -125,7 +125,7 @@ func nestedSymbolsForExpr(expr hcl.Expression) []Symbol { case *hclsyntax.ObjectConsExpr: for _, item := range e.Items { key, _ := item.KeyExpr.Value(nil) - if key.IsNull() || key.Type() != cty.String { + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { // skip items keys that can't be interpolated // without further context continue diff --git a/decoder/symbols_test.go b/decoder/symbols_test.go index 51d4511d..e651f5c3 100644 --- a/decoder/symbols_test.go +++ b/decoder/symbols_test.go @@ -452,6 +452,171 @@ resource "aws_instance" "test" { } } +func TestDecoder_Symbols_unknownExpression(t *testing.T) { + d := NewDecoder() + + testCfg := []byte(` +resource "aws_instance" "test" { + subnet_ids = [ var.test, "two-2" ] + configuration = { + var.key = "blah" + num = var.value + "${var.env}.${another}" = "prod" + foo(var.arg) = "bar" + } + random_kw = var.value +} +`) + f, pDiags := hclsyntax.ParseConfig(testCfg, "test.tf", hcl.InitialPos) + if len(pDiags) > 0 { + t.Fatal(pDiags) + } + + err := d.LoadFile("test.tf", f) + if err != nil { + t.Fatal(err) + } + + symbols, err := d.SymbolsInFile("test.tf") + if err != nil { + t.Fatal(err) + } + + expectedSymbols := []Symbol{ + &BlockSymbol{ + Type: "resource", + Labels: []string{ + "aws_instance", + "test", + }, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, + End: hcl.Pos{Line: 11, Column: 2, Byte: 220}, + }, + nestedSymbols: []Symbol{ + &AttributeSymbol{ + AttrName: "subnet_ids", + ExprKind: lang.TupleConsExprKind{}, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 36, + }, + End: hcl.Pos{ + Line: 3, + Column: 37, + Byte: 70, + }, + }, + nestedSymbols: []Symbol{ + &ExprSymbol{ + ExprName: "0", + ExprKind: lang.TraversalExprKind{}, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 18, + Byte: 51, + }, + End: hcl.Pos{ + Line: 3, + Column: 26, + Byte: 59, + }, + }, + nestedSymbols: []Symbol{}, + }, + &ExprSymbol{ + ExprName: "1", + ExprKind: lang.LiteralTypeKind{ + Type: cty.String, + }, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 28, + Byte: 61, + }, + End: hcl.Pos{ + Line: 3, + Column: 35, + Byte: 68, + }, + }, + nestedSymbols: []Symbol{}, + }, + }, + }, + &AttributeSymbol{ + AttrName: "configuration", + ExprKind: lang.ObjectConsExprKind{}, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 3, + Byte: 73, + }, + End: hcl.Pos{ + Line: 9, + Column: 4, + Byte: 194, + }, + }, + nestedSymbols: []Symbol{ + &ExprSymbol{ + ExprName: "num", + ExprKind: lang.TraversalExprKind{}, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 6, + Column: 4, + Byte: 114, + }, + End: hcl.Pos{ + Line: 6, + Column: 19, + Byte: 129, + }, + }, + nestedSymbols: []Symbol{}, + }, + }, + }, + &AttributeSymbol{ + AttrName: "random_kw", + ExprKind: lang.TraversalExprKind{}, + rng: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 10, + Column: 3, + Byte: 197, + }, + End: hcl.Pos{ + Line: 10, + Column: 24, + Byte: 218, + }, + }, + nestedSymbols: []Symbol{}, + }, + }, + }, + } + + diff := cmp.Diff(expectedSymbols, symbols) + if diff != "" { + t.Fatalf("unexpected symbols:\n%s", diff) + } +} + func TestDecoder_Symbols_query(t *testing.T) { d := NewDecoder()