zclsyntax: deal with template unwrapping at eval time
Previously we were detecting the exactly-one-part case at parse time and skipping the TemplateExpr node in the AST, but this was problematic because we then misaligned the source range for the expression to the _content_ of the quotes, rather than including the quotes themselves. As well as producing confusing diagnostics, this also caused problems for zclwrite since it relies on source ranges to map our AST back onto the source tokens it came from.
This commit is contained in:
parent
e571ec5810
commit
09f9e6c8e8
@ -10,7 +10,8 @@ import (
|
||||
)
|
||||
|
||||
type TemplateExpr struct {
|
||||
Parts []Expression
|
||||
Parts []Expression
|
||||
Unwrap bool
|
||||
|
||||
SrcRange zcl.Range
|
||||
}
|
||||
@ -22,6 +23,14 @@ func (e *TemplateExpr) walkChildNodes(w internalWalkFunc) {
|
||||
}
|
||||
|
||||
func (e *TemplateExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
||||
if e.Unwrap {
|
||||
if len(e.Parts) != 1 {
|
||||
// should never happen - parser bug, if so
|
||||
panic("Unwrap set with len(e.Parts) != 1")
|
||||
}
|
||||
return e.Parts[0].Value(ctx)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
var diags zcl.Diagnostics
|
||||
isKnown := true
|
||||
|
@ -698,7 +698,16 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
|
||||
case TokenOQuote, TokenOHeredoc:
|
||||
open := p.Read() // eat opening marker
|
||||
closer := p.oppositeBracket(open.Type)
|
||||
return p.ParseTemplate(closer)
|
||||
parts, unwrap, diags := p.parseTemplateParts(closer)
|
||||
|
||||
closeRange := p.PrevRange()
|
||||
|
||||
return &TemplateExpr{
|
||||
Parts: parts,
|
||||
Unwrap: unwrap,
|
||||
|
||||
SrcRange: zcl.RangeBetween(open.Range, closeRange),
|
||||
}, diags
|
||||
|
||||
case TokenMinus:
|
||||
tok := p.Read() // eat minus token
|
||||
@ -1028,7 +1037,27 @@ func (p *parser) parseObjectCons() (Expression, zcl.Diagnostics) {
|
||||
}, diags
|
||||
}
|
||||
|
||||
func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) {
|
||||
func (p *parser) ParseTemplate() (Expression, zcl.Diagnostics) {
|
||||
startRange := p.NextRange()
|
||||
parts, unwrap, diags := p.parseTemplateParts(TokenEOF)
|
||||
endRange := p.PrevRange()
|
||||
|
||||
return &TemplateExpr{
|
||||
Parts: parts,
|
||||
Unwrap: unwrap,
|
||||
|
||||
SrcRange: zcl.RangeBetween(startRange, endRange),
|
||||
}, diags
|
||||
}
|
||||
|
||||
// parseTemplateParts parses the expressions that make up the content of a
|
||||
// template, up to the given closing delimiter. It also returns a flag that
|
||||
// is true if the first part should be returned as-is, or false if the
|
||||
// full set of parts should be wrapped in a TemplateExpr to return.
|
||||
//
|
||||
// The wrapping is done separately by the caller so that any template
|
||||
// delimiters can be included in the template's source range.
|
||||
func (p *parser) parseTemplateParts(end TokenType) ([]Expression, bool, zcl.Diagnostics) {
|
||||
var parts []Expression
|
||||
var diags zcl.Diagnostics
|
||||
|
||||
@ -1129,29 +1158,19 @@ Token:
|
||||
// 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,
|
||||
return []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.StringVal(""),
|
||||
SrcRange: zcl.Range{
|
||||
Filename: startRange.Filename,
|
||||
Start: startRange.Start,
|
||||
End: startRange.Start,
|
||||
},
|
||||
},
|
||||
}, diags
|
||||
}, true, 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
|
||||
return parts, len(parts) == 1, diags
|
||||
}
|
||||
|
||||
// parseQuotedStringLiteral is a helper for parsing quoted strings that
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-zcl/zcl"
|
||||
@ -406,6 +407,65 @@ block "valid" {}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a = \"hello ${true}\"\n",
|
||||
0,
|
||||
&Body{
|
||||
Attributes: Attributes{
|
||||
"a": {
|
||||
Name: "a",
|
||||
Expr: &TemplateExpr{
|
||||
Parts: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.StringVal("hello "),
|
||||
|
||||
SrcRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 6, Byte: 5},
|
||||
End: zcl.Pos{Line: 1, Column: 12, Byte: 11},
|
||||
},
|
||||
},
|
||||
&LiteralValueExpr{
|
||||
Val: cty.True,
|
||||
|
||||
SrcRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 14, Byte: 13},
|
||||
End: zcl.Pos{Line: 1, Column: 18, Byte: 17},
|
||||
},
|
||||
},
|
||||
},
|
||||
Unwrap: false,
|
||||
|
||||
SrcRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 5, Byte: 4},
|
||||
End: zcl.Pos{Line: 1, Column: 20, Byte: 19},
|
||||
},
|
||||
},
|
||||
|
||||
SrcRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
|
||||
End: zcl.Pos{Line: 1, Column: 20, Byte: 19},
|
||||
},
|
||||
NameRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
|
||||
End: zcl.Pos{Line: 1, Column: 2, Byte: 1},
|
||||
},
|
||||
EqualsRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 3, Byte: 2},
|
||||
End: zcl.Pos{Line: 1, Column: 4, Byte: 3},
|
||||
},
|
||||
},
|
||||
},
|
||||
Blocks: Blocks{},
|
||||
SrcRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
|
||||
End: zcl.Pos{Line: 2, Column: 1, Byte: 20},
|
||||
},
|
||||
EndRange: zcl.Range{
|
||||
Start: zcl.Pos{Line: 2, Column: 1, Byte: 20},
|
||||
End: zcl.Pos{Line: 2, Column: 1, Byte: 20},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a = foo.bar\n",
|
||||
0,
|
||||
@ -560,7 +620,11 @@ block "valid" {}
|
||||
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
diff := prettyConfig.Compare(test.want, got)
|
||||
t.Errorf("wrong result\ninput: %s\ndiff: %s", test.input, diff)
|
||||
if diff != "" {
|
||||
t.Errorf("wrong result\ninput: %s\ndiff: %s", test.input, diff)
|
||||
} else {
|
||||
t.Errorf("wrong result\ninput: %s\ngot: %s\nwant: %s", test.input, spew.Sdump(got), spew.Sdump(test.want))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ func ParseTemplate(src []byte, filename string, start zcl.Pos) (Expression, zcl.
|
||||
tokens, diags := LexTemplate(src, filename, start)
|
||||
peeker := newPeeker(tokens, false)
|
||||
parser := &parser{peeker: peeker}
|
||||
expr, parseDiags := parser.ParseTemplate(TokenEOF)
|
||||
expr, parseDiags := parser.ParseTemplate()
|
||||
diags = append(diags, parseDiags...)
|
||||
return expr, diags
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user