Skip to content

Commit

Permalink
Add support for ObjectExpr expression type
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Mar 11, 2021
1 parent 8aac804 commit 61ccc17
Show file tree
Hide file tree
Showing 12 changed files with 762 additions and 60 deletions.
14 changes: 8 additions & 6 deletions decoder/attribute_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ func detailForAttribute(attr *schema.AttributeSchema) string {
var detail string
if attr.IsRequired {
detail = "Required"
} else {
} else if attr.IsOptional {
detail = "Optional"
}

ec := ExprConstraints(attr.Expr)
names := ec.FriendlyNames()

if len(names) > 0 {
detail += fmt.Sprintf(", %s", strings.Join(names, " or "))
friendlyName := attr.Expr.FriendlyName()
if friendlyName != "" {
if detail != "" {
detail = strings.Join([]string{detail, friendlyName}, ", ")
} else {
detail = friendlyName
}
}

return detail
Expand Down
2 changes: 1 addition & 1 deletion decoder/body_decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestDecoder_CandidateAtPos_incompleteAttributes(t *testing.T) {
List: []lang.Candidate{
{
Label: "attr1",
Detail: "Optional, number",
Detail: "number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Expand Down
11 changes: 6 additions & 5 deletions decoder/candidates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,8 @@ func TestDecoder_CandidatesAtPos_basic(t *testing.T) {
}): {
Attributes: map[string]*schema.AttributeSchema{
"one": {Expr: schema.LiteralTypeOnly(cty.String), IsRequired: true},
"two": {Expr: schema.LiteralTypeOnly(cty.Number)},
"three": {Expr: schema.LiteralTypeOnly(cty.Bool)},
"two": {Expr: schema.LiteralTypeOnly(cty.Number), IsOptional: true},
"three": {Expr: schema.LiteralTypeOnly(cty.Bool), IsOptional: true},
},
},
schema.NewSchemaKey(schema.DependencyKeys{
Expand Down Expand Up @@ -944,7 +944,7 @@ func TestDecoder_CandidatesAtPos_AnyAttribute(t *testing.T) {
expectedCandidates := lang.CompleteCandidates([]lang.Candidate{
{
Label: "name",
Detail: "Optional, object",
Detail: "object",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Expand Down Expand Up @@ -979,6 +979,7 @@ func TestDecoder_CandidatesAtPos_multipleTypes(t *testing.T) {
schema.LiteralTypeExpr{Type: cty.Set(cty.DynamicPseudoType)},
schema.LiteralTypeExpr{Type: cty.Map(cty.DynamicPseudoType)},
},
IsOptional: true,
},
},
},
Expand Down Expand Up @@ -1045,7 +1046,7 @@ func TestDecoder_CandidatesAtPos_incompleteAttrOrBlock(t *testing.T) {
Labels: resourceLabelSchema,
Body: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"count": {Expr: schema.LiteralTypeOnly(cty.Number)},
"count": {Expr: schema.LiteralTypeOnly(cty.Number), IsOptional: true},
},
},
}
Expand Down Expand Up @@ -1173,7 +1174,7 @@ func TestDecoder_CandidatesAtPos_incompleteLabel(t *testing.T) {
Labels: resourceLabelSchema,
Body: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"count": {Expr: schema.LiteralTypeOnly(cty.Number)},
"count": {Expr: schema.LiteralTypeOnly(cty.Number), IsOptional: true},
},
},
DependentBody: map[schema.SchemaKey]*schema.BodySchema{
Expand Down
80 changes: 74 additions & 6 deletions decoder/expression_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ func constraintsAtPos(expr hcl.Expression, constraints ExprConstraints, pos hcl.
Filename: eType.Range().Filename,
}
}
case *hclsyntax.ObjectConsExpr:
oe, ok := constraints.ObjectExpr()
if ok {
undeclaredAttributes := oe.Attributes
for _, item := range eType.Items {
key, _ := item.KeyExpr.Value(nil)
if !key.IsWhollyKnown() || key.Type() != cty.String {
continue
}
attr, ok := oe.Attributes[key.AsString()]
if !ok {
// unknown attribute
continue
}
delete(undeclaredAttributes, key.AsString())

itemRng := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range())
if item.ValueExpr.Range().ContainsPos(pos) {
return constraintsAtPos(item.ValueExpr, ExprConstraints(attr.Expr), pos)
} else if itemRng.ContainsPos(pos) {
// middle of attribute name or equal sign
return ExprConstraints{}, expr.Range()
}
}

return ExprConstraints{undeclaredAttributes}, hcl.Range{
Start: pos,
End: pos,
Filename: eType.Range().Filename,
}
}
}

return ExprConstraints{}, expr.Range()
Expand Down Expand Up @@ -96,9 +127,10 @@ func constraintToCandidates(constraint schema.ExprConstraint, editRng hcl.Range)
})
case schema.TupleConsExpr:
candidates = append(candidates, lang.Candidate{
Label: fmt.Sprintf(`[%s]`, labelForConstraints(c.AnyElem)),
Detail: c.Name,
Kind: lang.TupleCandidateKind,
Label: fmt.Sprintf(`[%s]`, labelForConstraints(c.AnyElem)),
Detail: c.Name,
Description: c.Description,
Kind: lang.TupleCandidateKind,
TextEdit: lang.TextEdit{
NewText: `[ ]`,
Snippet: `[ ${0} ]`,
Expand All @@ -108,9 +140,10 @@ func constraintToCandidates(constraint schema.ExprConstraint, editRng hcl.Range)
})
case schema.MapExpr:
candidates = append(candidates, lang.Candidate{
Label: fmt.Sprintf(`{ key =%s}`, labelForConstraints(c.Elem)),
Detail: c.FriendlyName(),
Kind: lang.MapCandidateKind,
Label: fmt.Sprintf(`{ key =%s}`, labelForConstraints(c.Elem)),
Detail: c.FriendlyName(),
Description: c.Description,
Kind: lang.MapCandidateKind,
TextEdit: lang.TextEdit{
NewText: fmt.Sprintf("{\n name = %s\n}",
newTextForConstraints(c.Elem, true)),
Expand All @@ -120,6 +153,35 @@ func constraintToCandidates(constraint schema.ExprConstraint, editRng hcl.Range)
},
TriggerSuggest: len(c.Elem) > 0,
})
case schema.ObjectExpr:
candidates = append(candidates, lang.Candidate{
Label: `{ }`,
Detail: c.FriendlyName(),
Description: c.Description,
Kind: lang.ObjectCandidateKind,
TextEdit: lang.TextEdit{
NewText: "{\n \n}",
Snippet: "{\n ${1}\n}",
Range: editRng,
},
TriggerSuggest: len(c.Attributes) > 0,
})
case schema.ObjectExprAttributes:
attrNames := sortedObjectExprAttrNames(c)
for _, name := range attrNames {
attr := c[name]
candidates = append(candidates, lang.Candidate{
Label: name,
Detail: attr.Expr.FriendlyName(),
Description: attr.Description,
Kind: lang.AttributeCandidateKind,
TextEdit: lang.TextEdit{
NewText: fmt.Sprintf("%s = %s", name, newTextForConstraints(attr.Expr, true)),
Snippet: fmt.Sprintf("%s = %s", name, snippetForConstraints(1, attr.Expr, true)),
Range: editRng,
},
})
}
}

