8 "github.com/apparentlymart/go-textseg/textseg"
9 "github.com/hashicorp/hcl2/hcl"
10 "github.com/zclconf/go-cty/cty"
13 func (p *parser) ParseTemplate() (Expression, hcl.Diagnostics) {
14 return p.parseTemplate(TokenEOF, false)
17 func (p *parser) parseTemplate(end TokenType, flushHeredoc bool) (Expression, hcl.Diagnostics) {
18 exprs, passthru, rng, diags := p.parseTemplateInner(end, flushHeredoc)
22 panic("passthru set with len(exprs) != 1")
24 return &TemplateWrapExpr{
36 func (p *parser) parseTemplateInner(end TokenType, flushHeredoc bool) ([]Expression, bool, hcl.Range, hcl.Diagnostics) {
37 parts, diags := p.parseTemplateParts(end)
39 flushHeredocTemplateParts(parts) // Trim off leading spaces on lines per the flush heredoc spec
43 SrcRange: parts.SrcRange,
45 exprs, exprsDiags := tp.parseRoot()
46 diags = append(diags, exprsDiags...)
49 if len(parts.Tokens) == 2 { // one real token and one synthetic "end" token
50 if _, isInterp := parts.Tokens[0].(*templateInterpToken); isInterp {
55 return exprs, passthru, parts.SrcRange, diags
58 type templateParser struct {
59 Tokens []templateToken
65 func (p *templateParser) parseRoot() ([]Expression, hcl.Diagnostics) {
66 var exprs []Expression
67 var diags hcl.Diagnostics
71 if _, isEnd := next.(*templateEndToken); isEnd {
75 expr, exprDiags := p.parseExpr()
76 diags = append(diags, exprDiags...)
77 exprs = append(exprs, expr)
83 func (p *templateParser) parseExpr() (Expression, hcl.Diagnostics) {
85 switch tok := next.(type) {
87 case *templateLiteralToken:
88 p.Read() // eat literal
89 return &LiteralValueExpr{
90 Val: cty.StringVal(tok.Val),
91 SrcRange: tok.SrcRange,
94 case *templateInterpToken:
95 p.Read() // eat interp
98 case *templateIfToken:
101 case *templateForToken:
104 case *templateEndToken:
105 p.Read() // eat erroneous token
106 return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
108 // This is a particularly unhelpful diagnostic, so callers
109 // should attempt to pre-empt it and produce a more helpful
110 // diagnostic that is context-aware.
111 Severity: hcl.DiagError,
112 Summary: "Unexpected end of template",
113 Detail: "The control directives within this template are unbalanced.",
114 Subject: &tok.SrcRange,
118 case *templateEndCtrlToken:
119 p.Read() // eat erroneous token
120 return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
122 Severity: hcl.DiagError,
123 Summary: fmt.Sprintf("Unexpected %s directive", tok.Name()),
124 Detail: "The control directives within this template are unbalanced.",
125 Subject: &tok.SrcRange,
130 // should never happen, because above should be exhaustive
131 panic(fmt.Sprintf("unhandled template token type %T", next))
135 func (p *templateParser) parseIf() (Expression, hcl.Diagnostics) {
137 openIf, isIf := open.(*templateIfToken)
139 // should never happen if caller is behaving
140 panic("parseIf called with peeker not pointing at if token")
143 var ifExprs, elseExprs []Expression
144 var diags hcl.Diagnostics
145 var endifRange hcl.Range
147 currentExprs := &ifExprs
151 if end, isEnd := next.(*templateEndToken); isEnd {
152 diags = append(diags, &hcl.Diagnostic{
153 Severity: hcl.DiagError,
154 Summary: "Unexpected end of template",
156 "The if directive at %s is missing its corresponding endif directive.",
159 Subject: &end.SrcRange,
161 return errPlaceholderExpr(end.SrcRange), diags
163 if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
164 p.Read() // eat end directive
169 if currentExprs == &ifExprs {
170 currentExprs = &elseExprs
174 diags = append(diags, &hcl.Diagnostic{
175 Severity: hcl.DiagError,
176 Summary: "Unexpected else directive",
178 "Already in the else clause for the if started at %s.",
181 Subject: &end.SrcRange,
185 endifRange = end.SrcRange
189 diags = append(diags, &hcl.Diagnostic{
190 Severity: hcl.DiagError,
191 Summary: fmt.Sprintf("Unexpected %s directive", end.Name()),
193 "Expecting an endif directive for the if started at %s.",
196 Subject: &end.SrcRange,
200 return errPlaceholderExpr(end.SrcRange), diags
203 expr, exprDiags := p.parseExpr()
204 diags = append(diags, exprDiags...)
205 *currentExprs = append(*currentExprs, expr)
208 if len(ifExprs) == 0 {
209 ifExprs = append(ifExprs, &LiteralValueExpr{
210 Val: cty.StringVal(""),
212 Filename: openIf.SrcRange.Filename,
213 Start: openIf.SrcRange.End,
214 End: openIf.SrcRange.End,
218 if len(elseExprs) == 0 {
219 elseExprs = append(elseExprs, &LiteralValueExpr{
220 Val: cty.StringVal(""),
222 Filename: endifRange.Filename,
223 Start: endifRange.Start,
224 End: endifRange.Start,
229 trueExpr := &TemplateExpr{
231 SrcRange: hcl.RangeBetween(ifExprs[0].Range(), ifExprs[len(ifExprs)-1].Range()),
233 falseExpr := &TemplateExpr{
235 SrcRange: hcl.RangeBetween(elseExprs[0].Range(), elseExprs[len(elseExprs)-1].Range()),
238 return &ConditionalExpr{
239 Condition: openIf.CondExpr,
240 TrueResult: trueExpr,
241 FalseResult: falseExpr,
243 SrcRange: hcl.RangeBetween(openIf.SrcRange, endifRange),
247 func (p *templateParser) parseFor() (Expression, hcl.Diagnostics) {
249 openFor, isFor := open.(*templateForToken)
251 // should never happen if caller is behaving
252 panic("parseFor called with peeker not pointing at for token")
255 var contentExprs []Expression
256 var diags hcl.Diagnostics
257 var endforRange hcl.Range
262 if end, isEnd := next.(*templateEndToken); isEnd {
263 diags = append(diags, &hcl.Diagnostic{
264 Severity: hcl.DiagError,
265 Summary: "Unexpected end of template",
267 "The for directive at %s is missing its corresponding endfor directive.",
270 Subject: &end.SrcRange,
272 return errPlaceholderExpr(end.SrcRange), diags
274 if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
275 p.Read() // eat end directive
280 diags = append(diags, &hcl.Diagnostic{
281 Severity: hcl.DiagError,
282 Summary: "Unexpected else directive",
283 Detail: "An else clause is not expected for a for directive.",
284 Subject: &end.SrcRange,
288 endforRange = end.SrcRange
292 diags = append(diags, &hcl.Diagnostic{
293 Severity: hcl.DiagError,
294 Summary: fmt.Sprintf("Unexpected %s directive", end.Name()),
296 "Expecting an endfor directive corresponding to the for directive at %s.",
299 Subject: &end.SrcRange,
303 return errPlaceholderExpr(end.SrcRange), diags
306 expr, exprDiags := p.parseExpr()
307 diags = append(diags, exprDiags...)
308 contentExprs = append(contentExprs, expr)
311 if len(contentExprs) == 0 {
312 contentExprs = append(contentExprs, &LiteralValueExpr{
313 Val: cty.StringVal(""),
315 Filename: openFor.SrcRange.Filename,
316 Start: openFor.SrcRange.End,
317 End: openFor.SrcRange.End,
322 contentExpr := &TemplateExpr{
324 SrcRange: hcl.RangeBetween(contentExprs[0].Range(), contentExprs[len(contentExprs)-1].Range()),
328 KeyVar: openFor.KeyVar,
329 ValVar: openFor.ValVar,
331 CollExpr: openFor.CollExpr,
332 ValExpr: contentExpr,
334 SrcRange: hcl.RangeBetween(openFor.SrcRange, endforRange),
335 OpenRange: openFor.SrcRange,
336 CloseRange: endforRange,
339 return &TemplateJoinExpr{
344 func (p *templateParser) Peek() templateToken {
345 return p.Tokens[p.pos]
348 func (p *templateParser) Read() templateToken {
350 if _, end := ret.(*templateEndToken); !end {
356 // parseTemplateParts produces a flat sequence of "template tokens", which are
357 // either literal values (with any "trimming" already applied), interpolation
358 // sequences, or control flow markers.
360 // A further pass is required on the result to turn it into an AST.
361 func (p *parser) parseTemplateParts(end TokenType) (*templateParts, hcl.Diagnostics) {
362 var parts []templateToken
363 var diags hcl.Diagnostics
365 startRange := p.NextRange()
367 nextCanTrimPrev := false
368 var endRange hcl.Range
373 if next.Type == end {
375 endRange = next.Range
381 canTrimPrev := nextCanTrimPrev
382 nextCanTrimPrev = false
385 case TokenStringLit, TokenQuotedLit:
386 str, strDiags := p.decodeStringLit(next)
387 diags = append(diags, strDiags...)
390 str = strings.TrimLeftFunc(str, unicode.IsSpace)
393 parts = append(parts, &templateLiteralToken{
395 SrcRange: next.Range,
397 nextCanTrimPrev = true
399 case TokenTemplateInterp:
400 // if the opener is ${~ then we want to eat any trailing whitespace
401 // in the preceding literal token, assuming it is indeed a literal
403 if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
404 prevExpr := parts[len(parts)-1]
405 if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
406 lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
410 p.PushIncludeNewlines(false)
411 expr, exprDiags := p.ParseExpression()
412 diags = append(diags, exprDiags...)
414 if close.Type != TokenTemplateSeqEnd {
416 diags = append(diags, &hcl.Diagnostic{
417 Severity: hcl.DiagError,
418 Summary: "Extra characters after interpolation expression",
419 Detail: "Expected a closing brace to end the interpolation expression, but found extra characters.",
420 Subject: &close.Range,
421 Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
424 p.recover(TokenTemplateSeqEnd)
426 p.Read() // eat closing brace
428 // If the closer is ~} then we want to eat any leading
429 // whitespace on the next token, if it turns out to be a
431 if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
435 p.PopIncludeNewlines()
436 parts = append(parts, &templateInterpToken{
438 SrcRange: hcl.RangeBetween(next.Range, close.Range),
441 case TokenTemplateControl:
442 // if the opener is %{~ then we want to eat any trailing whitespace
443 // in the preceding literal token, assuming it is indeed a literal
445 if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
446 prevExpr := parts[len(parts)-1]
447 if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
448 lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
451 p.PushIncludeNewlines(false)
454 if kw.Type != TokenIdent {
456 diags = append(diags, &hcl.Diagnostic{
457 Severity: hcl.DiagError,
458 Summary: "Invalid template directive",
459 Detail: "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a %{ sequence.",
461 Context: hcl.RangeBetween(next.Range, kw.Range).Ptr(),
464 p.recover(TokenTemplateSeqEnd)
465 p.PopIncludeNewlines()
468 p.Read() // eat keyword token
472 case ifKeyword.TokenMatches(kw):
473 condExpr, exprDiags := p.ParseExpression()
474 diags = append(diags, exprDiags...)
475 parts = append(parts, &templateIfToken{
477 SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
480 case elseKeyword.TokenMatches(kw):
481 parts = append(parts, &templateEndCtrlToken{
483 SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
486 case endifKeyword.TokenMatches(kw):
487 parts = append(parts, &templateEndCtrlToken{
489 SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
492 case forKeyword.TokenMatches(kw):
493 var keyName, valName string
494 if p.Peek().Type != TokenIdent {
496 diags = append(diags, &hcl.Diagnostic{
497 Severity: hcl.DiagError,
498 Summary: "Invalid 'for' directive",
499 Detail: "For directive requires variable name after 'for'.",
500 Subject: p.Peek().Range.Ptr(),
503 p.recover(TokenTemplateSeqEnd)
504 p.PopIncludeNewlines()
508 valName = string(p.Read().Bytes)
510 if p.Peek().Type == TokenComma {
511 // What we just read was actually the key, then.
513 p.Read() // eat comma
515 if p.Peek().Type != TokenIdent {
517 diags = append(diags, &hcl.Diagnostic{
518 Severity: hcl.DiagError,
519 Summary: "Invalid 'for' directive",
520 Detail: "For directive requires value variable name after comma.",
521 Subject: p.Peek().Range.Ptr(),
524 p.recover(TokenTemplateSeqEnd)
525 p.PopIncludeNewlines()
529 valName = string(p.Read().Bytes)
532 if !inKeyword.TokenMatches(p.Peek()) {
534 diags = append(diags, &hcl.Diagnostic{
535 Severity: hcl.DiagError,
536 Summary: "Invalid 'for' directive",
537 Detail: "For directive requires 'in' keyword after names.",
538 Subject: p.Peek().Range.Ptr(),
541 p.recover(TokenTemplateSeqEnd)
542 p.PopIncludeNewlines()
545 p.Read() // eat 'in' keyword
547 collExpr, collDiags := p.ParseExpression()
548 diags = append(diags, collDiags...)
549 parts = append(parts, &templateForToken{
554 SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
557 case endforKeyword.TokenMatches(kw):
558 parts = append(parts, &templateEndCtrlToken{
559 Type: templateEndFor,
560 SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
565 suggestions := []string{"if", "for", "else", "endif", "endfor"}
566 given := string(kw.Bytes)
567 suggestion := nameSuggestion(given, suggestions)
568 if suggestion != "" {
569 suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
572 diags = append(diags, &hcl.Diagnostic{
573 Severity: hcl.DiagError,
574 Summary: "Invalid template control keyword",
575 Detail: fmt.Sprintf("%q is not a valid template control keyword.%s", given, suggestion),
577 Context: hcl.RangeBetween(next.Range, kw.Range).Ptr(),
580 p.recover(TokenTemplateSeqEnd)
581 p.PopIncludeNewlines()
587 if close.Type != TokenTemplateSeqEnd {
589 diags = append(diags, &hcl.Diagnostic{
590 Severity: hcl.DiagError,
591 Summary: fmt.Sprintf("Extra characters in %s marker", kw.Bytes),
592 Detail: "Expected a closing brace to end the sequence, but found extra characters.",
593 Subject: &close.Range,
594 Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
597 p.recover(TokenTemplateSeqEnd)
599 p.Read() // eat closing brace
601 // If the closer is ~} then we want to eat any leading
602 // whitespace on the next token, if it turns out to be a
604 if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
608 p.PopIncludeNewlines()
612 diags = append(diags, &hcl.Diagnostic{
613 Severity: hcl.DiagError,
614 Summary: "Unterminated template string",
615 Detail: "No closing marker was found for the string.",
616 Subject: &next.Range,
617 Context: hcl.RangeBetween(startRange, next.Range).Ptr(),
620 final := p.recover(end)
621 endRange = final.Range
627 // If a sequence has no content, we'll treat it as if it had an
628 // empty string in it because that's what the user probably means
629 // if they write "" in configuration.
630 parts = append(parts, &templateLiteralToken{
633 // Range is the zero-character span immediately after the
635 Filename: startRange.Filename,
636 Start: startRange.End,
642 // Always end with an end token, so the parser can produce diagnostics
643 // about unclosed items with proper position information.
644 parts = append(parts, &templateEndToken{
648 ret := &templateParts{
650 SrcRange: hcl.RangeBetween(startRange, endRange),
656 // flushHeredocTemplateParts modifies in-place the line-leading literal strings
657 // to apply the flush heredoc processing rule: find the line with the smallest
658 // number of whitespace characters as prefix and then trim that number of
659 // characters from all of the lines.
661 // This rule is applied to static tokens rather than to the rendered result,
662 // so interpolating a string with leading whitespace cannot affect the chosen
664 func flushHeredocTemplateParts(parts *templateParts) {
665 if len(parts.Tokens) == 0 {
670 const maxInt = int((^uint(0)) >> 1)
674 var adjust []*templateLiteralToken
675 for _, ttok := range parts.Tokens {
679 if lit, ok := ttok.(*templateLiteralToken); ok {
681 trimmed := strings.TrimLeftFunc(orig, unicode.IsSpace)
682 // If a token is entirely spaces and ends with a newline
683 // then it's a "blank line" and thus not considered for
684 // space-prefix-counting purposes.
685 if len(trimmed) == 0 && strings.HasSuffix(orig, "\n") {
688 spaceBytes := len(lit.Val) - len(trimmed)
689 spaces, _ = textseg.TokenCount([]byte(orig[:spaceBytes]), textseg.ScanGraphemeClusters)
690 adjust = append(adjust, lit)
692 } else if _, ok := ttok.(*templateEndToken); ok {
693 break // don't process the end token since it never has spaces before it
695 if spaces < minSpaces {
699 if lit, ok := ttok.(*templateLiteralToken); ok {
700 if strings.HasSuffix(lit.Val, "\n") {
701 newline = true // The following token, if any, begins a new line
706 for _, lit := range adjust {
707 // Since we want to count space _characters_ rather than space _bytes_,
708 // we can't just do a straightforward slice operation here and instead
709 // need to hunt for the split point with a scanner.
710 valBytes := []byte(lit.Val)
712 for i := 0; i < minSpaces; i++ {
713 adv, _, _ := textseg.ScanGraphemeClusters(valBytes, true)
714 spaceByteCount += adv
715 valBytes = valBytes[adv:]
717 lit.Val = lit.Val[spaceByteCount:]
718 lit.SrcRange.Start.Column += minSpaces
719 lit.SrcRange.Start.Byte += spaceByteCount
723 type templateParts struct {
724 Tokens []templateToken
728 // templateToken is a higher-level token that represents a single atom within
729 // the template language. Our template parsing first raises the raw token
730 // stream to a sequence of templateToken, and then transforms the result into
731 // an expression tree.
732 type templateToken interface {
733 templateToken() templateToken
736 type templateLiteralToken struct {
742 type templateInterpToken struct {
748 type templateIfToken struct {
754 type templateForToken struct {
755 KeyVar string // empty if ignoring key
762 type templateEndCtrlType int
765 templateEndIf templateEndCtrlType = iota
770 type templateEndCtrlToken struct {
771 Type templateEndCtrlType
776 func (t *templateEndCtrlToken) Name() string {
785 // should never happen
786 panic("invalid templateEndCtrlType")
790 type templateEndToken struct {
795 type isTemplateToken [0]int
797 func (t isTemplateToken) templateToken() templateToken {