From 61ebd9b65b8ef2e0f9fe47e9116e4ac6546a1a6d Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 18 Jun 2017 08:14:55 -0700 Subject: [PATCH] zclsyntax: template "for" construct --- zcl/zclsyntax/expression_template.go | 80 ++++++++++ zcl/zclsyntax/expression_template_test.go | 46 ++++++ zcl/zclsyntax/expression_vars.go | 4 + zcl/zclsyntax/parser_template.go | 172 +++++++++++++++++++++- 4 files changed, 300 insertions(+), 2 deletions(-) diff --git a/zcl/zclsyntax/expression_template.go b/zcl/zclsyntax/expression_template.go index 4c50e15..635db6b 100644 --- a/zcl/zclsyntax/expression_template.go +++ b/zcl/zclsyntax/expression_template.go @@ -85,6 +85,86 @@ func (e *TemplateExpr) StartRange() zcl.Range { return e.Parts[0].StartRange() } +// TemplateJoinExpr is used to convert tuples of strings produced by template +// constructs (i.e. for loops) into flat strings, by converting the values +// tos strings and joining them. This AST node is not used directly; it's +// produced as part of the AST of a "for" loop in a template. +type TemplateJoinExpr struct { + Tuple Expression +} + +func (e *TemplateJoinExpr) walkChildNodes(w internalWalkFunc) { + e.Tuple = w(e.Tuple).(Expression) +} + +func (e *TemplateJoinExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) { + tuple, diags := e.Tuple.Value(ctx) + + if tuple.IsNull() { + // This indicates a bug in the code that constructed the AST. + panic("TemplateJoinExpr got null tuple") + } + if tuple.Type() == cty.DynamicPseudoType { + return cty.UnknownVal(cty.String), diags + } + if !tuple.Type().IsTupleType() { + // This indicates a bug in the code that constructed the AST. + panic("TemplateJoinExpr got non-tuple tuple") + } + if !tuple.IsKnown() { + return cty.UnknownVal(cty.String), diags + } + + buf := &bytes.Buffer{} + it := tuple.ElementIterator() + for it.Next() { + _, val := it.Element() + + if val.IsNull() { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid template interpolation value", + Detail: fmt.Sprintf( + "An iteration result is null. Cannot include a null value in a string template.", + ), + Subject: e.Range().Ptr(), + }) + continue + } + if val.Type() == cty.DynamicPseudoType { + return cty.UnknownVal(cty.String), diags + } + strVal, err := convert.Convert(val, cty.String) + if err != nil { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid template interpolation value", + Detail: fmt.Sprintf( + "Cannot include one of the interpolation results into the string template: %s.", + err.Error(), + ), + Subject: e.Range().Ptr(), + }) + continue + } + if !val.IsKnown() { + return cty.UnknownVal(cty.String), diags + } + + buf.WriteString(strVal.AsString()) + } + + return cty.StringVal(buf.String()), diags +} + +func (e *TemplateJoinExpr) Range() zcl.Range { + return e.Tuple.Range() +} + +func (e *TemplateJoinExpr) StartRange() zcl.Range { + return e.Tuple.StartRange() +} + // TemplateWrapExpr is used instead of a TemplateExpr when a template // consists _only_ of a single interpolation sequence. In that case, the // template's result is the single interpolation's result, verbatim with diff --git a/zcl/zclsyntax/expression_template_test.go b/zcl/zclsyntax/expression_template_test.go index 4ce078f..7d89042 100644 --- a/zcl/zclsyntax/expression_template_test.go +++ b/zcl/zclsyntax/expression_template_test.go @@ -171,6 +171,52 @@ trim`, cty.UnknownVal(cty.String), 2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected }, + { + `%{ for v in ["a", "b", "c"] }${v}%{ endfor }`, + nil, + cty.StringVal("abc"), + 0, + }, + { + `%{ for v in ["a", "b", "c"] } ${v} %{ endfor }`, + nil, + cty.StringVal(" a b c "), + 0, + }, + { + `%{ for v in ["a", "b", "c"] ~} ${v} %{~ endfor }`, + nil, + cty.StringVal("abc"), + 0, + }, + { + `%{ for v in [] }${v}%{ endfor }`, + nil, + cty.StringVal(""), + 0, + }, + { + `%{ for i, v in ["a", "b", "c"] }${i}${v}%{ endfor }`, + nil, + cty.StringVal("0a1b2c"), + 0, + }, + { + `%{ for k, v in {"A" = "a", "B" = "b", "C" = "c"} }${k}${v}%{ endfor }`, + nil, + cty.StringVal("AaBbCc"), + 0, + }, + { + `%{ for v in ["a", "b", "c"] }${v}${nl}%{ endfor }`, + &zcl.EvalContext{ + Variables: map[string]cty.Value{ + "nl": cty.StringVal("\n"), + }, + }, + cty.StringVal("a\nb\nc\n"), + 0, + }, } for _, test := range tests { diff --git a/zcl/zclsyntax/expression_vars.go b/zcl/zclsyntax/expression_vars.go index 9d0c411..a0bfea5 100755 --- a/zcl/zclsyntax/expression_vars.go +++ b/zcl/zclsyntax/expression_vars.go @@ -55,6 +55,10 @@ func (e *TemplateExpr) Variables() []zcl.Traversal { return Variables(e) } +func (e *TemplateJoinExpr) Variables() []zcl.Traversal { + return Variables(e) +} + func (e *TemplateWrapExpr) Variables() []zcl.Traversal { return Variables(e) } diff --git a/zcl/zclsyntax/parser_template.go b/zcl/zclsyntax/parser_template.go index 7c0114a..cc5120d 100644 --- a/zcl/zclsyntax/parser_template.go +++ b/zcl/zclsyntax/parser_template.go @@ -240,6 +240,103 @@ Token: }, diags } +func (p *templateParser) parseFor() (Expression, zcl.Diagnostics) { + open := p.Read() + openFor, isFor := open.(*templateForToken) + if !isFor { + // should never happen if caller is behaving + panic("parseFor called with peeker not pointing at for token") + } + + var contentExprs []Expression + var diags zcl.Diagnostics + var endforRange zcl.Range + +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 for directive at %s is missing its corresponding endfor directive.", + openFor.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: + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unexpected else directive", + Detail: "An else clause is not expected for a for directive.", + Subject: &end.SrcRange, + }) + + case templateEndFor: + endforRange = 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 endfor directive corresponding to the for directive at %s.", + openFor.SrcRange, + ), + Subject: &end.SrcRange, + }) + } + + return errPlaceholderExpr(end.SrcRange), diags + } + + expr, exprDiags := p.parseExpr() + diags = append(diags, exprDiags...) + contentExprs = append(contentExprs, expr) + } + + if len(contentExprs) == 0 { + contentExprs = append(contentExprs, &LiteralValueExpr{ + Val: cty.StringVal(""), + SrcRange: zcl.Range{ + Filename: openFor.SrcRange.Filename, + Start: openFor.SrcRange.End, + End: openFor.SrcRange.End, + }, + }) + } + + contentExpr := &TemplateExpr{ + Parts: contentExprs, + SrcRange: zcl.RangeBetween(contentExprs[0].Range(), contentExprs[len(contentExprs)-1].Range()), + } + + forExpr := &ForExpr{ + KeyVar: openFor.KeyVar, + ValVar: openFor.ValVar, + + CollExpr: openFor.CollExpr, + ValExpr: contentExpr, + + SrcRange: zcl.RangeBetween(openFor.SrcRange, endforRange), + OpenRange: openFor.SrcRange, + CloseRange: endforRange, + } + + return &TemplateJoinExpr{ + Tuple: forExpr, + }, diags +} + func (p *templateParser) Peek() templateToken { return p.Tokens[p.pos] } @@ -354,8 +451,8 @@ Token: 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.", + Summary: "Invalid template directive", + Detail: "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.", Subject: &kw.Range, Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(), }) @@ -388,6 +485,77 @@ Token: SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), }) + case forKeyword.TokenMatches(kw): + var keyName, valName string + if p.Peek().Type != TokenIdent { + if !p.recovery { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid 'for' directive", + Detail: "For directive requires variable name after 'for'.", + Subject: p.Peek().Range.Ptr(), + }) + } + p.recover(TokenTemplateSeqEnd) + p.PopIncludeNewlines() + continue Token + } + + valName = string(p.Read().Bytes) + + if p.Peek().Type == TokenComma { + // What we just read was actually the key, then. + keyName = valName + p.Read() // eat comma + + if p.Peek().Type != TokenIdent { + if !p.recovery { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid 'for' directive", + Detail: "For directive requires value variable name after comma.", + Subject: p.Peek().Range.Ptr(), + }) + } + p.recover(TokenTemplateSeqEnd) + p.PopIncludeNewlines() + continue Token + } + + valName = string(p.Read().Bytes) + } + + if !inKeyword.TokenMatches(p.Peek()) { + if !p.recovery { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid 'for' directive", + Detail: "For directive requires 'in' keyword after names.", + Subject: p.Peek().Range.Ptr(), + }) + } + p.recover(TokenTemplateSeqEnd) + p.PopIncludeNewlines() + continue Token + } + p.Read() // eat 'in' keyword + + collExpr, collDiags := p.ParseExpression() + diags = append(diags, collDiags...) + parts = append(parts, &templateForToken{ + KeyVar: keyName, + ValVar: valName, + CollExpr: collExpr, + + SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), + }) + + case endforKeyword.TokenMatches(kw): + parts = append(parts, &templateEndCtrlToken{ + Type: templateEndFor, + SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), + }) + default: if !p.recovery { suggestions := []string{"if", "for", "else", "endif", "endfor"}