Skip to content

Commit

Permalink
Add expression support for literals & keywords (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko authored Feb 25, 2021
1 parent 7e7aea1 commit 01f0b45
Show file tree
Hide file tree
Showing 32 changed files with 4,455 additions and 305 deletions.
65 changes: 6 additions & 59 deletions decoder/attribute_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,71 +34,18 @@ func detailForAttribute(attr *schema.AttributeSchema) string {
detail = "Optional"
}

if len(attr.ValueTypes) > 0 {
detail += fmt.Sprintf(", %s", strings.Join(attr.ValueTypes.FriendlyNames(), " or "))
} else {
detail += fmt.Sprintf(", %s", attr.ValueType.FriendlyName())
ec := ExprConstraints(attr.Expr)
names := ec.FriendlyNames()

if len(names) > 0 {
detail += fmt.Sprintf(", %s", strings.Join(names, " or "))
}

return detail
}

func snippetForAttribute(name string, attr *schema.AttributeSchema) string {
if len(attr.ValueTypes) > 0 {
return fmt.Sprintf("%s = %s", name, snippetForAttrValue(1, attr.ValueTypes[0]))
}
return fmt.Sprintf("%s = %s", name, snippetForAttrValue(1, attr.ValueType))
}

func snippetForAttrValue(placeholder uint, attrType cty.Type) string {
switch attrType {
case cty.String:
return fmt.Sprintf(`"${%d:value}"`, placeholder)
case cty.Bool:
return fmt.Sprintf(`${%d:false}`, placeholder)
case cty.Number:
return fmt.Sprintf(`${%d:1}`, placeholder)
case cty.DynamicPseudoType:
return fmt.Sprintf(`${%d}`, placeholder)
}

if attrType.IsMapType() {
return fmt.Sprintf("{\n"+` "${1:key}" = %s`+"\n}",
snippetForAttrValue(placeholder+1, *attrType.MapElementType()))
}

if attrType.IsListType() || attrType.IsSetType() {
elType := attrType.ElementType()
return fmt.Sprintf("[ %s ]", snippetForAttrValue(placeholder, elType))
}

if attrType.IsObjectType() {
objSnippet := ""
for _, name := range sortedObjectAttrNames(attrType) {
valType := attrType.AttributeType(name)

objSnippet += fmt.Sprintf(" %s = %s\n", name,
snippetForAttrValue(placeholder, valType))
placeholder++
}
return fmt.Sprintf("{\n%s}", objSnippet)
}

if attrType.IsTupleType() {
elTypes := attrType.TupleElementTypes()
if len(elTypes) == 1 {
return fmt.Sprintf("[ %s ]", snippetForAttrValue(placeholder, elTypes[0]))
}

tupleSnippet := ""
for _, elType := range elTypes {
placeholder++
tupleSnippet += snippetForAttrValue(placeholder, elType)
}
return fmt.Sprintf("[\n%s]", tupleSnippet)
}

return ""
return fmt.Sprintf("%s = %s", name, snippetForExprContraints(1, attr.Expr))
}

func sortedObjectAttrNames(obj cty.Type) []string {
Expand Down
61 changes: 30 additions & 31 deletions decoder/attribute_candidates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ func TestSnippetForAttribute(t *testing.T) {
"primitive type",
"primitive",
&schema.AttributeSchema{
ValueType: cty.String,
Expr: schema.LiteralTypeOnly(cty.String),
},
`primitive = "${1:value}"`,
},
{
"map of strings",
"mymap",
&schema.AttributeSchema{
ValueType: cty.Map(cty.String),
Expr: schema.LiteralTypeOnly(cty.Map(cty.String)),
},
`mymap = {
"${1:key}" = "${2:value}"
Expand All @@ -38,7 +38,7 @@ func TestSnippetForAttribute(t *testing.T) {
"map of numbers",
"mymap",
&schema.AttributeSchema{
ValueType: cty.Map(cty.Number),
Expr: schema.LiteralTypeOnly(cty.Map(cty.Number)),
},
`mymap = {
"${1:key}" = ${2:1}
Expand All @@ -48,18 +48,18 @@ func TestSnippetForAttribute(t *testing.T) {
"list of numbers",
"mylist",
&schema.AttributeSchema{
ValueType: cty.List(cty.Number),
Expr: schema.LiteralTypeOnly(cty.List(cty.Number)),
},
`mylist = [ ${1:1} ]`,
},
{
"list of objects",
"mylistobj",
&schema.AttributeSchema{
ValueType: cty.List(cty.Object(map[string]cty.Type{
Expr: schema.LiteralTypeOnly(cty.List(cty.Object(map[string]cty.Type{
"first": cty.String,
"second": cty.Number,
})),
}))),
},
`mylistobj = [ {
first = "${1:value}"
Expand All @@ -70,19 +70,19 @@ func TestSnippetForAttribute(t *testing.T) {
"set of numbers",
"myset",
&schema.AttributeSchema{
ValueType: cty.Set(cty.Number),
Expr: schema.LiteralTypeOnly(cty.Set(cty.Number)),
},
`myset = [ ${1:1} ]`,
},
{
"object",
"myobj",
&schema.AttributeSchema{
ValueType: cty.Object(map[string]cty.Type{
Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{
"keystr": cty.String,
"keynum": cty.Number,
"keybool": cty.Bool,
}),
})),
},
`myobj = {
keybool = ${1:false}
Expand All @@ -94,31 +94,30 @@ func TestSnippetForAttribute(t *testing.T) {
"unknown type",
"mynil",
&schema.AttributeSchema{
ValueType: cty.DynamicPseudoType,
Expr: schema.LiteralTypeOnly(cty.DynamicPseudoType),
},
`mynil = ${1}`,
},
// TODO: Indent nested objects correctly
// {
// "nested object",
// "myobj",
// &schema.AttributeSchema{
// ValueType: cty.Object(map[string]cty.Type{
// "keystr": cty.String,
// "another": cty.Object(map[string]cty.Type{
// "nestedstr": cty.String,
// "nested_number": cty.Number,
// }),
// }),
// },
// `myobj {
// another {
// nested_number = ${1:1}
// nestedstr = "${2:value}"
// }
// keystr = "${2:value}"
// }`,
// },
{
"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,
}),
})),
},
`myobj = {
another = {
nested_number = ${1:1}
nestedstr = "${2:value}"
}
keystr = "${3:value}"
}`,
},
}

