pulumi/pkg/codegen/hcl2/syntax/comments.go
Pat Gavlin 8fded5f29f
[codegen/hcl2] Fix template control tokens. (#4584)
Token detection was broken for conditional and for expressions that
represent template control sequences. The code originally attempted to
determine whether or not a conditional or for expression was a control
sequence by inspecting the expression's parent. Unfortunately, that
approach is unable to distinguish between expressions that are control
sequences and those that are merely template parts. These changes
instead inspect the first token of the expression for a template control
token (i.e. `%{`): if such a token is found, the expression is detected
as a template control sequence.
2020-05-07 09:12:00 -07:00

830 lines
26 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package syntax
import (
"bytes"
"regexp"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/pulumi/pulumi/pkg/v2/codegen"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
)
// tokenList is a list of Tokens with methods to aid in mapping source positions to tokens.
type tokenList []Token
// offsetIndex returns the index of the token that contains the given byte offset or -1 if no such token exists.
func (l tokenList) offsetIndex(offset int) int {
base := 0
for len(l) > 0 {
i := len(l) / 2
r := l[i].Range()
switch {
case offset < r.Start.Byte:
l = l[:i]
case r.Start.Byte <= offset && offset < r.End.Byte:
return base + i
case r.End.Byte <= offset:
l, base = l[i+1:], base+i+1
default:
contract.Failf("unexpected index condition: %v, %v, %v", r.Start.Byte, r.End.Byte, offset)
}
}
return -1
}
// atOffset returns the token that contains the given byte offset or the zero value if no such token exists.
func (l tokenList) atOffset(offset int) Token {
if i := l.offsetIndex(offset); i >= 0 {
return l[i]
}
return Token{}
}
// atPos returns the token that contains the given hcl.Pos or the zero value if no such token exists.
func (l tokenList) atPos(p hcl.Pos) Token {
return l.atOffset(p.Byte)
}
// inRange returns a slice of the tokens that cover the given range or nil if either the start or end position is
// uncovered by a token.
func (l tokenList) inRange(r hcl.Range) []Token {
// If the range is empty, ignore it.
if r.Empty() {
return nil
}
// Find the index of the start and end tokens for this range.
start, end := l.offsetIndex(r.Start.Byte), l.offsetIndex(r.End.Byte-1)
if start == -1 || end == -1 {
return nil
}
return l[start : end+1]
}
// A TokenMap is used to map from syntax nodes to information about their tokens and leading whitespace/comments.
type TokenMap interface {
ForNode(n hclsyntax.Node) NodeTokens
isTokenMap()
}
type tokenMap map[hclsyntax.Node]NodeTokens
// ForNode returns the token information for the given node, if any.
func (m tokenMap) ForNode(n hclsyntax.Node) NodeTokens {
return m[n]
}
func (tokenMap) isTokenMap() {}
// NewTokenMapForFiles creates a new token map that can be used to look up tokens for nodes in any of the given files.
func NewTokenMapForFiles(files []*File) TokenMap {
tokens := tokenMap{}
for _, f := range files {
for node, ts := range f.Tokens.(tokenMap) {
tokens[node] = ts
}
}
return tokens
}
type tokenMapper struct {
tokenMap tokenMap
tokens tokenList
stack []hclsyntax.Node
templateControlExprs codegen.Set
}
func (m *tokenMapper) getParent() (hclsyntax.Node, bool) {
if len(m.stack) < 2 {
return nil, false
}
return m.stack[len(m.stack)-2], true
}
func (m *tokenMapper) isSingleFunctionCallArg() bool {
parent, ok := m.getParent()
if !ok {
return false
}
call, ok := parent.(*hclsyntax.FunctionCallExpr)
return ok && len(call.Args) == 1
}
func (m *tokenMapper) inTemplateControl() bool {
if len(m.stack) < 2 {
return false
}
parent := m.stack[len(m.stack)-2]
return m.templateControlExprs.Has(parent)
}
func (m *tokenMapper) collectLabelTokens(r hcl.Range) Token {
tokens := m.tokens.inRange(r)
if len(tokens) != 3 {
return m.tokens.atPos(r.Start)
}
open := tokens[0]
if open.Raw.Type != hclsyntax.TokenOQuote || len(open.TrailingTrivia) != 0 {
return m.tokens.atPos(r.Start)
}
str := tokens[1]
if str.Raw.Type != hclsyntax.TokenQuotedLit || len(str.LeadingTrivia) != 0 || len(str.TrailingTrivia) != 0 {
return m.tokens.atPos(r.Start)
}
close := tokens[2]
if close.Raw.Type != hclsyntax.TokenCQuote || len(close.LeadingTrivia) != 0 {
return m.tokens.atPos(r.Start)
}
return Token{
Raw: hclsyntax.Token{
Type: hclsyntax.TokenQuotedLit,
Bytes: append(append(open.Raw.Bytes, str.Raw.Bytes...), close.Raw.Bytes...),
Range: hcl.Range{
Filename: open.Raw.Range.Filename,
Start: open.Raw.Range.Start,
End: close.Raw.Range.End,
},
},
LeadingTrivia: open.LeadingTrivia,
TrailingTrivia: close.TrailingTrivia,
}
}
func (m *tokenMapper) mapRelativeTraversalTokens(traversal hcl.Traversal) []TraverserTokens {
if len(traversal) == 0 {
return nil
}
contract.Assert(traversal.IsRelative())
items := make([]TraverserTokens, len(traversal))
for i, t := range traversal {
rng := t.SourceRange()
leadingToken := m.tokens.atPos(rng.Start)
indexToken := m.tokens.atOffset(rng.Start.Byte + 1)
if leadingToken.Raw.Type == hclsyntax.TokenOBrack {
if indexToken.Raw.Type == hclsyntax.TokenOQuote {
indexToken = m.collectLabelTokens(hcl.Range{
Filename: rng.Filename,
Start: hcl.Pos{Byte: rng.Start.Byte + 1},
End: hcl.Pos{Byte: rng.End.Byte - 1},
})
}
items[i] = &BracketTraverserTokens{
OpenBracket: leadingToken,
Index: indexToken,
CloseBracket: m.tokens.atOffset(rng.End.Byte - 1),
}
} else {
items[i] = &DotTraverserTokens{
Dot: leadingToken,
Index: indexToken,
}
}
}
return items
}
func (m *tokenMapper) nodeRange(n hclsyntax.Node) hcl.Range {
tokens, ok := m.tokenMap[n]
if !ok {
return n.Range()
}
filename := n.Range().Filename
var parens Parentheses
var start, end hcl.Pos
switch n := n.(type) {
case *hclsyntax.Attribute:
tokens := tokens.(*AttributeTokens)
start, end = tokens.Name.Range().Start, m.nodeRange(n.Expr).End
case *hclsyntax.BinaryOpExpr:
tokens := tokens.(*BinaryOpTokens)
parens = tokens.Parentheses
start, end = m.nodeRange(n.LHS).Start, m.nodeRange(n.RHS).End
case *hclsyntax.Block:
tokens := tokens.(*BlockTokens)
start, end = tokens.Type.Range().Start, tokens.CloseBrace.Range().End
case *hclsyntax.ConditionalExpr:
switch tokens := tokens.(type) {
case *ConditionalTokens:
parens = tokens.Parentheses
start, end = m.nodeRange(n.Condition).Start, m.nodeRange(n.FalseResult).End
case *TemplateConditionalTokens:
start, end = tokens.OpenIf.Range().Start, tokens.CloseEndif.Range().End
}
case *hclsyntax.ForExpr:
switch tokens := tokens.(type) {
case *ForTokens:
parens = tokens.Parentheses
start, end = tokens.Open.Range().Start, tokens.Close.Range().End
case *TemplateForTokens:
start, end = tokens.OpenFor.Range().Start, tokens.CloseEndfor.Range().End
}
case *hclsyntax.FunctionCallExpr:
tokens := tokens.(*FunctionCallTokens)
parens = tokens.Parentheses
start, end = tokens.Name.Range().Start, tokens.CloseParen.Range().End
case *hclsyntax.IndexExpr:
tokens := tokens.(*IndexTokens)
parens = tokens.Parentheses
start, end = m.nodeRange(n.Collection).Start, tokens.CloseBracket.Range().End
case *hclsyntax.LiteralValueExpr, *hclsyntax.ObjectConsKeyExpr:
tokens := tokens.(*LiteralValueTokens)
parens = tokens.Parentheses
start, end = tokens.Value[0].Range().Start, tokens.Value[len(tokens.Value)-1].Range().End
case *hclsyntax.ObjectConsExpr:
tokens := tokens.(*ObjectConsTokens)
parens = tokens.Parentheses
start, end = tokens.OpenBrace.Range().Start, tokens.CloseBrace.Range().End
case *hclsyntax.RelativeTraversalExpr:
tokens := tokens.(*RelativeTraversalTokens)
parens = tokens.Parentheses
start, end = m.nodeRange(n.Source).Start, tokens.Traversal[len(tokens.Traversal)-1].Range().End
case *hclsyntax.ScopeTraversalExpr:
tokens := tokens.(*ScopeTraversalTokens)
parens = tokens.Parentheses
start, end = tokens.Root.Range().Start, tokens.Root.Range().End
if len(tokens.Traversal) > 0 {
end = tokens.Traversal[len(tokens.Traversal)-1].Range().End
}
case *hclsyntax.SplatExpr:
tokens := tokens.(*SplatTokens)
parens = tokens.Parentheses
start, end = m.nodeRange(n.Source).Start, m.nodeRange(n.Each).End
case *hclsyntax.TemplateExpr:
tokens := tokens.(*TemplateTokens)
parens = tokens.Parentheses
start, end = tokens.Open.Range().Start, tokens.Close.Range().End
case *hclsyntax.TemplateWrapExpr:
tokens := tokens.(*TemplateTokens)
parens = tokens.Parentheses
start, end = tokens.Open.Range().Start, tokens.Close.Range().End
case *hclsyntax.TupleConsExpr:
tokens := tokens.(*TupleConsTokens)
parens = tokens.Parentheses
start, end = tokens.OpenBracket.Range().Start, tokens.CloseBracket.Range().End
case *hclsyntax.UnaryOpExpr:
tokens := tokens.(*UnaryOpTokens)
parens = tokens.Parentheses
start, end = tokens.Operator.Range().Start, m.nodeRange(n.Val).End
}
return exprRange(filename, parens, start, end)
}
func (m *tokenMapper) Enter(n hclsyntax.Node) hcl.Diagnostics {
switch n := n.(type) {
case hclsyntax.Attributes, hclsyntax.Blocks, hclsyntax.ChildScope:
// Do nothing
default:
m.stack = append(m.stack, n)
}
switch n := n.(type) {
case *hclsyntax.ConditionalExpr:
open := m.tokens.atPos(n.SrcRange.Start)
if open.Raw.Type == hclsyntax.TokenTemplateControl {
m.templateControlExprs.Add(n)
}
case *hclsyntax.ForExpr:
open := m.tokens.atPos(n.OpenRange.Start)
if open.Raw.Type == hclsyntax.TokenTemplateControl {
m.templateControlExprs.Add(n)
}
}
// Work around a bug in the HCL syntax library. The walkChildNodes implementation for ObjectConsKeyExpr does not
// descend into its wrapped expression if the wrapped expression can be interpreted as a keyword even if the
// syntactical form that _forces_ the wrapped expression to be a non-literal is used.
if x, ok := n.(*hclsyntax.ObjectConsKeyExpr); ok && x.ForceNonLiteral && hcl.ExprAsKeyword(x.Wrapped) != "" {
return hclsyntax.Walk(x.Wrapped, m)
}
return nil
}
func (m *tokenMapper) Exit(n hclsyntax.Node) hcl.Diagnostics {
// Gather parentheses.
var parens Parentheses
startParens, endParens := n.Range().Start.Byte-1, n.Range().End.Byte
for {
open, close := m.tokens.atOffset(startParens), m.tokens.atOffset(endParens)
if open.Raw.Type != hclsyntax.TokenOParen || close.Raw.Type != hclsyntax.TokenCParen {
break
}
parens.Open, parens.Close = append(parens.Open, open), append(parens.Close, close)
startParens, endParens = open.Range().Start.Byte-1, close.Range().End.Byte
}
if m.isSingleFunctionCallArg() && len(parens.Open) > 0 {
parens.Open, parens.Close = parens.Open[:len(parens.Open)-1], parens.Close[:len(parens.Close)-1]
}
var nodeTokens NodeTokens
switch n := n.(type) {
case *hclsyntax.Attribute:
nodeTokens = &AttributeTokens{
Name: m.tokens.atPos(n.NameRange.Start),
Equals: m.tokens.atPos(n.EqualsRange.Start),
}
case *hclsyntax.BinaryOpExpr:
nodeTokens = &BinaryOpTokens{
Parentheses: parens,
Operator: m.tokens.atPos(m.nodeRange(n.LHS).End),
}
case *hclsyntax.Block:
labels := make([]Token, len(n.Labels))
for i, r := range n.LabelRanges {
labels[i] = m.collectLabelTokens(r)
}
nodeTokens = &BlockTokens{
Type: m.tokens.atPos(n.TypeRange.Start),
Labels: labels,
OpenBrace: m.tokens.atPos(n.OpenBraceRange.Start),
CloseBrace: m.tokens.atPos(n.CloseBraceRange.Start),
}
case *hclsyntax.ConditionalExpr:
condRange := m.nodeRange(n.Condition)
trueRange := m.nodeRange(n.TrueResult)
falseRange := m.nodeRange(n.FalseResult)
if m.templateControlExprs.Has(n) {
condition := m.tokens.atPos(condRange.Start)
ift := m.tokens.atOffset(condition.Range().Start.Byte - 1)
openIf := m.tokens.atOffset(ift.Range().Start.Byte - 1)
closeIf := m.tokens.atPos(condRange.End)
openEndifOrElse := m.tokens.atPos(trueRange.End)
endifOrElse := m.tokens.atPos(openEndifOrElse.Range().End)
closeEndifOrElse := m.tokens.atPos(endifOrElse.Range().End)
var openElse, elset, closeElse *Token
if endifOrElse.Raw.Type == hclsyntax.TokenIdent && string(endifOrElse.Raw.Bytes) == "else" {
open, t, close := openEndifOrElse, endifOrElse, closeEndifOrElse
openElse, elset, closeElse = &open, &t, &close
openEndifOrElse = m.tokens.atPos(falseRange.End)
endifOrElse = m.tokens.atPos(openEndifOrElse.Range().End)
closeEndifOrElse = m.tokens.atPos(endifOrElse.Range().End)
}
nodeTokens = &TemplateConditionalTokens{
OpenIf: openIf,
If: ift,
CloseIf: closeIf,
OpenElse: openElse,
Else: elset,
CloseElse: closeElse,
OpenEndif: openEndifOrElse,
Endif: endifOrElse,
CloseEndif: closeEndifOrElse,
}
} else {
nodeTokens = &ConditionalTokens{
Parentheses: parens,
QuestionMark: m.tokens.atPos(condRange.End),
Colon: m.tokens.atPos(trueRange.End),
}
}
case *hclsyntax.ForExpr:
openToken := m.tokens.atPos(n.OpenRange.Start)
forToken := m.tokens.atPos(openToken.Range().End)
var keyToken, commaToken *Token
var valueToken Token
if n.KeyVar != "" {
key := m.tokens.atPos(forToken.Range().End)
comma := m.tokens.atPos(key.Range().End)
value := m.tokens.atPos(comma.Range().End)
keyToken, commaToken, valueToken = &key, &comma, value
} else {
valueToken = m.tokens.atPos(forToken.Range().End)
}
var arrowToken *Token
if n.KeyExpr != nil {
arrow := m.tokens.atPos(m.nodeRange(n.KeyExpr).End)
arrowToken = &arrow
}
valRange := m.nodeRange(n.ValExpr)
var groupToken *Token
if n.Group {
group := m.tokens.atPos(valRange.End)
groupToken = &group
}
var ifToken *Token
if n.CondExpr != nil {
pos := valRange.End
if groupToken != nil {
pos = groupToken.Range().End
}
ift := m.tokens.atPos(pos)
ifToken = &ift
}
if m.templateControlExprs.Has(n) {
closeFor := m.tokens.atPos(m.nodeRange(n.CollExpr).End)
openEndfor := m.tokens.atPos(valRange.End)
endfor := m.tokens.atPos(openEndfor.Range().End)
closeEndfor := m.tokens.atPos(endfor.Range().End)
nodeTokens = &TemplateForTokens{
OpenFor: openToken,
For: forToken,
Key: keyToken,
Comma: commaToken,
Value: valueToken,
In: m.tokens.atPos(valueToken.Range().End),
CloseFor: closeFor,
OpenEndfor: openEndfor,
Endfor: endfor,
CloseEndfor: closeEndfor,
}
} else {
nodeTokens = &ForTokens{
Parentheses: parens,
Open: openToken,
For: forToken,
Key: keyToken,
Comma: commaToken,
Value: valueToken,
In: m.tokens.atPos(valueToken.Range().End),
Colon: m.tokens.atPos(m.nodeRange(n.CollExpr).End),
Arrow: arrowToken,
Group: groupToken,
If: ifToken,
Close: m.tokens.atPos(n.CloseRange.Start),
}
}
case *hclsyntax.FunctionCallExpr:
args := n.Args
commas := make([]Token, 0, len(args))
if len(args) > 0 {
for _, ex := range args[:len(args)-1] {
commas = append(commas, m.tokens.atPos(m.nodeRange(ex).End))
}
if trailing := m.tokens.atPos(m.nodeRange(args[len(args)-1]).End); trailing.Raw.Type == hclsyntax.TokenComma {
commas = append(commas, trailing)
}
}
nodeTokens = &FunctionCallTokens{
Parentheses: parens,
Name: m.tokens.atPos(n.NameRange.Start),
OpenParen: m.tokens.atPos(n.OpenParenRange.Start),
Commas: commas,
CloseParen: m.tokens.atPos(n.CloseParenRange.Start),
}
case *hclsyntax.IndexExpr:
nodeTokens = &IndexTokens{
Parentheses: parens,
OpenBracket: m.tokens.atPos(n.OpenRange.Start),
CloseBracket: m.tokens.atOffset(n.BracketRange.End.Byte - 1),
}
case *hclsyntax.LiteralValueExpr:
nodeTokens = &LiteralValueTokens{
Parentheses: parens,
Value: m.tokens.inRange(n.Range()),
}
case *hclsyntax.ObjectConsKeyExpr:
nodeTokens = &LiteralValueTokens{
Parentheses: parens,
Value: m.tokens.inRange(n.Range()),
}
case *hclsyntax.ObjectConsExpr:
items := make([]ObjectConsItemTokens, len(n.Items))
for i, item := range n.Items {
var comma *Token
if t := m.tokens.atPos(m.nodeRange(item.ValueExpr).End); t.Raw.Type == hclsyntax.TokenComma {
comma = &t
}
items[i] = ObjectConsItemTokens{
Equals: m.tokens.atPos(m.nodeRange(item.KeyExpr).End),
Comma: comma,
}
}
nodeTokens = &ObjectConsTokens{
Parentheses: parens,
OpenBrace: m.tokens.atPos(n.OpenRange.Start),
Items: items,
CloseBrace: m.tokens.atOffset(n.SrcRange.End.Byte - 1),
}
case *hclsyntax.RelativeTraversalExpr:
nodeTokens = &RelativeTraversalTokens{
Parentheses: parens,
Traversal: m.mapRelativeTraversalTokens(n.Traversal),
}
case *hclsyntax.ScopeTraversalExpr:
nodeTokens = &ScopeTraversalTokens{
Parentheses: parens,
Root: m.tokens.atPos(n.Traversal[0].SourceRange().Start),
Traversal: m.mapRelativeTraversalTokens(n.Traversal[1:]),
}
case *hclsyntax.SplatExpr:
openToken := m.tokens.atPos(m.nodeRange(n.Source).End)
starToken := m.tokens.atPos(openToken.Range().End)
var closeToken *Token
if openToken.Raw.Type == hclsyntax.TokenOBrack {
cbrack := m.tokens.atPos(starToken.Range().End)
closeToken = &cbrack
}
nodeTokens = &SplatTokens{
Parentheses: parens,
Open: openToken,
Star: starToken,
Close: closeToken,
}
case *hclsyntax.TemplateExpr:
// NOTE: the HCL parser lifts control sequences into if or for expressions. This is handled in the correspoding
// mappers.
if m.inTemplateControl() {
nodeTokens = &TemplateTokens{
Open: Token{
Raw: hclsyntax.Token{
Range: hcl.Range{
Filename: n.SrcRange.Filename,
Start: n.SrcRange.Start,
End: n.SrcRange.Start,
},
},
},
Close: Token{
Raw: hclsyntax.Token{
Range: hcl.Range{
Filename: n.SrcRange.Filename,
Start: n.SrcRange.End,
End: n.SrcRange.End,
},
},
},
}
} else {
// If we're inside the body of a template if or for, there cannot be any delimiting tokens.
nodeTokens = &TemplateTokens{
Parentheses: parens,
Open: m.tokens.atPos(n.SrcRange.Start),
Close: m.tokens.atOffset(n.SrcRange.End.Byte - 1),
}
}
case *hclsyntax.TemplateWrapExpr:
nodeTokens = &TemplateTokens{
Parentheses: parens,
Open: m.tokens.atPos(n.SrcRange.Start),
Close: m.tokens.atOffset(n.SrcRange.End.Byte - 1),
}
case *hclsyntax.TupleConsExpr:
exprs := n.Exprs
commas := make([]Token, 0, len(exprs))
if len(exprs) > 0 {
for _, ex := range exprs[:len(exprs)-1] {
commas = append(commas, m.tokens.atPos(m.nodeRange(ex).End))
}
if trailing := m.tokens.atPos(m.nodeRange(exprs[len(exprs)-1]).End); trailing.Raw.Type == hclsyntax.TokenComma {
commas = append(commas, trailing)
}
}
nodeTokens = &TupleConsTokens{
Parentheses: parens,
OpenBracket: m.tokens.atPos(n.OpenRange.Start),
Commas: commas,
CloseBracket: m.tokens.atOffset(n.SrcRange.End.Byte - 1),
}
case *hclsyntax.UnaryOpExpr:
nodeTokens = &UnaryOpTokens{
Parentheses: parens,
Operator: m.tokens.atPos(n.SymbolRange.Start),
}
}
if nodeTokens != nil {
m.tokenMap[n] = nodeTokens
}
if n == m.stack[len(m.stack)-1] {
m.stack = m.stack[:len(m.stack)-1]
}
return nil
}
// mapTokens builds a mapping from the syntax nodes in the given source file to their tokens. The mapping is recorded
// in the map passed in to the function.
func mapTokens(rawTokens hclsyntax.Tokens, filename string, root hclsyntax.Node, contents []byte, tokenMap tokenMap,
initialPos hcl.Pos) {
// Turn the list of raw tokens into a list of trivia-carrying tokens.
lastEndPos := initialPos
var tokens tokenList
trivia := TriviaList{}
inControlSeq := false
for _, raw := range rawTokens {
// Snip whitespace out of the body and turn it in to trivia.
if startPos := raw.Range.Start; startPos.Byte != lastEndPos.Byte {
triviaBytes := contents[lastEndPos.Byte-initialPos.Byte : startPos.Byte-initialPos.Byte]
// If this trivia begins a new line, attach the current trivia to the last processed token, if any.
if len(tokens) > 0 {
if nl := bytes.IndexByte(triviaBytes, '\n'); nl != -1 {
trailingTriviaBytes := triviaBytes[:nl+1]
triviaBytes = triviaBytes[nl+1:]
endPos := hcl.Pos{Line: lastEndPos.Line + 1, Column: 0, Byte: lastEndPos.Byte + nl + 1}
rng := hcl.Range{Filename: filename, Start: lastEndPos, End: endPos}
trivia = append(trivia, Whitespace{rng: rng, bytes: trailingTriviaBytes})
tokens[len(tokens)-1].TrailingTrivia, trivia = trivia, TriviaList{}
lastEndPos = endPos
}
}
rng := hcl.Range{Filename: filename, Start: lastEndPos, End: startPos}
trivia = append(trivia, Whitespace{rng: rng, bytes: triviaBytes})
}
switch raw.Type {
case hclsyntax.TokenComment:
trivia = append(trivia, Comment{Lines: processComment(raw.Bytes), rng: raw.Range, bytes: raw.Bytes})
case hclsyntax.TokenTemplateInterp:
// Treat these as trailing trivia.
trivia = append(trivia, TemplateDelimiter{Type: raw.Type, rng: raw.Range, bytes: raw.Bytes})
if len(tokens) > 0 {
tokens[len(tokens)-1].TrailingTrivia, trivia = trivia, TriviaList{}
}
case hclsyntax.TokenTemplateControl:
tokens, trivia = append(tokens, Token{Raw: raw, LeadingTrivia: trivia}), TriviaList{}
inControlSeq = true
case hclsyntax.TokenTemplateSeqEnd:
// If this terminates a template control sequence, it is a proper token. Otherwise, it is treated as leading
// trivia.
if !inControlSeq {
tokens[len(tokens)-1].TrailingTrivia = trivia
trivia = TriviaList{TemplateDelimiter{Type: raw.Type, rng: raw.Range, bytes: raw.Bytes}}
} else {
tokens, trivia = append(tokens, Token{Raw: raw, LeadingTrivia: trivia}), TriviaList{}
}
inControlSeq = false
case hclsyntax.TokenNewline, hclsyntax.TokenBitwiseAnd, hclsyntax.TokenBitwiseOr,
hclsyntax.TokenBitwiseNot, hclsyntax.TokenBitwiseXor, hclsyntax.TokenStarStar, hclsyntax.TokenApostrophe,
hclsyntax.TokenBacktick, hclsyntax.TokenSemicolon, hclsyntax.TokenTabs, hclsyntax.TokenInvalid,
hclsyntax.TokenBadUTF8, hclsyntax.TokenQuotedNewline:
// Treat these as whitespace. We cannot omit their bytes from the list entirely, as the algorithm below
// that maps positions to tokens requires that every byte in the source file is covered by the token list.
continue
default:
tokens, trivia = append(tokens, Token{Raw: raw, LeadingTrivia: trivia}), TriviaList{}
}
lastEndPos = raw.Range.End
}
// If we had any tokens, we should have attached all trivia to something.
contract.Assert(len(trivia) == 0 || len(tokens) == 0)
// Now build the token map.
//
// If a node records the ranges of relevant tokens in its syntax node, the start position of those ranges is used
// to fetch the corresponding token.
//
// If a node does not record the range of a relevant token, the end position of a preceding element (e.g. an
// expression or a token) is used to find the token.
//
// TODO(pdg): handle parenthesized expressions
diags := hclsyntax.Walk(root, &tokenMapper{
tokenMap: tokenMap,
tokens: tokens,
templateControlExprs: codegen.Set{},
})
contract.Assert(diags == nil)
// If the root was a Body and there is a trailing end-of-file token, attach it to the body.
body, isBody := root.(*hclsyntax.Body)
if isBody && len(tokens) > 0 && tokens[len(tokens)-1].Raw.Type == hclsyntax.TokenEOF {
tokenMap[body] = &BodyTokens{EndOfFile: &tokens[len(tokens)-1]}
}
}
// processComment separates the given comment into lines and attempts to remove comment tokens.
func processComment(bytes []byte) []string {
comment := string(bytes)
// Each HCL comment may be either a line comment or a block comment. Line comments start with '#' or '//' and
// terminate with an EOL. Block comments begin with a '/*' and terminate with a '*/'. All comment delimiters are
// preserved in the HCL comment text.
//
// To make life easier for the code generators, HCL comments are pre-processed to remove comment delimiters. For
// line comments, this process is trivial. For block comments, things are a bit more involved.
switch {
case comment[0] == '#':
return []string{strings.TrimSuffix(comment[1:], "\n")}
case comment[0:2] == "//":
return []string{strings.TrimSuffix(comment[2:], "\n")}
default:
return processBlockComment(comment)
}
}
// These regexes are used by processBlockComment. The first matches a block comment start, the second a block comment
// end, and the third a block comment line prefix.
var blockStartPat = regexp.MustCompile(`^/\*+`)
var blockEndPat = regexp.MustCompile(`[[:space:]]*\*+/$`)
var blockPrefixPat = regexp.MustCompile(`^[[:space:]]*\*`)
// processBlockComment splits a block comment into mutiple lines, removes comment delimiters, and attempts to remove
// common comment prefixes from interior lines. For example, the following HCL block comment:
//
// /**
// * This is a block comment!
// *
// * It has multiple lines.
// */
//
// becomes this set of lines:
//
// []string{" This is a block comment!", "", " It has multiple lines."}
//
func processBlockComment(text string) []string {
lines := strings.Split(text, "\n")
// We will always trim the following:
// - '^/\*+' from the first line
// - a trailing '[[:space:]]\*+/$' from the last line
// In addition, we will trim '^[[:space:]]*\*' from the second through last lines iff all lines in that set share
// a prefix that matches that pattern.
prefix := ""
for i, l := range lines[1:] {
m := blockPrefixPat.FindString(l)
if i == 0 {
prefix = m
} else if m != prefix {
prefix = ""
break
}
}
for i, l := range lines {
switch i {
case 0:
start := blockStartPat.FindString(l)
contract.Assert(start != "")
l = l[len(start):]
// If this is a single-line block comment, trim the end pattern as well.
if len(lines) == 1 {
contract.Assert(prefix == "")
if end := blockEndPat.FindString(l); end != "" {
l = l[:len(l)-len(end)]
}
}
case len(lines) - 1:
// The prefix we're trimming may overlap with the end pattern we're trimming. In this case, trim the entire
// line.
if len(l)-len(prefix) == 1 {
l = ""
} else {
l = l[len(prefix):]
if end := blockEndPat.FindString(l); end != "" {
l = l[:len(l)-len(end)]
}
}
default:
// Trim the prefix.
l = l[len(prefix):]
}
lines[i] = l
}
// If the first or last line is empty, drop it.
if lines[0] == "" {
lines = lines[1:]
}
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}