Skip to content

Commit

Permalink
Add type declarations for variable type (#47)
Browse files Browse the repository at this point in the history
So that completion, hover and semantic-tokens work for variable `type`.

Related to hashicorp/terraform-ls#490
  • Loading branch information
beandrad authored Jun 9, 2021
1 parent f2d6b65 commit 247bfad
Show file tree
Hide file tree
Showing 10 changed files with 738 additions and 2 deletions.
39 changes: 39 additions & 0 deletions decoder/expression_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
160 changes: 160 additions & 0 deletions decoder/expression_candidates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions decoder/expression_constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 16 additions & 0 deletions decoder/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
100 changes: 100 additions & 0 deletions decoder/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,3 +765,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)
}
})
}
}
Loading

0 comments on commit 247bfad

Please sign in to comment.