return candidates
Expand All @@ -141,6 +203,8 @@ func newTextForConstraints(cons schema.ExprConstraints, isNested bool) string {
return fmt.Sprintf("[\n %s\n]", newTextForConstraints(c.AnyElem, true))
case schema.MapExpr:
return fmt.Sprintf("{\n %s\n}", newTextForConstraints(c.Elem, true))
case schema.ObjectExpr:
return "{\n \n}"
}
}
return ""
Expand All @@ -162,6 +226,8 @@ func snippetForConstraints(placeholder uint, cons schema.ExprConstraints, isNest
return fmt.Sprintf("[\n %s\n]", snippetForConstraints(placeholder+1, c.AnyElem, true))
case schema.MapExpr:
return fmt.Sprintf("{\n %s\n}", snippetForConstraints(placeholder+1, c.Elem, true))
case schema.ObjectExpr:
return fmt.Sprintf("{\n ${%d}\n}", placeholder+1)
}
}
return ""
Expand Down Expand Up @@ -319,6 +385,8 @@ func snippetForExprContraints(placeholder uint, ec schema.ExprConstraints) strin
return fmt.Sprintf("{\n ${%d:name} = %s\n }",
placeholder,
snippetForExprContraints(placeholder+1, et.Elem))
case schema.ObjectExpr:
return fmt.Sprintf("{\n ${%d}\n }", placeholder+1)
}
return ""
}
Expand Down
166 changes: 166 additions & 0 deletions decoder/expression_candidates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,172 @@ func TestDecoder_CandidateAtPos_expressions(t *testing.T) {
},
}),
},
{
"object as expression",
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 =
`,
hcl.Pos{Line: 1, Column: 8, Byte: 7},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "{ }",
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: "{\n \n}",
Snippet: "{\n ${1}\n}",
},
Kind: lang.ObjectCandidateKind,
TriggerSuggest: true,
},
}),
},
{
"object as expression - attribute",
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 = {
}
`,
hcl.Pos{Line: 2, Column: 3, Byte: 11},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "first",
Detail: "string",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{
Line: 2,
Column: 3,
Byte: 11,
},
End: hcl.Pos{
Line: 2,
Column: 3,
Byte: 11,
},
},
NewText: `first = ""`,
Snippet: `first = "${1:value}"`,
},
Kind: lang.AttributeCandidateKind,
},
{
Label: "second",
Detail: "number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{
Line: 2,
Column: 3,
Byte: 11,
},
End: hcl.Pos{
Line: 2,
Column: 3,
Byte: 11,
},
},
NewText: "second = 1",
Snippet: "second = ${1:1}",
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"object as expression - attributes partially declared",
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"
}
`,
hcl.Pos{Line: 3, Column: 3, Byte: 28},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "second",
Detail: "number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{
Line: 3,
Column: 3,
Byte: 28,
},
End: hcl.Pos{
Line: 3,
Column: 3,
Byte: 28,
},
},
NewText: "second = 1",
Snippet: "second = ${1:1}",
},
Kind: lang.AttributeCandidateKind,
},
}),
},
{
"list as value",
map[string]*schema.AttributeSchema{
Expand Down
Loading

0 comments on commit 61ccc17

Please sign in to comment.