diff --git a/decoder/expression_candidates.go b/decoder/expression_candidates.go index 38736b48..300720ac 100644 --- a/decoder/expression_candidates.go +++ b/decoder/expression_candidates.go @@ -287,6 +287,28 @@ func (d *Decoder) constraintToCandidates(constraint schema.ExprConstraint, outer }, }) } + case schema.TypeDeclarationExpr: + for _, t := range []string{ + "bool", + "number", + "string", + "list()", + "set()", + "tuple()", + "map()", + "object({})", + } { + candidates = append(candidates, lang.Candidate{ + Label: t, + Detail: t, + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: t, + Snippet: snippetForTypeDeclaration(t), + Range: editRng, + }, + }) + } } return candidates @@ -371,6 +393,23 @@ func newTextForConstraints(cons schema.ExprConstraints, isNested bool) string { return "" } +func snippetForTypeDeclaration(td string) string { + switch td { + case "list()": + return "list(${0})" + case "set()": + return "set(${0})" + case "tuple()": + return "tuple(${0})" + case "map()": + return "map(${0})" + case "object({})": + return "object({\n ${1:name} = ${2}\n})" + default: + return td + } +} + func snippetForConstraints(placeholder uint, cons schema.ExprConstraints, isNested bool) string { for _, constraint := range cons { switch c := constraint.(type) { diff --git a/decoder/expression_candidates_test.go b/decoder/expression_candidates_test.go index bd8fb45c..6b464d1f 100644 --- a/decoder/expression_candidates_test.go +++ b/decoder/expression_candidates_test.go @@ -1281,6 +1281,166 @@ func TestDecoder_CandidateAtPos_expressions(t *testing.T) { hcl.Pos{Line: 1, Column: 8, Byte: 7}, lang.ZeroCandidates(), }, + { + "type declaration", + map[string]*schema.AttributeSchema{ + "attr": { + Expr: schema.ExprConstraints{ + schema.TypeDeclarationExpr{}, + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "bool", + Detail: "bool", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "bool", Snippet: "bool"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "number", + Detail: "number", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "number", Snippet: "number"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "string", + Detail: "string", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "string", Snippet: "string"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "list()", + Detail: "list()", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "list()", Snippet: "list(${0})"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "set()", + Detail: "set()", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "set()", Snippet: "set(${0})"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "tuple()", + Detail: "tuple()", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "tuple()", Snippet: "tuple(${0})"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "map()", + Detail: "map()", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "map()", Snippet: "map(${0})"}, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "object({})", + Detail: "object({})", + TextEdit: lang.TextEdit{Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, NewText: "object({})", Snippet: "object({\n ${1:name} = ${2}\n})"}, + Kind: lang.AttributeCandidateKind, + }, + }), + }, } for i, tc := range testCases { diff --git a/decoder/expression_constraints.go b/decoder/expression_constraints.go index d86026d6..407cbc04 100644 --- a/decoder/expression_constraints.go +++ b/decoder/expression_constraints.go @@ -219,3 +219,12 @@ func (ec ExprConstraints) LiteralValueOfObjectConsExpr(expr *hclsyntax.ObjectCon return schema.LiteralValue{}, false } + +func (ec ExprConstraints) TypeDeclarationExpr() (schema.TypeDeclarationExpr, bool) { + for _, c := range ec { + if td, ok := c.(schema.TypeDeclarationExpr); ok { + return td, ok + } + } + return schema.TypeDeclarationExpr{}, false +} diff --git a/decoder/hover.go b/decoder/hover.go index 89c8fcd9..5ffdc0f7 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -246,6 +246,22 @@ func (d *Decoder) hoverDataForExpr(expr hcl.Expression, constraints ExprConstrai Range: expr.Range(), }, nil } + + _, ok = constraints.TypeDeclarationExpr() + if ok { + return &lang.HoverData{ + Content: lang.Markdown("Type declaration"), + Range: expr.Range(), + }, nil + } + case *hclsyntax.FunctionCallExpr: + _, ok := constraints.TypeDeclarationExpr() + if ok { + return &lang.HoverData{ + Content: lang.Markdown("Type declaration"), + Range: expr.Range(), + }, nil + } case *hclsyntax.TemplateExpr: if e.IsStringLiteral() { data, err := d.hoverDataForExpr(e.Parts[0], constraints, nestingLvl, pos) diff --git a/decoder/hover_test.go b/decoder/hover_test.go index 9afb9406..ba45dff8 100644 --- a/decoder/hover_test.go +++ b/decoder/hover_test.go @@ -584,3 +584,103 @@ My food block }) } } + +func TestDecoder_HoverAtPos_typeDeclaration(t *testing.T) { + resourceLabelSchema := []*schema.LabelSchema{ + {Name: "name", IsDepKey: true}, + } + blockSchema := &schema.BlockSchema{ + Labels: resourceLabelSchema, + Description: lang.Markdown("My special block"), + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "type": { + Expr: schema.ExprConstraints{schema.TypeDeclarationExpr{}}, + IsOptional: true, + Description: lang.PlainText("Special attribute"), + }, + }, + }, + } + bodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "myblock": blockSchema, + }, + } + + d := NewDecoder() + d.SetSchema(bodySchema) + + testCases := []struct { + name string + cfg string + expectedData *lang.HoverData + }{ + { + "primitive type", + `myblock "sushi" { + type = string +} +`, + &lang.HoverData{ + Content: lang.Markdown("Type declaration"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 10, Byte: 27}, + End: hcl.Pos{Line: 2, Column: 16, Byte: 33}, + }, + }, + }, + { + "capsule type", + `myblock "sushi" { + type = list(string) +} +`, + &lang.HoverData{ + Content: lang.Markdown("Type declaration"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 10, Byte: 27}, + End: hcl.Pos{Line: 2, Column: 22, Byte: 39}, + }, + }, + }, + { + "object type", + `myblock "sushi" { + type = object({ + vegan = bool + }) +} +`, + &lang.HoverData{ + Content: lang.Markdown("Type declaration"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 10, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 5, Byte: 56}, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + testConfig := []byte(tc.cfg) + f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) + err := d.LoadFile("test.tf", f) + if err != nil { + t.Fatal(err) + } + pos := hcl.Pos{Line: 2, Column: 6, Byte: 32} + data, err := d.HoverAtPos("test.tf", pos) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tc.expectedData, data, ctydebug.CmpOptions); diff != "" { + t.Fatalf("hover data mismatch: %s", diff) + } + }) + } +} diff --git a/decoder/semantic_tokens.go b/decoder/semantic_tokens.go index 223d9403..9813e722 100644 --- a/decoder/semantic_tokens.go +++ b/decoder/semantic_tokens.go @@ -218,6 +218,28 @@ func (d *Decoder) tokensForExpression(expr hclsyntax.Expression, constraints Exp } } + _, ok = constraints.TypeDeclarationExpr() + if ok { + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenTypePrimitive, + Modifiers: []lang.SemanticTokenModifier{}, + Range: expr.Range(), + }) + } + case *hclsyntax.FunctionCallExpr: + _, ok := constraints.TypeDeclarationExpr() + if ok { + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenTypeCapsule, + Modifiers: []lang.SemanticTokenModifier{}, + Range: eType.NameRange, + }) + for _, arg := range eType.Args { + tokens = append(tokens, d.tokensForExpression(arg, constraints)...) + } + return tokens + } + case *hclsyntax.TemplateExpr: // complex templates are not supported yet if !eType.IsStringLiteral() && !isMultilineStringLiteral(eType) { @@ -323,6 +345,10 @@ func (d *Decoder) tokensForExpression(expr hclsyntax.Expression, constraints Exp if ok { return tokensForObjectConsExpr(eType, litVal.Val.Type()) } + _, ok = constraints.TypeDeclarationExpr() + if ok { + return d.tokensForObjectConsTypeDeclarationExpr(eType, constraints) + } case *hclsyntax.LiteralValueExpr: valType := eType.Val.Type() if constraints.HasLiteralTypeOf(valType) { @@ -335,6 +361,27 @@ func (d *Decoder) tokensForExpression(expr hclsyntax.Expression, constraints Exp return tokens } +func (d *Decoder) tokensForObjectConsTypeDeclarationExpr(expr *hclsyntax.ObjectConsExpr, constraints ExprConstraints) []lang.SemanticToken { + tokens := make([]lang.SemanticToken, 0) + for _, item := range expr.Items { + key, _ := item.KeyExpr.Value(nil) + if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String { + // skip items keys that can't be interpolated + // without further context + continue + } + + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: item.KeyExpr.Range(), + }) + + tokens = append(tokens, d.tokensForExpression(item.ValueExpr, constraints)...) + } + return tokens +} + func tokenForTypedExpression(expr hclsyntax.Expression, consType cty.Type) []lang.SemanticToken { switch eType := expr.(type) { case *hclsyntax.LiteralValueExpr: diff --git a/decoder/semantic_tokens_test.go b/decoder/semantic_tokens_test.go index 0304584c..889a5798 100644 --- a/decoder/semantic_tokens_test.go +++ b/decoder/semantic_tokens_test.go @@ -507,3 +507,350 @@ resource "aws_instance" "beta" { t.Fatalf("unexpected tokens: %s", diff) } } + +func TestDecoder_SemanticTokensInFile_typeDeclaration(t *testing.T) { + d := NewDecoder() + d.SetSchema(&schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "variable": { + Labels: []*schema.LabelSchema{ + {Name: "name", IsDepKey: true}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "type": { + Expr: schema.ExprConstraints{schema.TypeDeclarationExpr{}}, + }, + }, + }, + }, + }, + }) + + testCfg := []byte(`variable "meh" { + type = string +} +variable "bah" { + type = map(string) +} +variable "bah" { + type = object({ + model = string + latest = bool + }) +} +`) + + 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) + } + + tokens, err := d.SemanticTokensInFile("test.tf") + if err != nil { + t.Fatal(err) + } + + expectedTokens := []lang.SemanticToken{ + { // module + Type: lang.TokenBlockType, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 9, + Byte: 8, + }, + }, + }, + { + Type: lang.TokenBlockLabel, + Modifiers: []lang.SemanticTokenModifier{lang.TokenModifierDependent}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 10, + Byte: 9, + }, + End: hcl.Pos{ + Line: 1, + Column: 15, + Byte: 14, + }, + }, + }, + { + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 2, + Byte: 18, + }, + End: hcl.Pos{ + Line: 2, + Column: 6, + Byte: 22, + }, + }, + }, + { + Type: lang.TokenTypePrimitive, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 9, + Byte: 25, + }, + End: hcl.Pos{ + Line: 2, + Column: 15, + Byte: 31, + }, + }, + }, + { + Type: lang.TokenBlockType, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 1, + Byte: 35, + }, + End: hcl.Pos{ + Line: 4, + Column: 9, + Byte: 43, + }, + }, + }, + { + Type: lang.TokenBlockLabel, + Modifiers: []lang.SemanticTokenModifier{lang.TokenModifierDependent}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 10, + Byte: 44, + }, + End: hcl.Pos{ + Line: 4, + Column: 15, + Byte: 49, + }, + }, + }, + { + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 5, + Column: 2, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 6, + Byte: 57, + }, + }, + }, + { + Type: lang.TokenTypeCapsule, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 5, + Column: 9, + Byte: 60, + }, + End: hcl.Pos{ + Line: 5, + Column: 12, + Byte: 63, + }, + }, + }, + { + Type: lang.TokenTypePrimitive, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 5, + Column: 13, + Byte: 64, + }, + End: hcl.Pos{ + Line: 5, + Column: 19, + Byte: 70, + }, + }, + }, + + //// + { + Type: lang.TokenBlockType, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 7, + Column: 1, + Byte: 75, + }, + End: hcl.Pos{ + Line: 7, + Column: 9, + Byte: 83, + }, + }, + }, + { + Type: lang.TokenBlockLabel, + Modifiers: []lang.SemanticTokenModifier{lang.TokenModifierDependent}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 7, + Column: 10, + Byte: 84, + }, + End: hcl.Pos{ + Line: 7, + Column: 15, + Byte: 89, + }, + }, + }, + { + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 8, + Column: 2, + Byte: 93, + }, + End: hcl.Pos{ + Line: 8, + Column: 6, + Byte: 97, + }, + }, + }, + { + Type: lang.TokenTypeCapsule, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 8, + Column: 9, + Byte: 100, + }, + End: hcl.Pos{ + Line: 8, + Column: 15, + Byte: 106, + }, + }, + }, + { + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 9, + Column: 3, + Byte: 111, + }, + End: hcl.Pos{ + Line: 9, + Column: 8, + Byte: 116, + }, + }, + }, + { + Type: lang.TokenTypePrimitive, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 9, + Column: 11, + Byte: 119, + }, + End: hcl.Pos{ + Line: 9, + Column: 17, + Byte: 125, + }, + }, + }, + { + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 10, + Column: 3, + Byte: 128, + }, + End: hcl.Pos{ + Line: 10, + Column: 9, + Byte: 134, + }, + }, + }, + { + Type: lang.TokenTypePrimitive, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 10, + Column: 12, + Byte: 137, + }, + End: hcl.Pos{ + Line: 10, + Column: 16, + Byte: 141, + }, + }, + }, + } + + diff := cmp.Diff(expectedTokens, tokens) + if diff != "" { + t.Fatalf("unexpected tokens: %s", diff) + } +} diff --git a/lang/semantic_token.go b/lang/semantic_token.go index b5543af6..643c118e 100644 --- a/lang/semantic_token.go +++ b/lang/semantic_token.go @@ -31,6 +31,8 @@ const ( TokenMapKey TokenKeyword TokenTraversalStep + TokenTypeCapsule + TokenTypePrimitive ) func (t SemanticTokenType) GoString() string { diff --git a/lang/semantic_token_type_string.go b/lang/semantic_token_type_string.go index bd3a5f23..1c9ddf70 100644 --- a/lang/semantic_token_type_string.go +++ b/lang/semantic_token_type_string.go @@ -19,11 +19,13 @@ func _() { _ = x[TokenMapKey-8] _ = x[TokenKeyword-9] _ = x[TokenTraversalStep-10] + _ = x[TokenTypeCapsule-11] + _ = x[TokenTypePrimitive-12] } -const _SemanticTokenType_name = "TokenNilTokenAttrNameTokenBlockTypeTokenBlockLabelTokenBoolTokenStringTokenNumberTokenObjectKeyTokenMapKeyTokenKeywordTokenTraversalStep" +const _SemanticTokenType_name = "TokenNilTokenAttrNameTokenBlockTypeTokenBlockLabelTokenBoolTokenStringTokenNumberTokenObjectKeyTokenMapKeyTokenKeywordTokenTraversalStepTokenTypeCapsuleTokenTypePrimitive" -var _SemanticTokenType_index = [...]uint8{0, 8, 21, 35, 50, 59, 70, 81, 95, 106, 118, 136} +var _SemanticTokenType_index = [...]uint8{0, 8, 21, 35, 50, 59, 70, 81, 95, 106, 118, 136, 152, 170} func (i SemanticTokenType) String() string { if i >= SemanticTokenType(len(_SemanticTokenType_index)-1) { diff --git a/schema/expressions.go b/schema/expressions.go index 0a2884ae..aea25739 100644 --- a/schema/expressions.go +++ b/schema/expressions.go @@ -384,3 +384,17 @@ func LiteralTypeOnly(t cty.Type) ExprConstraints { LiteralTypeExpr{Type: t}, } } + +type TypeDeclarationExpr struct{} + +func (TypeDeclarationExpr) isExprConstraintImpl() exprConstrSigil { + return exprConstrSigil{} +} + +func (td TypeDeclarationExpr) FriendlyName() string { + return "type" +} + +func (td TypeDeclarationExpr) Copy() ExprConstraint { + return TypeDeclarationExpr{} +}