for i, tc := range testCases {
Expand Down
16 changes: 8 additions & 8 deletions decoder/body_decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ func TestDecoder_CandidateAtPos_incompleteAttributes(t *testing.T) {
},
Body: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"attr1": {ValueType: cty.Number},
"attr2": {ValueType: cty.Number},
"some_other_attr": {ValueType: cty.Number},
"another_attr": {ValueType: cty.Number},
"attr1": {Expr: schema.LiteralTypeOnly(cty.Number)},
"attr2": {Expr: schema.LiteralTypeOnly(cty.Number)},
"some_other_attr": {Expr: schema.LiteralTypeOnly(cty.Number)},
"another_attr": {Expr: schema.LiteralTypeOnly(cty.Number)},
},
},
},
Expand Down Expand Up @@ -93,10 +93,10 @@ func TestDecoder_CandidateAtPos_computedAttributes(t *testing.T) {
},
Body: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"attr1": {ValueType: cty.Number, IsComputed: true},
"attr2": {ValueType: cty.Number, IsComputed: true, IsOptional: true},
"some_other_attr": {ValueType: cty.Number},
"another_attr": {ValueType: cty.Number},
"attr1": {Expr: schema.LiteralTypeOnly(cty.Number), IsComputed: true},
"attr2": {Expr: schema.LiteralTypeOnly(cty.Number), IsComputed: true, IsOptional: true},
"some_other_attr": {Expr: schema.LiteralTypeOnly(cty.Number)},
"another_attr": {Expr: schema.LiteralTypeOnly(cty.Number)},
},
},
},
Expand Down
36 changes: 11 additions & 25 deletions decoder/candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,24 @@ func (d *Decoder) candidatesAtPos(body *hclsyntax.Body, bodySchema *schema.BodyS
filename := body.Range().Filename

for _, attr := range body.Attributes {
if isRightHandSidePos(attr, pos) {
// TODO: RHS candidates (requires a form of expression schema)
return lang.ZeroCandidates(), &PositionalError{
Filename: filename,
Pos: pos,
Msg: fmt.Sprintf("%s: no candidates for attribute value", attr.Name),
if attr.Expr.Range().ContainsPos(pos) || attr.EqualsRange.End.Byte == pos.Byte {
if aSchema, ok := bodySchema.Attributes[attr.Name]; ok {
return d.attrValueCandidatesAtPos(attr, aSchema, pos)
}
if bodySchema.AnyAttribute != nil {
return d.attrValueCandidatesAtPos(attr, bodySchema.AnyAttribute, pos)
}

return lang.ZeroCandidates(), nil
}
if attr.NameRange.ContainsPos(pos) {
prefixRng := attr.NameRange
prefixRng.End = pos
return d.bodySchemaCandidates(body, bodySchema, prefixRng, attr.Range()), nil
}
if attr.EqualsRange.ContainsPos(pos) {
return lang.ZeroCandidates(), nil
}
}

rng := hcl.Range{
Expand Down Expand Up @@ -128,25 +133,6 @@ func (d *Decoder) candidatesAtPos(body *hclsyntax.Body, bodySchema *schema.BodyS
return d.bodySchemaCandidates(body, bodySchema, rng, rng), nil
}

func isRightHandSidePos(attr *hclsyntax.Attribute, pos hcl.Pos) bool {
// Here we assume 1 attribute per line
// which allows us to also catch position in trailing whitespace
// (which HCL parser doesn't consider attribute's range)
if attr.Range().End.Line != pos.Line {
// entirely different line
return false
}
if pos.Column < attr.Range().Start.Column {
// indentation
return false
}
if attr.NameRange.ContainsPos(pos) {
return false
}

return true
}

func (d *Decoder) nameTokenRangeAtPos(filename string, pos hcl.Pos) (hcl.Range, error) {
rng := hcl.Range{
Filename: filename,
Expand Down
Loading

0 comments on commit 01f0b45

Please sign in to comment.