hcl/hclsyntax: Parsing of the "full splat" operator

The evaluation of this was there but the parsing was still a TODO comment
from early development. Whoops!

Fortunately the existing parser functionality makes this straightforward
since we can just have the traversal parser recursively call itself.

This fixes #63.
This commit is contained in:
Martin Atkins 2018-12-12 17:41:28 -08:00
parent ebe27107e1
commit b056768a1c
2 changed files with 107 additions and 30 deletions

View File

@ -781,6 +781,14 @@ upper(
}), }),
0, 0,
}, },
{
`{name: "Steve"}[*].name`,
nil,
cty.TupleVal([]cty.Value{
cty.StringVal("Steve"),
}),
0,
},
{ {
`set.*.name`, `set.*.name`,
&hcl.EvalContext{ &hcl.EvalContext{
@ -915,6 +923,31 @@ upper(
cty.StringVal("Steve"), cty.StringVal("Steve"),
0, 0,
}, },
{
// For a "full" splat, an index operator is consumed as part
// of the splat's traversal.
`[{names: ["Steve"]}, {names: ["Ermintrude"]}][*].names[0]`,
nil,
cty.TupleVal([]cty.Value{cty.StringVal("Steve"), cty.StringVal("Ermintrude")}),
0,
},
{
// Another "full" splat, this time with the index first.
`[[{name: "Steve"}], [{name: "Ermintrude"}]][*][0].name`,
nil,
cty.TupleVal([]cty.Value{cty.StringVal("Steve"), cty.StringVal("Ermintrude")}),
0,
},
{
// Full splats can nest, which produces nested tuples.
`[[{name: "Steve"}], [{name: "Ermintrude"}]][*][*].name`,
nil,
cty.TupleVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("Steve")}),
cty.TupleVal([]cty.Value{cty.StringVal("Ermintrude")}),
}),
0,
},
{ {
`[["hello"], ["goodbye"]].*.*`, `[["hello"], ["goodbye"]].*.*`,
nil, nil,

View File

@ -462,7 +462,14 @@ func (p *parser) parseBinaryOps(ops []map[TokenType]*Operation) (Expression, hcl
func (p *parser) parseExpressionWithTraversals() (Expression, hcl.Diagnostics) { func (p *parser) parseExpressionWithTraversals() (Expression, hcl.Diagnostics) {
term, diags := p.parseExpressionTerm() term, diags := p.parseExpressionTerm()
ret := term ret, moreDiags := p.parseExpressionTraversals(term)
diags = append(diags, moreDiags...)
return ret, diags
}
func (p *parser) parseExpressionTraversals(from Expression) (Expression, hcl.Diagnostics) {
var diags hcl.Diagnostics
ret := from
Traversal: Traversal:
for { for {
@ -660,44 +667,81 @@ Traversal:
// the key value is something constant. // the key value is something constant.
open := p.Read() open := p.Read()
// TODO: If we have a TokenStar inside our brackets, parse as switch p.Peek().Type {
// a Splat expression: foo[*].baz[0]. case TokenStar:
var close Token // This is a full splat expression, like foo[*], which consumes
p.PushIncludeNewlines(false) // arbitrary newlines allowed in brackets // the rest of the traversal steps after it using a recursive
keyExpr, keyDiags := p.ParseExpression() // call to this function.
diags = append(diags, keyDiags...) p.Read() // consume star
if p.recovery && keyDiags.HasErrors() { close := p.Read()
close = p.recover(TokenCBrack)
} else {
close = p.Read()
if close.Type != TokenCBrack && !p.recovery { if close.Type != TokenCBrack && !p.recovery {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Missing close bracket on index", Summary: "Missing close bracket on splat index",
Detail: "The index operator must end with a closing bracket (\"]\").", Detail: "The star for a full splat operator must be immediately followed by a closing bracket (\"]\").",
Subject: &close.Range, Subject: &close.Range,
}) })
close = p.recover(TokenCBrack) close = p.recover(TokenCBrack)
} }
} // Splat expressions use a special "anonymous symbol" as a
p.PopIncludeNewlines() // placeholder in an expression to be evaluated once for each
// item in the source expression.
if lit, isLit := keyExpr.(*LiteralValueExpr); isLit { itemExpr := &AnonSymbolExpr{
litKey, _ := lit.Value(nil) SrcRange: hcl.RangeBetween(open.Range, close.Range),
rng := hcl.RangeBetween(open.Range, close.Range)
step := hcl.TraverseIndex{
Key: litKey,
SrcRange: rng,
} }
ret = makeRelativeTraversal(ret, step, rng) // Now we'll recursively call this same function to eat any
} else { // remaining traversal steps against the anonymous symbol.
rng := hcl.RangeBetween(open.Range, close.Range) travExpr, nestedDiags := p.parseExpressionTraversals(itemExpr)
ret = &IndexExpr{ diags = append(diags, nestedDiags...)
Collection: ret,
Key: keyExpr,
SrcRange: rng, ret = &SplatExpr{
OpenRange: open.Range, Source: ret,
Each: travExpr,
Item: itemExpr,
SrcRange: hcl.RangeBetween(open.Range, travExpr.Range()),
MarkerRange: hcl.RangeBetween(open.Range, close.Range),
}
default:
var close Token
p.PushIncludeNewlines(false) // arbitrary newlines allowed in brackets
keyExpr, keyDiags := p.ParseExpression()
diags = append(diags, keyDiags...)
if p.recovery && keyDiags.HasErrors() {
close = p.recover(TokenCBrack)
} else {
close = p.Read()
if close.Type != TokenCBrack && !p.recovery {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing close bracket on index",
Detail: "The index operator must end with a closing bracket (\"]\").",
Subject: &close.Range,
})
close = p.recover(TokenCBrack)
}
}
p.PopIncludeNewlines()
if lit, isLit := keyExpr.(*LiteralValueExpr); isLit {
litKey, _ := lit.Value(nil)
rng := hcl.RangeBetween(open.Range, close.Range)
step := hcl.TraverseIndex{
Key: litKey,
SrcRange: rng,
}
ret = makeRelativeTraversal(ret, step, rng)
} else {
rng := hcl.RangeBetween(open.Range, close.Range)
ret = &IndexExpr{
Collection: ret,
Key: keyExpr,
SrcRange: rng,
OpenRange: open.Range,
}
} }
} }