zclsyntax: parsing of template if construct

Since this ultimately just returns a ConditionalExpr, the evaluation is
already implemented too.
This commit is contained in:
Martin Atkins 2017-06-17 10:07:59 -07:00
parent 2f1bfd284c
commit 15e3d80e6c
3 changed files with 261 additions and 4 deletions

View File

@ -128,6 +128,49 @@ trim`,
cty.StringVal("truetrimtrue"), // trimming is no-op of neighbors aren't literal strings
0,
},
{
`!{ if true ~} hello !{~ endif }`,
nil,
cty.StringVal("hello"),
0,
},
{
`!{ if false ~} hello !{~ endif}`,
nil,
cty.StringVal(""),
0,
},
{
`!{ if true ~} hello !{~ else ~} goodbye !{~ endif }`,
nil,
cty.StringVal("hello"),
0,
},
{
`!{ if false ~} hello !{~ else ~} goodbye !{~ endif }`,
nil,
cty.StringVal("goodbye"),
0,
},
{
`!{ if true ~} !{~ if false ~} hello !{~ else ~} goodbye !{~ endif ~} !{~ endif }`,
nil,
cty.StringVal("goodbye"),
0,
},
{
`!{ if false ~} !{~ if false ~} hello !{~ else ~} goodbye !{~ endif ~} !{~ endif }`,
nil,
cty.StringVal(""),
0,
},
{
`!{ of true ~} hello !{~ endif}`,
nil,
cty.UnknownVal(cty.String),
2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected
},
}
for _, test := range tests {

View File

@ -9,6 +9,9 @@ type Keyword []byte
var forKeyword = Keyword([]byte{'f', 'o', 'r'})
var inKeyword = Keyword([]byte{'i', 'n'})
var ifKeyword = Keyword([]byte{'i', 'f'})
var elseKeyword = Keyword([]byte{'e', 'l', 's', 'e'})
var endifKeyword = Keyword([]byte{'e', 'n', 'd', 'i', 'f'})
var endforKeyword = Keyword([]byte{'e', 'n', 'd', 'f', 'o', 'r'})
func (kw Keyword) TokenMatches(token Token) bool {
if token.Type != TokenIdent {

View File

@ -69,27 +69,29 @@ func (p *templateParser) parseRoot() ([]Expression, zcl.Diagnostics) {
}
func (p *templateParser) parseExpr() (Expression, zcl.Diagnostics) {
next := p.Read()
next := p.Peek()
switch tok := next.(type) {
case *templateLiteralToken:
p.Read() // eat literal
return &LiteralValueExpr{
Val: cty.StringVal(tok.Val),
SrcRange: tok.SrcRange,
}, nil
case *templateInterpToken:
p.Read() // eat interp
return tok.Expr, nil
case *templateIfToken:
// TODO: implement
panic("template if token not yet implemented")
return p.parseIf()
case *templateForToken:
// TODO: implement
panic("template for token not yet implemented")
case *templateEndToken:
p.Read() // eat erroneous token
return errPlaceholderExpr(tok.SrcRange), zcl.Diagnostics{
{
// This is a particularly unhelpful diagnostic, so callers
@ -103,6 +105,7 @@ func (p *templateParser) parseExpr() (Expression, zcl.Diagnostics) {
}
case *templateEndCtrlToken:
p.Read() // eat erroneous token
return errPlaceholderExpr(tok.SrcRange), zcl.Diagnostics{
{
Severity: zcl.DiagError,
@ -118,6 +121,118 @@ func (p *templateParser) parseExpr() (Expression, zcl.Diagnostics) {
}
}
func (p *templateParser) parseIf() (Expression, zcl.Diagnostics) {
open := p.Read()
openIf, isIf := open.(*templateIfToken)
if !isIf {
// should never happen if caller is behaving
panic("parseIf called with peeker not pointing at if token")
}
var ifExprs, elseExprs []Expression
var diags zcl.Diagnostics
var endifRange zcl.Range
currentExprs := &ifExprs
Token:
for {
next := p.Peek()
if end, isEnd := next.(*templateEndToken); isEnd {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unexpected end of template",
Detail: fmt.Sprintf(
"The if directive at %s is missing its corresponding endif directive.",
openIf.SrcRange,
),
Subject: &end.SrcRange,
})
return errPlaceholderExpr(end.SrcRange), diags
}
if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
p.Read() // eat end directive
switch end.Type {
case templateElse:
if currentExprs == &ifExprs {
currentExprs = &elseExprs
continue Token
}
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unexpected else directive",
Detail: fmt.Sprintf(
"Already in the else clause for the if started at %s.",
openIf.SrcRange,
),
Subject: &end.SrcRange,
})
case templateEndIf:
endifRange = end.SrcRange
break Token
default:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: fmt.Sprintf("Unexpected %s directive", end.Name()),
Detail: fmt.Sprintf(
"Expecting an endif directive for the if started at %s.",
openIf.SrcRange,
),
Subject: &end.SrcRange,
})
}
return errPlaceholderExpr(end.SrcRange), diags
}
expr, exprDiags := p.parseExpr()
diags = append(diags, exprDiags...)
*currentExprs = append(*currentExprs, expr)
}
if len(ifExprs) == 0 {
ifExprs = append(ifExprs, &LiteralValueExpr{
Val: cty.StringVal(""),
SrcRange: zcl.Range{
Filename: openIf.SrcRange.Filename,
Start: openIf.SrcRange.End,
End: openIf.SrcRange.End,
},
})
}
if len(elseExprs) == 0 {
elseExprs = append(elseExprs, &LiteralValueExpr{
Val: cty.StringVal(""),
SrcRange: zcl.Range{
Filename: endifRange.Filename,
Start: endifRange.Start,
End: endifRange.Start,
},
})
}
trueExpr := &TemplateExpr{
Parts: ifExprs,
SrcRange: zcl.RangeBetween(ifExprs[0].Range(), ifExprs[len(ifExprs)-1].Range()),
}
falseExpr := &TemplateExpr{
Parts: elseExprs,
SrcRange: zcl.RangeBetween(elseExprs[0].Range(), elseExprs[len(elseExprs)-1].Range()),
}
return &ConditionalExpr{
Condition: openIf.CondExpr,
TrueResult: trueExpr,
FalseResult: falseExpr,
SrcRange: zcl.RangeBetween(openIf.SrcRange, endifRange),
}, diags
}
func (p *templateParser) Peek() templateToken {
return p.Tokens[p.pos]
}
@ -214,8 +329,104 @@ Token:
Expr: expr,
SrcRange: zcl.RangeBetween(next.Range, close.Range),
})
case TokenTemplateControl:
panic("template control sequences not yet supported")
// if the opener is !{~ then we want to eat any trailing whitespace
// in the preceding literal token, assuming it is indeed a literal
// token.
if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
prevExpr := parts[len(parts)-1]
if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
}
}
p.PushIncludeNewlines(false)
kw := p.Peek()
if kw.Type != TokenIdent {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid template control keyword",
Detail: "A template control keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.",
Subject: &kw.Range,
Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
p.PopIncludeNewlines()
continue Token
}
p.Read() // eat keyword token
switch {
case ifKeyword.TokenMatches(kw):
condExpr, exprDiags := p.ParseExpression()
diags = append(diags, exprDiags...)
parts = append(parts, &templateIfToken{
CondExpr: condExpr,
SrcRange: zcl.RangeBetween(next.Range, p.NextRange()),
})
case elseKeyword.TokenMatches(kw):
parts = append(parts, &templateEndCtrlToken{
Type: templateElse,
SrcRange: zcl.RangeBetween(next.Range, p.NextRange()),
})
case endifKeyword.TokenMatches(kw):
parts = append(parts, &templateEndCtrlToken{
Type: templateEndIf,
SrcRange: zcl.RangeBetween(next.Range, p.NextRange()),
})
default:
if !p.recovery {
suggestions := []string{"if", "for", "else", "endif", "endfor"}
given := string(kw.Bytes)
suggestion := nameSuggestion(given, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid template control keyword",
Detail: fmt.Sprintf("%q is not a valid template control keyword.%s", given, suggestion),
Subject: &kw.Range,
Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
p.PopIncludeNewlines()
continue Token
}
close := p.Peek()
if close.Type != TokenTemplateSeqEnd {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: fmt.Sprintf("Extra characters in %s marker", kw.Bytes),
Detail: "Expected a closing brace to end the sequence, but found extra characters.",
Subject: &close.Range,
Context: zcl.RangeBetween(startRange, close.Range).Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
} else {
p.Read() // eat closing brace
// If the closer is ~} then we want to eat any leading
// whitespace on the next token, if it turns out to be a
// literal token.
if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
ltrimNext = true
}
}
p.PopIncludeNewlines()
default:
if !p.recovery {