zclsyntax: basic parsing and evaluation of string templates
Control sequences are not yet supported, but interpolation sequences work.
This commit is contained in:
parent
ab9bab3578
commit
8532fe32e6
86
zcl/zclsyntax/expression_template.go
Normal file
86
zcl/zclsyntax/expression_template.go
Normal 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()
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user