zclsyntax: basic parsing and evaluation of string templates

Control sequences are not yet supported, but interpolation sequences work.
This commit is contained in:
Martin Atkins 2017-06-01 08:01:12 -07:00
parent ab9bab3578
commit 8532fe32e6
4 changed files with 232 additions and 0 deletions

View File

@ -0,0 +1,86 @@
package zclsyntax
import (
"bytes"
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-zcl/zcl"
)
type TemplateExpr struct {
Parts []Expression
SrcRange zcl.Range
}
func (e *TemplateExpr) walkChildNodes(w internalWalkFunc) {
for i, part := range e.Parts {
e.Parts[i] = w(part).(Expression)
}
}
func (e *TemplateExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
buf := &bytes.Buffer{}
var diags zcl.Diagnostics
isKnown := true
for _, part := range e.Parts {
partVal, partDiags := part.Value(ctx)
diags = append(diags, partDiags...)
if partVal.IsNull() {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"The expression result is null. Cannot include a null value in a string template.",
),
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
})
continue
}
if !partVal.IsKnown() {
// If any part is unknown then the result as a whole must be
// unknown too. We'll keep on processing the rest of the parts
// anyway, because we want to still emit any diagnostics resulting
// from evaluating those.
isKnown = false
continue
}
strVal, err := convert.Convert(partVal, cty.String)
if err != nil {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"Cannot include the given value in a string template: %s.",
err.Error(),
),
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
})
continue
}
buf.WriteString(strVal.AsString())
}
if !isKnown {
return cty.UnknownVal(cty.String), diags
}
return cty.StringVal(buf.String()), diags
}
func (e *TemplateExpr) Range() zcl.Range {
return e.SrcRange
}
func (e *TemplateExpr) StartRange() zcl.Range {
return e.Parts[0].StartRange()
}

View File

@ -60,6 +60,60 @@ func TestExpressionParseAndValue(t *testing.T) {
cty.True,
1, // extra characters after expression
},
{
`"hello"`,
nil,
cty.StringVal("hello"),
0,
},
{
`"hello\nworld"`,
nil,
cty.StringVal("hello\nworld"),
0,
},
{
`"unclosed`,
nil,
cty.StringVal("unclosed"),
1, // Unterminated template string
},
{
`"hello ${"world"}"`,
nil,
cty.StringVal("hello world"),
0,
},
{
`"hello ${12.5}"`,
nil,
cty.StringVal("hello 12.5"),
0,
},
{
`"silly ${"${"nesting"}"}"`,
nil,
cty.StringVal("silly nesting"),
0,
},
{
`"silly ${"${true}"}"`,
nil,
cty.StringVal("silly true"),
0,
},
{
`"hello $${escaped}"`,
nil,
cty.StringVal("hello ${escaped}"),
0,
},
{
`"hello $$nonescape"`,
nil,
cty.StringVal("hello $$nonescape"),
0,
},
}
for _, test := range tests {

View File

@ -27,6 +27,10 @@ func (e *ScopeTraversalExpr) Variables() []zcl.Traversal {
return Variables(e)
}
func (e *TemplateExpr) Variables() []zcl.Traversal {
return Variables(e)
}
func (e *UnaryOpExpr) Variables() []zcl.Traversal {
return Variables(e)
}

View File

@ -485,6 +485,11 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
}, nil
}
case TokenOQuote, TokenOHeredoc:
open := p.Read() // eat opening marker
closer := p.oppositeBracket(open.Type)
return p.ParseTemplate(closer)
case TokenMinus:
tok := p.Read() // eat minus token
@ -537,6 +542,89 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
}
}
func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) {
var parts []Expression
var diags zcl.Diagnostics
startRange := p.NextRange()
Token:
for {
next := p.Read()
if next.Type == end {
// all done!
break
}
switch next.Type {
case TokenStringLit, TokenQuotedLit:
str, strDiags := p.decodeStringLit(next)
diags = append(diags, strDiags...)
parts = append(parts, &LiteralValueExpr{
Val: cty.StringVal(str),
SrcRange: next.Range,
})
case TokenTemplateInterp:
// TODO: if opener has ~ mark, eat trailing spaces in the previous
// literal.
expr, exprDiags := p.ParseExpression()
diags = append(diags, exprDiags...)
close := p.Peek()
if close.Type != TokenTemplateSeqEnd {
p.recover(TokenTemplateSeqEnd)
} else {
p.Read() // eat closing brace
// TODO: if closer has ~ mark, remember to eat leading spaces
// in the following literal.
}
parts = append(parts, expr)
case TokenTemplateControl:
panic("template control sequences not yet supported")
default:
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unterminated template string",
Detail: "No closing marker was found for the string.",
Subject: &next.Range,
Context: zcl.RangeBetween(startRange, next.Range).Ptr(),
})
}
p.recover(end)
break Token
}
}
if len(parts) == 0 {
// If a sequence has no content, we'll treat it as if it had an
// empty string in it because that's what the user probably means
// if they write "" in configuration.
return &LiteralValueExpr{
Val: cty.StringVal(""),
SrcRange: zcl.Range{
Filename: startRange.Filename,
Start: startRange.Start,
End: startRange.Start,
},
}, diags
}
if len(parts) == 1 {
// If a sequence only has one part then as a special case we return
// that part alone. This allows the use of single-part templates to
// represent general expressions in syntaxes such as JSON where
// un-quoted expressions are not possible.
return parts[0], diags
}
return &TemplateExpr{
Parts: parts,
SrcRange: zcl.RangeBetween(parts[0].Range(), parts[len(parts)-1].Range()),
}, diags
}
// parseQuotedStringLiteral is a helper for parsing quoted strings that
// aren't allowed to contain any interpolations, such as block labels.
func (p *parser) parseQuotedStringLiteral() (string, zcl.Range, zcl.Diagnostics) {