Skip to content

Commit

Permalink
Support for nested expressions - 1st stage (#177)
Browse files Browse the repository at this point in the history
* schema: Introduce Constraint

* schema: (re)create & document existing constraints

This includes all existing constraints except for LiteralValue & the top level meta-constraint ExprConstraints which will be handled in separate commits.

* schema: LiteralValue -> LegacyLiteralValue & add new LiteralValue

Due to interface incompatibility in case of Copy() return value and the same type name, this is a necessary middle step towards phasing out the "old" constraint representation in favour of the new one.

* schema: Add Constraint to AttributeSchema

* schema: Add OneOf constraint

* schema: Update tests to use Constraint instead of Expr

* schema: Bootstrap EmptyCompletionData for constraints

* schema: Implement Comformable for Constraint

* decoder: Remove unused PathDecoder.referenceOriginAtPos()

* decoder: Plumb schema.Constraint through & introduce Expression

* decoder: bootstrap Expression implementations

* decoder: Rewrite existing expression completion tests

Previously the tests were testing internal API, this change makes the tests less brittle and easier to reuse when implementation changes.

* decoder: Remove unused traversalAtPos

* decoder: introduce context to Expression.Reference* methods

* decoder+schema: retain existing tests as legacy for now

This is to make it easier for introducing new tests later, which are expected to be mostly copies of the existing tests, with Expr -> Constraint appropriate changes.

* schema: Change Conformable -> Comparable & fix condition
  • Loading branch information
radeksimko authored Jan 10, 2023
1 parent 3af6c1a commit 1b2203e
Show file tree
Hide file tree
Showing 45 changed files with 1,882 additions and 355 deletions.
23 changes: 20 additions & 3 deletions decoder/attribute_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ import (
)

func attributeSchemaToCandidate(name string, attr *schema.AttributeSchema, rng hcl.Range) lang.Candidate {
var snippet string
var triggerSuggest bool
if attr.Constraint != nil {
cData := attr.Constraint.EmptyCompletionData(1)
snippet = fmt.Sprintf("%s = %s", name, cData.Snippet)
triggerSuggest = cData.TriggerSuggest
} else {
snippet = snippetForAttribute(name, attr)
triggerSuggest = triggerSuggestForExprConstraints(attr.Expr)
}

return lang.Candidate{
Label: name,
Detail: detailForAttribute(attr),
Expand All @@ -20,10 +31,10 @@ func attributeSchemaToCandidate(name string, attr *schema.AttributeSchema, rng h
Kind: lang.AttributeCandidateKind,
TextEdit: lang.TextEdit{
NewText: name,
Snippet: snippetForAttribute(name, attr),
Snippet: snippet,
Range: rng,
},
TriggerSuggest: triggerSuggestForExprConstraints(attr.Expr),
TriggerSuggest: triggerSuggest,
}
}

Expand All @@ -40,7 +51,13 @@ func detailForAttribute(attr *schema.AttributeSchema) string {
details = append(details, "sensitive")
}

friendlyName := attr.Expr.FriendlyName()
var friendlyName string
if attr.Constraint != nil {
friendlyName = attr.Constraint.FriendlyName()
} else {
friendlyName = attr.Expr.FriendlyName()
}

if friendlyName != "" {
details = append(details, friendlyName)
}
Expand Down
287 changes: 287 additions & 0 deletions decoder/attribute_candidates_legacy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package decoder

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"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 TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) {
testCases := []struct {
testName string
attrName string
attrSchema *schema.AttributeSchema
expectedCandidates lang.Candidates
}{
{
"primitive type",
"primitive",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.String),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "primitive",
Detail: "string",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "primitive",
Snippet: `primitive = "${1:value}"`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"map of strings",
"mymap",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.Map(cty.String)),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "mymap",
Detail: "map of string",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "mymap",
Snippet: `mymap = {
"${1:key}" = "${2:value}"
}`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"map of numbers",
"mymap",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.Map(cty.Number)),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "mymap",
Detail: "map of number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "mymap",
Snippet: `mymap = {
"${1:key}" = ${2:1}
}`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"list of numbers",
"mylist",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.List(cty.Number)),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "mylist",
Detail: "list of number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "mylist",
Snippet: `mylist = [ ${1:1} ]`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"list of objects",
"mylistobj",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.List(cty.Object(map[string]cty.Type{
"first": cty.String,
"second": cty.Number,
}))),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "mylistobj",
Detail: "list of object",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "mylistobj",
Snippet: `mylistobj = [ {
first = "${1:value}"
second = ${2:1}
} ]`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"set of numbers",
"myset",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.Set(cty.Number)),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "myset",
Detail: "set of number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "myset",
Snippet: `myset = [ ${1:1} ]`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"object",
"myobj",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{
"keystr": cty.String,
"keynum": cty.Number,
"keybool": cty.Bool,
})),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "myobj",
Detail: "object",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "myobj",
Snippet: `myobj = {
keybool = ${1:false}
keynum = ${2:1}
keystr = "${3:value}"
}`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"unknown type",
"mynil",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.DynamicPseudoType),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "mynil",
Detail: "any type",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "mynil",
Snippet: `mynil = ${1}`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"nested object",
"myobj",
&schema.AttributeSchema{
Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{
"keystr": cty.String,
"another": cty.Object(map[string]cty.Type{
"nestedstr": cty.String,
"nested_number": cty.Number,
}),
})),
},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "myobj",
Detail: "object",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
NewText: "myobj",
Snippet: `myobj = {
another = {
nested_number = ${1:1}
nestedstr = "${2:value}"
}
keystr = "${3:value}"
}`,
},
Kind: lang.AttributeCandidateKind,
},
}),
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) {
f, _ := hclsyntax.ParseConfig([]byte("\n"), "test.tf", hcl.InitialPos)
d := testPathDecoder(t, &PathContext{
Schema: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
tc.attrName: tc.attrSchema,
},
},
Files: map[string]*hcl.File{
"test.tf": f,
},
})

ctx := context.Background()
candidates, err := d.CandidatesAtPos(ctx, "test.tf", hcl.InitialPos)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" {
t.Fatalf("unexpected candidates: %s", diff)
}
})
}
}
Loading

0 comments on commit 1b2203e

Please sign in to comment.