github.com/hashicorp/hcl/v2@v2.20.0/hclsyntax/parser_template.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hclsyntax
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"unicode"
    10  
    11  	"github.com/apparentlymart/go-textseg/v15/textseg"
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/zclconf/go-cty/cty"
    14  )
    15  
    16  func (p *parser) ParseTemplate() (Expression, hcl.Diagnostics) {
    17  	return p.parseTemplate(TokenEOF, false)
    18  }
    19  
    20  func (p *parser) parseTemplate(end TokenType, flushHeredoc bool) (Expression, hcl.Diagnostics) {
    21  	exprs, passthru, rng, diags := p.parseTemplateInner(end, flushHeredoc)
    22  
    23  	if passthru {
    24  		if len(exprs) != 1 {
    25  			panic("passthru set with len(exprs) != 1")
    26  		}
    27  		return &TemplateWrapExpr{
    28  			Wrapped:  exprs[0],
    29  			SrcRange: rng,
    30  		}, diags
    31  	}
    32  
    33  	return &TemplateExpr{
    34  		Parts:    exprs,
    35  		SrcRange: rng,
    36  	}, diags
    37  }
    38  
    39  func (p *parser) parseTemplateInner(end TokenType, flushHeredoc bool) ([]Expression, bool, hcl.Range, hcl.Diagnostics) {
    40  	parts, diags := p.parseTemplateParts(end)
    41  	if flushHeredoc {
    42  		flushHeredocTemplateParts(parts) // Trim off leading spaces on lines per the flush heredoc spec
    43  	}
    44  	meldConsecutiveStringLiterals(parts)
    45  	tp := templateParser{
    46  		Tokens:   parts.Tokens,
    47  		SrcRange: parts.SrcRange,
    48  	}
    49  	exprs, exprsDiags := tp.parseRoot()
    50  	diags = append(diags, exprsDiags...)
    51  
    52  	passthru := false
    53  	if len(parts.Tokens) == 2 { // one real token and one synthetic "end" token
    54  		if _, isInterp := parts.Tokens[0].(*templateInterpToken); isInterp {
    55  			passthru = true
    56  		}
    57  	}
    58  
    59  	return exprs, passthru, parts.SrcRange, diags
    60  }
    61  
    62  type templateParser struct {
    63  	Tokens   []templateToken
    64  	SrcRange hcl.Range
    65  
    66  	pos int
    67  }
    68  
    69  func (p *templateParser) parseRoot() ([]Expression, hcl.Diagnostics) {
    70  	var exprs []Expression
    71  	var diags hcl.Diagnostics
    72  
    73  	for {
    74  		next := p.Peek()
    75  		if _, isEnd := next.(*templateEndToken); isEnd {
    76  			break
    77  		}
    78  
    79  		expr, exprDiags := p.parseExpr()
    80  		diags = append(diags, exprDiags...)
    81  		exprs = append(exprs, expr)
    82  	}
    83  
    84  	return exprs, diags
    85  }
    86  
    87  func (p *templateParser) parseExpr() (Expression, hcl.Diagnostics) {
    88  	next := p.Peek()
    89  	switch tok := next.(type) {
    90  
    91  	case *templateLiteralToken:
    92  		p.Read() // eat literal
    93  		return &LiteralValueExpr{
    94  			Val:      cty.StringVal(tok.Val),
    95  			SrcRange: tok.SrcRange,
    96  		}, nil
    97  
    98  	case *templateInterpToken:
    99  		p.Read() // eat interp
   100  		return tok.Expr, nil
   101  
   102  	case *templateIfToken:
   103  		return p.parseIf()
   104  
   105  	case *templateForToken:
   106  		return p.parseFor()
   107  
   108  	case *templateEndToken:
   109  		p.Read() // eat erroneous token
   110  		return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
   111  			{
   112  				// This is a particularly unhelpful diagnostic, so callers
   113  				// should attempt to pre-empt it and produce a more helpful
   114  				// diagnostic that is context-aware.
   115  				Severity: hcl.DiagError,
   116  				Summary:  "Unexpected end of template",
   117  				Detail:   "The control directives within this template are unbalanced.",
   118  				Subject:  &tok.SrcRange,
   119  			},
   120  		}
   121  
   122  	case *templateEndCtrlToken:
   123  		p.Read() // eat erroneous token
   124  		return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
   125  			{
   126  				Severity: hcl.DiagError,
   127  				Summary:  fmt.Sprintf("Unexpected %s directive", tok.Name()),
   128  				Detail:   "The control directives within this template are unbalanced.",
   129  				Subject:  &tok.SrcRange,
   130  			},
   131  		}
   132  
   133  	default:
   134  		// should never happen, because above should be exhaustive
   135  		panic(fmt.Sprintf("unhandled template token type %T", next))
   136  	}
   137  }
   138  
   139  func (p *templateParser) parseIf() (Expression, hcl.Diagnostics) {
   140  	open := p.Read()
   141  	openIf, isIf := open.(*templateIfToken)
   142  	if !isIf {
   143  		// should never happen if caller is behaving
   144  		panic("parseIf called with peeker not pointing at if token")
   145  	}
   146  
   147  	var ifExprs, elseExprs []Expression
   148  	var diags hcl.Diagnostics
   149  	var endifRange hcl.Range
   150  
   151  	currentExprs := &ifExprs
   152  Token:
   153  	for {
   154  		next := p.Peek()
   155  		if end, isEnd := next.(*templateEndToken); isEnd {
   156  			diags = append(diags, &hcl.Diagnostic{
   157  				Severity: hcl.DiagError,
   158  				Summary:  "Unexpected end of template",
   159  				Detail: fmt.Sprintf(
   160  					"The if directive at %s is missing its corresponding endif directive.",
   161  					openIf.SrcRange,
   162  				),
   163  				Subject: &end.SrcRange,
   164  			})
   165  			return errPlaceholderExpr(end.SrcRange), diags
   166  		}
   167  		if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
   168  			p.Read() // eat end directive
   169  
   170  			switch end.Type {
   171  
   172  			case templateElse:
   173  				if currentExprs == &ifExprs {
   174  					currentExprs = &elseExprs
   175  					continue Token
   176  				}
   177  
   178  				diags = append(diags, &hcl.Diagnostic{
   179  					Severity: hcl.DiagError,
   180  					Summary:  "Unexpected else directive",
   181  					Detail: fmt.Sprintf(
   182  						"Already in the else clause for the if started at %s.",
   183  						openIf.SrcRange,
   184  					),
   185  					Subject: &end.SrcRange,
   186  				})
   187  
   188  			case templateEndIf:
   189  				endifRange = end.SrcRange
   190  				break Token
   191  
   192  			default:
   193  				diags = append(diags, &hcl.Diagnostic{
   194  					Severity: hcl.DiagError,
   195  					Summary:  fmt.Sprintf("Unexpected %s directive", end.Name()),
   196  					Detail: fmt.Sprintf(
   197  						"Expecting an endif directive for the if started at %s.",
   198  						openIf.SrcRange,
   199  					),
   200  					Subject: &end.SrcRange,
   201  				})
   202  			}
   203  
   204  			return errPlaceholderExpr(end.SrcRange), diags
   205  		}
   206  
   207  		expr, exprDiags := p.parseExpr()
   208  		diags = append(diags, exprDiags...)
   209  		*currentExprs = append(*currentExprs, expr)
   210  	}
   211  
   212  	if len(ifExprs) == 0 {
   213  		ifExprs = append(ifExprs, &LiteralValueExpr{
   214  			Val: cty.StringVal(""),
   215  			SrcRange: hcl.Range{
   216  				Filename: openIf.SrcRange.Filename,
   217  				Start:    openIf.SrcRange.End,
   218  				End:      openIf.SrcRange.End,
   219  			},
   220  		})
   221  	}
   222  	if len(elseExprs) == 0 {
   223  		elseExprs = append(elseExprs, &LiteralValueExpr{
   224  			Val: cty.StringVal(""),
   225  			SrcRange: hcl.Range{
   226  				Filename: endifRange.Filename,
   227  				Start:    endifRange.Start,
   228  				End:      endifRange.Start,
   229  			},
   230  		})
   231  	}
   232  
   233  	trueExpr := &TemplateExpr{
   234  		Parts:    ifExprs,
   235  		SrcRange: hcl.RangeBetween(ifExprs[0].Range(), ifExprs[len(ifExprs)-1].Range()),
   236  	}
   237  	falseExpr := &TemplateExpr{
   238  		Parts:    elseExprs,
   239  		SrcRange: hcl.RangeBetween(elseExprs[0].Range(), elseExprs[len(elseExprs)-1].Range()),
   240  	}
   241  
   242  	return &ConditionalExpr{
   243  		Condition:   openIf.CondExpr,
   244  		TrueResult:  trueExpr,
   245  		FalseResult: falseExpr,
   246  
   247  		SrcRange: hcl.RangeBetween(openIf.SrcRange, endifRange),
   248  	}, diags
   249  }
   250  
   251  func (p *templateParser) parseFor() (Expression, hcl.Diagnostics) {
   252  	open := p.Read()
   253  	openFor, isFor := open.(*templateForToken)
   254  	if !isFor {
   255  		// should never happen if caller is behaving
   256  		panic("parseFor called with peeker not pointing at for token")
   257  	}
   258  
   259  	var contentExprs []Expression
   260  	var diags hcl.Diagnostics
   261  	var endforRange hcl.Range
   262  
   263  Token:
   264  	for {
   265  		next := p.Peek()
   266  		if end, isEnd := next.(*templateEndToken); isEnd {
   267  			diags = append(diags, &hcl.Diagnostic{
   268  				Severity: hcl.DiagError,
   269  				Summary:  "Unexpected end of template",
   270  				Detail: fmt.Sprintf(
   271  					"The for directive at %s is missing its corresponding endfor directive.",
   272  					openFor.SrcRange,
   273  				),
   274  				Subject: &end.SrcRange,
   275  			})
   276  			return errPlaceholderExpr(end.SrcRange), diags
   277  		}
   278  		if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
   279  			p.Read() // eat end directive
   280  
   281  			switch end.Type {
   282  
   283  			case templateElse:
   284  				diags = append(diags, &hcl.Diagnostic{
   285  					Severity: hcl.DiagError,
   286  					Summary:  "Unexpected else directive",
   287  					Detail:   "An else clause is not expected for a for directive.",
   288  					Subject:  &end.SrcRange,
   289  				})
   290  
   291  			case templateEndFor:
   292  				endforRange = end.SrcRange
   293  				break Token
   294  
   295  			default:
   296  				diags = append(diags, &hcl.Diagnostic{
   297  					Severity: hcl.DiagError,
   298  					Summary:  fmt.Sprintf("Unexpected %s directive", end.Name()),
   299  					Detail: fmt.Sprintf(
   300  						"Expecting an endfor directive corresponding to the for directive at %s.",
   301  						openFor.SrcRange,
   302  					),
   303  					Subject: &end.SrcRange,
   304  				})
   305  			}
   306  
   307  			return errPlaceholderExpr(end.SrcRange), diags
   308  		}
   309  
   310  		expr, exprDiags := p.parseExpr()
   311  		diags = append(diags, exprDiags...)
   312  		contentExprs = append(contentExprs, expr)
   313  	}
   314  
   315  	if len(contentExprs) == 0 {
   316  		contentExprs = append(contentExprs, &LiteralValueExpr{
   317  			Val: cty.StringVal(""),
   318  			SrcRange: hcl.Range{
   319  				Filename: openFor.SrcRange.Filename,
   320  				Start:    openFor.SrcRange.End,
   321  				End:      openFor.SrcRange.End,
   322  			},
   323  		})
   324  	}
   325  
   326  	contentExpr := &TemplateExpr{
   327  		Parts:    contentExprs,
   328  		SrcRange: hcl.RangeBetween(contentExprs[0].Range(), contentExprs[len(contentExprs)-1].Range()),
   329  	}
   330  
   331  	forExpr := &ForExpr{
   332  		KeyVar: openFor.KeyVar,
   333  		ValVar: openFor.ValVar,
   334  
   335  		CollExpr: openFor.CollExpr,
   336  		ValExpr:  contentExpr,
   337  
   338  		SrcRange:   hcl.RangeBetween(openFor.SrcRange, endforRange),
   339  		OpenRange:  openFor.SrcRange,
   340  		CloseRange: endforRange,
   341  	}
   342  
   343  	return &TemplateJoinExpr{
   344  		Tuple: forExpr,
   345  	}, diags
   346  }
   347  
   348  func (p *templateParser) Peek() templateToken {
   349  	return p.Tokens[p.pos]
   350  }
   351  
   352  func (p *templateParser) Read() templateToken {
   353  	ret := p.Peek()
   354  	if _, end := ret.(*templateEndToken); !end {
   355  		p.pos++
   356  	}
   357  	return ret
   358  }
   359  
   360  // parseTemplateParts produces a flat sequence of "template tokens", which are
   361  // either literal values (with any "trimming" already applied), interpolation
   362  // sequences, or control flow markers.
   363  //
   364  // A further pass is required on the result to turn it into an AST.
   365  func (p *parser) parseTemplateParts(end TokenType) (*templateParts, hcl.Diagnostics) {
   366  	var parts []templateToken
   367  	var diags hcl.Diagnostics
   368  
   369  	startRange := p.NextRange()
   370  	ltrimNext := false
   371  	nextCanTrimPrev := false
   372  	var endRange hcl.Range
   373  
   374  Token:
   375  	for {
   376  		next := p.Read()
   377  		if next.Type == end {
   378  			// all done!
   379  			endRange = next.Range
   380  			break
   381  		}
   382  
   383  		ltrim := ltrimNext
   384  		ltrimNext = false
   385  		canTrimPrev := nextCanTrimPrev
   386  		nextCanTrimPrev = false
   387  
   388  		switch next.Type {
   389  		case TokenStringLit, TokenQuotedLit:
   390  			str, strDiags := ParseStringLiteralToken(next)
   391  			diags = append(diags, strDiags...)
   392  
   393  			if ltrim {
   394  				str = strings.TrimLeftFunc(str, unicode.IsSpace)
   395  			}
   396  
   397  			parts = append(parts, &templateLiteralToken{
   398  				Val:      str,
   399  				SrcRange: next.Range,
   400  			})
   401  			nextCanTrimPrev = true
   402  
   403  		case TokenTemplateInterp:
   404  			// if the opener is ${~ then we want to eat any trailing whitespace
   405  			// in the preceding literal token, assuming it is indeed a literal
   406  			// token.
   407  			if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
   408  				prevExpr := parts[len(parts)-1]
   409  				if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
   410  					lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
   411  				}
   412  			}
   413  
   414  			p.PushIncludeNewlines(false)
   415  			expr, exprDiags := p.ParseExpression()
   416  			diags = append(diags, exprDiags...)
   417  			close := p.Peek()
   418  			if close.Type != TokenTemplateSeqEnd {
   419  				if !p.recovery {
   420  					switch close.Type {
   421  					case TokenEOF:
   422  						diags = append(diags, &hcl.Diagnostic{
   423  							Severity: hcl.DiagError,
   424  							Summary:  "Unclosed template interpolation sequence",
   425  							Detail:   "There is no closing brace for this interpolation sequence before the end of the file. This might be caused by incorrect nesting inside the given expression.",
   426  							Subject:  &startRange,
   427  						})
   428  					case TokenColon:
   429  						diags = append(diags, &hcl.Diagnostic{
   430  							Severity: hcl.DiagError,
   431  							Summary:  "Extra characters after interpolation expression",
   432  							Detail:   "Template interpolation doesn't expect a colon at this location. Did you intend this to be a literal sequence to be processed as part of another language? If so, you can escape it by starting with \"$${\" instead of just \"${\".",
   433  							Subject:  &close.Range,
   434  							Context:  hcl.RangeBetween(startRange, close.Range).Ptr(),
   435  						})
   436  					default:
   437  						if (close.Type == TokenCQuote || close.Type == TokenOQuote) && end == TokenCQuote {
   438  							// We'll get here if we're processing a _quoted_
   439  							// template and we find an errant quote inside an
   440  							// interpolation sequence, which suggests that
   441  							// the interpolation sequence is missing its terminator.
   442  							diags = append(diags, &hcl.Diagnostic{
   443  								Severity: hcl.DiagError,
   444  								Summary:  "Unclosed template interpolation sequence",
   445  								Detail:   "There is no closing brace for this interpolation sequence before the end of the quoted template. This might be caused by incorrect nesting inside the given expression.",
   446  								Subject:  &startRange,
   447  							})
   448  						} else {
   449  							diags = append(diags, &hcl.Diagnostic{
   450  								Severity: hcl.DiagError,
   451  								Summary:  "Extra characters after interpolation expression",
   452  								Detail:   "Expected a closing brace to end the interpolation expression, but found extra characters.\n\nThis can happen when you include interpolation syntax for another language, such as shell scripting, but forget to escape the interpolation start token. If this is an embedded sequence for another language, escape it by starting with \"$${\" instead of just \"${\".",
   453  								Subject:  &close.Range,
   454  								Context:  hcl.RangeBetween(startRange, close.Range).Ptr(),
   455  							})
   456  						}
   457  					}
   458  				}
   459  				p.recover(TokenTemplateSeqEnd)
   460  			} else {
   461  				p.Read() // eat closing brace
   462  
   463  				// If the closer is ~} then we want to eat any leading
   464  				// whitespace on the next token, if it turns out to be a
   465  				// literal token.
   466  				if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
   467  					ltrimNext = true
   468  				}
   469  			}
   470  			p.PopIncludeNewlines()
   471  			parts = append(parts, &templateInterpToken{
   472  				Expr:     expr,
   473  				SrcRange: hcl.RangeBetween(next.Range, close.Range),
   474  			})
   475  
   476  		case TokenTemplateControl:
   477  			// if the opener is %{~ then we want to eat any trailing whitespace
   478  			// in the preceding literal token, assuming it is indeed a literal
   479  			// token.
   480  			if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
   481  				prevExpr := parts[len(parts)-1]
   482  				if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
   483  					lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
   484  				}
   485  			}
   486  			p.PushIncludeNewlines(false)
   487  
   488  			kw := p.Peek()
   489  			if kw.Type != TokenIdent {
   490  				if !p.recovery {
   491  					diags = append(diags, &hcl.Diagnostic{
   492  						Severity: hcl.DiagError,
   493  						Summary:  "Invalid template directive",
   494  						Detail:   "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a %{ sequence.",
   495  						Subject:  &kw.Range,
   496  						Context:  hcl.RangeBetween(next.Range, kw.Range).Ptr(),
   497  					})
   498  				}
   499  				p.recover(TokenTemplateSeqEnd)
   500  				p.PopIncludeNewlines()
   501  				continue Token
   502  			}
   503  			p.Read() // eat keyword token
   504  
   505  			switch {
   506  
   507  			case ifKeyword.TokenMatches(kw):
   508  				condExpr, exprDiags := p.ParseExpression()
   509  				diags = append(diags, exprDiags...)
   510  				parts = append(parts, &templateIfToken{
   511  					CondExpr: condExpr,
   512  					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
   513  				})
   514  
   515  			case elseKeyword.TokenMatches(kw):
   516  				parts = append(parts, &templateEndCtrlToken{
   517  					Type:     templateElse,
   518  					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
   519  				})
   520  
   521  			case endifKeyword.TokenMatches(kw):
   522  				parts = append(parts, &templateEndCtrlToken{
   523  					Type:     templateEndIf,
   524  					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
   525  				})
   526  
   527  			case forKeyword.TokenMatches(kw):
   528  				var keyName, valName string
   529  				if p.Peek().Type != TokenIdent {
   530  					if !p.recovery {
   531  						diags = append(diags, &hcl.Diagnostic{
   532  							Severity: hcl.DiagError,
   533  							Summary:  "Invalid 'for' directive",
   534  							Detail:   "For directive requires variable name after 'for'.",
   535  							Subject:  p.Peek().Range.Ptr(),
   536  						})
   537  					}
   538  					p.recover(TokenTemplateSeqEnd)
   539  					p.PopIncludeNewlines()
   540  					continue Token
   541  				}
   542  
   543  				valName = string(p.Read().Bytes)
   544  
   545  				if p.Peek().Type == TokenComma {
   546  					// What we just read was actually the key, then.
   547  					keyName = valName
   548  					p.Read() // eat comma
   549  
   550  					if p.Peek().Type != TokenIdent {
   551  						if !p.recovery {
   552  							diags = append(diags, &hcl.Diagnostic{
   553  								Severity: hcl.DiagError,
   554  								Summary:  "Invalid 'for' directive",
   555  								Detail:   "For directive requires value variable name after comma.",
   556  								Subject:  p.Peek().Range.Ptr(),
   557  							})
   558  						}
   559  						p.recover(TokenTemplateSeqEnd)
   560  						p.PopIncludeNewlines()
   561  						continue Token
   562  					}
   563  
   564  					valName = string(p.Read().Bytes)
   565  				}
   566  
   567  				if !inKeyword.TokenMatches(p.Peek()) {
   568  					if !p.recovery {
   569  						diags = append(diags, &hcl.Diagnostic{
   570  							Severity: hcl.DiagError,
   571  							Summary:  "Invalid 'for' directive",
   572  							Detail:   "For directive requires 'in' keyword after names.",
   573  							Subject:  p.Peek().Range.Ptr(),
   574  						})
   575  					}
   576  					p.recover(TokenTemplateSeqEnd)
   577  					p.PopIncludeNewlines()
   578  					continue Token
   579  				}
   580  				p.Read() // eat 'in' keyword
   581  
   582  				collExpr, collDiags := p.ParseExpression()
   583  				diags = append(diags, collDiags...)
   584  				parts = append(parts, &templateForToken{
   585  					KeyVar:   keyName,
   586  					ValVar:   valName,
   587  					CollExpr: collExpr,
   588  
   589  					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
   590  				})
   591  
   592  			case endforKeyword.TokenMatches(kw):
   593  				parts = append(parts, &templateEndCtrlToken{
   594  					Type:     templateEndFor,
   595  					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
   596  				})
   597  
   598  			default:
   599  				if !p.recovery {
   600  					suggestions := []string{"if", "for", "else", "endif", "endfor"}
   601  					given := string(kw.Bytes)
   602  					suggestion := nameSuggestion(given, suggestions)
   603  					if suggestion != "" {
   604  						suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
   605  					}
   606  
   607  					diags = append(diags, &hcl.Diagnostic{
   608  						Severity: hcl.DiagError,
   609  						Summary:  "Invalid template control keyword",
   610  						Detail:   fmt.Sprintf("%q is not a valid template control keyword.%s", given, suggestion),
   611  						Subject:  &kw.Range,
   612  						Context:  hcl.RangeBetween(next.Range, kw.Range).Ptr(),
   613  					})
   614  				}
   615  				p.recover(TokenTemplateSeqEnd)
   616  				p.PopIncludeNewlines()
   617  				continue Token
   618  
   619  			}
   620  
   621  			close := p.Peek()
   622  			if close.Type != TokenTemplateSeqEnd {
   623  				if !p.recovery {
   624  					diags = append(diags, &hcl.Diagnostic{
   625  						Severity: hcl.DiagError,
   626  						Summary:  fmt.Sprintf("Extra characters in %s marker", kw.Bytes),
   627  						Detail:   "Expected a closing brace to end the sequence, but found extra characters.",
   628  						Subject:  &close.Range,
   629  						Context:  hcl.RangeBetween(startRange, close.Range).Ptr(),
   630  					})
   631  				}
   632  				p.recover(TokenTemplateSeqEnd)
   633  			} else {
   634  				p.Read() // eat closing brace
   635  
   636  				// If the closer is ~} then we want to eat any leading
   637  				// whitespace on the next token, if it turns out to be a
   638  				// literal token.
   639  				if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
   640  					ltrimNext = true
   641  				}
   642  			}
   643  			p.PopIncludeNewlines()
   644  
   645  		default:
   646  			if !p.recovery {
   647  				diags = append(diags, &hcl.Diagnostic{
   648  					Severity: hcl.DiagError,
   649  					Summary:  "Unterminated template string",
   650  					Detail:   "No closing marker was found for the string.",
   651  					Subject:  &next.Range,
   652  					Context:  hcl.RangeBetween(startRange, next.Range).Ptr(),
   653  				})
   654  			}
   655  			final := p.recover(end)
   656  			endRange = final.Range
   657  			break Token
   658  		}
   659  	}
   660  
   661  	if len(parts) == 0 {
   662  		// If a sequence has no content, we'll treat it as if it had an
   663  		// empty string in it because that's what the user probably means
   664  		// if they write "" in configuration.
   665  		parts = append(parts, &templateLiteralToken{
   666  			Val: "",
   667  			SrcRange: hcl.Range{
   668  				// Range is the zero-character span immediately after the
   669  				// opening quote.
   670  				Filename: startRange.Filename,
   671  				Start:    startRange.End,
   672  				End:      startRange.End,
   673  			},
   674  		})
   675  	}
   676  
   677  	// Always end with an end token, so the parser can produce diagnostics
   678  	// about unclosed items with proper position information.
   679  	parts = append(parts, &templateEndToken{
   680  		SrcRange: endRange,
   681  	})
   682  
   683  	ret := &templateParts{
   684  		Tokens:   parts,
   685  		SrcRange: hcl.RangeBetween(startRange, endRange),
   686  	}
   687  
   688  	return ret, diags
   689  }
   690  
   691  // flushHeredocTemplateParts modifies in-place the line-leading literal strings
   692  // to apply the flush heredoc processing rule: find the line with the smallest
   693  // number of whitespace characters as prefix and then trim that number of
   694  // characters from all of the lines.
   695  //
   696  // This rule is applied to static tokens rather than to the rendered result,
   697  // so interpolating a string with leading whitespace cannot affect the chosen
   698  // prefix length.
   699  func flushHeredocTemplateParts(parts *templateParts) {
   700  	if len(parts.Tokens) == 0 {
   701  		// Nothing to do
   702  		return
   703  	}
   704  
   705  	const maxInt = int((^uint(0)) >> 1)
   706  
   707  	minSpaces := maxInt
   708  	newline := true
   709  	var adjust []*templateLiteralToken
   710  	for _, ttok := range parts.Tokens {
   711  		if newline {
   712  			newline = false
   713  			var spaces int
   714  			if lit, ok := ttok.(*templateLiteralToken); ok {
   715  				orig := lit.Val
   716  				trimmed := strings.TrimLeftFunc(orig, unicode.IsSpace)
   717  				// If a token is entirely spaces and ends with a newline
   718  				// then it's a "blank line" and thus not considered for
   719  				// space-prefix-counting purposes.
   720  				if len(trimmed) == 0 && strings.HasSuffix(orig, "\n") {
   721  					spaces = maxInt
   722  				} else {
   723  					spaceBytes := len(lit.Val) - len(trimmed)
   724  					spaces, _ = textseg.TokenCount([]byte(orig[:spaceBytes]), textseg.ScanGraphemeClusters)
   725  					adjust = append(adjust, lit)
   726  				}
   727  			} else if _, ok := ttok.(*templateEndToken); ok {
   728  				break // don't process the end token since it never has spaces before it
   729  			}
   730  			if spaces < minSpaces {
   731  				minSpaces = spaces
   732  			}
   733  		}
   734  		if lit, ok := ttok.(*templateLiteralToken); ok {
   735  			if strings.HasSuffix(lit.Val, "\n") {
   736  				newline = true // The following token, if any, begins a new line
   737  			}
   738  		}
   739  	}
   740  
   741  	for _, lit := range adjust {
   742  		// Since we want to count space _characters_ rather than space _bytes_,
   743  		// we can't just do a straightforward slice operation here and instead
   744  		// need to hunt for the split point with a scanner.
   745  		valBytes := []byte(lit.Val)
   746  		spaceByteCount := 0
   747  		for i := 0; i < minSpaces; i++ {
   748  			adv, _, _ := textseg.ScanGraphemeClusters(valBytes, true)
   749  			spaceByteCount += adv
   750  			valBytes = valBytes[adv:]
   751  		}
   752  		lit.Val = lit.Val[spaceByteCount:]
   753  		lit.SrcRange.Start.Column += minSpaces
   754  		lit.SrcRange.Start.Byte += spaceByteCount
   755  	}
   756  }
   757  
   758  // meldConsecutiveStringLiterals simplifies the AST output by combining a
   759  // sequence of string literal tokens into a single string literal. This must be
   760  // performed after any whitespace trimming operations.
   761  func meldConsecutiveStringLiterals(parts *templateParts) {
   762  	if len(parts.Tokens) == 0 {
   763  		return
   764  	}
   765  
   766  	// Loop over all tokens starting at the second element, as we want to join
   767  	// pairs of consecutive string literals.
   768  	i := 1
   769  	for i < len(parts.Tokens) {
   770  		if prevLiteral, ok := parts.Tokens[i-1].(*templateLiteralToken); ok {
   771  			if literal, ok := parts.Tokens[i].(*templateLiteralToken); ok {
   772  				// The current and previous tokens are both literals: combine
   773  				prevLiteral.Val = prevLiteral.Val + literal.Val
   774  				prevLiteral.SrcRange.End = literal.SrcRange.End
   775  
   776  				// Remove the current token from the slice
   777  				parts.Tokens = append(parts.Tokens[:i], parts.Tokens[i+1:]...)
   778  
   779  				// Continue without moving forward in the slice
   780  				continue
   781  			}
   782  		}
   783  
   784  		// Try the next pair of tokens
   785  		i++
   786  	}
   787  }
   788  
   789  type templateParts struct {
   790  	Tokens   []templateToken
   791  	SrcRange hcl.Range
   792  }
   793  
   794  // templateToken is a higher-level token that represents a single atom within
   795  // the template language. Our template parsing first raises the raw token
   796  // stream to a sequence of templateToken, and then transforms the result into
   797  // an expression tree.
   798  type templateToken interface {
   799  	templateToken() templateToken
   800  }
   801  
   802  type templateLiteralToken struct {
   803  	Val      string
   804  	SrcRange hcl.Range
   805  	isTemplateToken
   806  }
   807  
   808  type templateInterpToken struct {
   809  	Expr     Expression
   810  	SrcRange hcl.Range
   811  	isTemplateToken
   812  }
   813  
   814  type templateIfToken struct {
   815  	CondExpr Expression
   816  	SrcRange hcl.Range
   817  	isTemplateToken
   818  }
   819  
   820  type templateForToken struct {
   821  	KeyVar   string // empty if ignoring key
   822  	ValVar   string
   823  	CollExpr Expression
   824  	SrcRange hcl.Range
   825  	isTemplateToken
   826  }
   827  
   828  type templateEndCtrlType int
   829  
   830  const (
   831  	templateEndIf templateEndCtrlType = iota
   832  	templateElse
   833  	templateEndFor
   834  )
   835  
   836  type templateEndCtrlToken struct {
   837  	Type     templateEndCtrlType
   838  	SrcRange hcl.Range
   839  	isTemplateToken
   840  }
   841  
   842  func (t *templateEndCtrlToken) Name() string {
   843  	switch t.Type {
   844  	case templateEndIf:
   845  		return "endif"
   846  	case templateElse:
   847  		return "else"
   848  	case templateEndFor:
   849  		return "endfor"
   850  	default:
   851  		// should never happen
   852  		panic("invalid templateEndCtrlType")
   853  	}
   854  }
   855  
   856  type templateEndToken struct {
   857  	SrcRange hcl.Range
   858  	isTemplateToken
   859  }
   860  
   861  type isTemplateToken [0]int
   862  
   863  func (t isTemplateToken) templateToken() templateToken {
   864  	return t
   865  }