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:
Martin Atkins 2017-06-11 08:39:40 -07:00
parent e571ec5810
commit 09f9e6c8e8
4 changed files with 117 additions and 25 deletions

View File

@ -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

View File

@ -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

View File

@ -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))
}
}
})
}

View File

@ -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
}