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,
|
cty.True,
|
||||||
1, // extra characters after expression
|
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 {
|
for _, test := range tests {
|
||||||
|
@ -27,6 +27,10 @@ func (e *ScopeTraversalExpr) Variables() []zcl.Traversal {
|
|||||||
return Variables(e)
|
return Variables(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *TemplateExpr) Variables() []zcl.Traversal {
|
||||||
|
return Variables(e)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *UnaryOpExpr) Variables() []zcl.Traversal {
|
func (e *UnaryOpExpr) Variables() []zcl.Traversal {
|
||||||
return Variables(e)
|
return Variables(e)
|
||||||
}
|
}
|
||||||
|
@ -485,6 +485,11 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case TokenOQuote, TokenOHeredoc:
|
||||||
|
open := p.Read() // eat opening marker
|
||||||
|
closer := p.oppositeBracket(open.Type)
|
||||||
|
return p.ParseTemplate(closer)
|
||||||
|
|
||||||
case TokenMinus:
|
case TokenMinus:
|
||||||
tok := p.Read() // eat minus token
|
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
|
// parseQuotedStringLiteral is a helper for parsing quoted strings that
|
||||||
// aren't allowed to contain any interpolations, such as block labels.
|
// aren't allowed to contain any interpolations, such as block labels.
|
||||||
func (p *parser) parseQuotedStringLiteral() (string, zcl.Range, zcl.Diagnostics) {
|
func (p *parser) parseQuotedStringLiteral() (string, zcl.Range, zcl.Diagnostics) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user