2055 lines
58 KiB
Go
2055 lines
58 KiB
Go
package hclsyntax
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strconv"
|
|
"unicode/utf8"
|
|
|
|
"github.com/apparentlymart/go-textseg/textseg"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type parser struct {
|
|
*peeker
|
|
|
|
// set to true if any recovery is attempted. The parser can use this
|
|
// to attempt to reduce error noise by suppressing "bad token" errors
|
|
// in recovery mode, assuming that the recovery heuristics have failed
|
|
// in this case and left the peeker in a wrong place.
|
|
recovery bool
|
|
}
|
|
|
|
func (p *parser) ParseBody(end TokenType) (*Body, hcl.Diagnostics) {
|
|
attrs := Attributes{}
|
|
blocks := Blocks{}
|
|
var diags hcl.Diagnostics
|
|
|
|
startRange := p.PrevRange()
|
|
var endRange hcl.Range
|
|
|
|
Token:
|
|
for {
|
|
next := p.Peek()
|
|
if next.Type == end {
|
|
endRange = p.NextRange()
|
|
p.Read()
|
|
break Token
|
|
}
|
|
|
|
switch next.Type {
|
|
case TokenNewline:
|
|
p.Read()
|
|
continue
|
|
case TokenIdent:
|
|
item, itemDiags := p.ParseBodyItem()
|
|
diags = append(diags, itemDiags...)
|
|
switch titem := item.(type) {
|
|
case *Block:
|
|
blocks = append(blocks, titem)
|
|
case *Attribute:
|
|
if existing, exists := attrs[titem.Name]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Attribute redefined",
|
|
Detail: fmt.Sprintf(
|
|
"The argument %q was already set at %s. Each argument may be set only once.",
|
|
titem.Name, existing.NameRange.String(),
|
|
),
|
|
Subject: &titem.NameRange,
|
|
})
|
|
} else {
|
|
attrs[titem.Name] = titem
|
|
}
|
|
default:
|
|
// This should never happen for valid input, but may if a
|
|
// syntax error was detected in ParseBodyItem that prevented
|
|
// it from even producing a partially-broken item. In that
|
|
// case, it would've left at least one error in the diagnostics
|
|
// slice we already dealt with above.
|
|
//
|
|
// We'll assume ParseBodyItem attempted recovery to leave
|
|
// us in a reasonable position to try parsing the next item.
|
|
continue
|
|
}
|
|
default:
|
|
bad := p.Read()
|
|
if !p.recovery {
|
|
if bad.Type == TokenOQuote {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid argument name",
|
|
Detail: "Argument names must not be quoted.",
|
|
Subject: &bad.Range,
|
|
})
|
|
} else {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Argument or block definition required",
|
|
Detail: "An argument or block definition is required here.",
|
|
Subject: &bad.Range,
|
|
})
|
|
}
|
|
}
|
|
endRange = p.PrevRange() // arbitrary, but somewhere inside the body means better diagnostics
|
|
|
|
p.recover(end) // attempt to recover to the token after the end of this body
|
|
break Token
|
|
}
|
|
}
|
|
|
|
return &Body{
|
|
Attributes: attrs,
|
|
Blocks: blocks,
|
|
|
|
SrcRange: hcl.RangeBetween(startRange, endRange),
|
|
EndRange: hcl.Range{
|
|
Filename: endRange.Filename,
|
|
Start: endRange.End,
|
|
End: endRange.End,
|
|
},
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) ParseBodyItem() (Node, hcl.Diagnostics) {
|
|
ident := p.Read()
|
|
if ident.Type != TokenIdent {
|
|
p.recoverAfterBodyItem()
|
|
return nil, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Argument or block definition required",
|
|
Detail: "An argument or block definition is required here.",
|
|
Subject: &ident.Range,
|
|
},
|
|
}
|
|
}
|
|
|
|
next := p.Peek()
|
|
|
|
switch next.Type {
|
|
case TokenEqual:
|
|
return p.finishParsingBodyAttribute(ident, false)
|
|
case TokenOQuote, TokenOBrace, TokenIdent:
|
|
return p.finishParsingBodyBlock(ident)
|
|
default:
|
|
p.recoverAfterBodyItem()
|
|
return nil, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Argument or block definition required",
|
|
Detail: "An argument or block definition is required here. To set an argument, use the equals sign \"=\" to introduce the argument value.",
|
|
Subject: &ident.Range,
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// parseSingleAttrBody is a weird variant of ParseBody that deals with the
|
|
// body of a nested block containing only one attribute value all on a single
|
|
// line, like foo { bar = baz } . It expects to find a single attribute item
|
|
// immediately followed by the end token type with no intervening newlines.
|
|
func (p *parser) parseSingleAttrBody(end TokenType) (*Body, hcl.Diagnostics) {
|
|
ident := p.Read()
|
|
if ident.Type != TokenIdent {
|
|
p.recoverAfterBodyItem()
|
|
return nil, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Argument or block definition required",
|
|
Detail: "An argument or block definition is required here.",
|
|
Subject: &ident.Range,
|
|
},
|
|
}
|
|
}
|
|
|
|
var attr *Attribute
|
|
var diags hcl.Diagnostics
|
|
|
|
next := p.Peek()
|
|
|
|
switch next.Type {
|
|
case TokenEqual:
|
|
node, attrDiags := p.finishParsingBodyAttribute(ident, true)
|
|
diags = append(diags, attrDiags...)
|
|
attr = node.(*Attribute)
|
|
case TokenOQuote, TokenOBrace, TokenIdent:
|
|
p.recoverAfterBodyItem()
|
|
return nil, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Argument definition required",
|
|
Detail: fmt.Sprintf("A single-line block definition can contain only a single argument. If you meant to define argument %q, use an equals sign to assign it a value. To define a nested block, place it on a line of its own within its parent block.", ident.Bytes),
|
|
Subject: hcl.RangeBetween(ident.Range, next.Range).Ptr(),
|
|
},
|
|
}
|
|
default:
|
|
p.recoverAfterBodyItem()
|
|
return nil, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Argument or block definition required",
|
|
Detail: "An argument or block definition is required here. To set an argument, use the equals sign \"=\" to introduce the argument value.",
|
|
Subject: &ident.Range,
|
|
},
|
|
}
|
|
}
|
|
|
|
return &Body{
|
|
Attributes: Attributes{
|
|
string(ident.Bytes): attr,
|
|
},
|
|
|
|
SrcRange: attr.SrcRange,
|
|
EndRange: hcl.Range{
|
|
Filename: attr.SrcRange.Filename,
|
|
Start: attr.SrcRange.End,
|
|
End: attr.SrcRange.End,
|
|
},
|
|
}, diags
|
|
|
|
}
|
|
|
|
func (p *parser) finishParsingBodyAttribute(ident Token, singleLine bool) (Node, hcl.Diagnostics) {
|
|
eqTok := p.Read() // eat equals token
|
|
if eqTok.Type != TokenEqual {
|
|
// should never happen if caller behaves
|
|
panic("finishParsingBodyAttribute called with next not equals")
|
|
}
|
|
|
|
var endRange hcl.Range
|
|
|
|
expr, diags := p.ParseExpression()
|
|
if p.recovery && diags.HasErrors() {
|
|
// recovery within expressions tends to be tricky, so we've probably
|
|
// landed somewhere weird. We'll try to reset to the start of a body
|
|
// item so parsing can continue.
|
|
endRange = p.PrevRange()
|
|
p.recoverAfterBodyItem()
|
|
} else {
|
|
endRange = p.PrevRange()
|
|
if !singleLine {
|
|
end := p.Peek()
|
|
if end.Type != TokenNewline && end.Type != TokenEOF {
|
|
if !p.recovery {
|
|
summary := "Missing newline after argument"
|
|
detail := "An argument definition must end with a newline."
|
|
|
|
if end.Type == TokenComma {
|
|
summary = "Unexpected comma after argument"
|
|
detail = "Argument definitions must be separated by newlines, not commas. " + detail
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: summary,
|
|
Detail: detail,
|
|
Subject: &end.Range,
|
|
Context: hcl.RangeBetween(ident.Range, end.Range).Ptr(),
|
|
})
|
|
}
|
|
endRange = p.PrevRange()
|
|
p.recoverAfterBodyItem()
|
|
} else {
|
|
endRange = p.PrevRange()
|
|
p.Read() // eat newline
|
|
}
|
|
}
|
|
}
|
|
|
|
return &Attribute{
|
|
Name: string(ident.Bytes),
|
|
Expr: expr,
|
|
|
|
SrcRange: hcl.RangeBetween(ident.Range, endRange),
|
|
NameRange: ident.Range,
|
|
EqualsRange: eqTok.Range,
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) finishParsingBodyBlock(ident Token) (Node, hcl.Diagnostics) {
|
|
var blockType = string(ident.Bytes)
|
|
var diags hcl.Diagnostics
|
|
var labels []string
|
|
var labelRanges []hcl.Range
|
|
|
|
var oBrace Token
|
|
|
|
Token:
|
|
for {
|
|
tok := p.Peek()
|
|
|
|
switch tok.Type {
|
|
|
|
case TokenOBrace:
|
|
oBrace = p.Read()
|
|
break Token
|
|
|
|
case TokenOQuote:
|
|
label, labelRange, labelDiags := p.parseQuotedStringLiteral()
|
|
diags = append(diags, labelDiags...)
|
|
labels = append(labels, label)
|
|
labelRanges = append(labelRanges, labelRange)
|
|
// parseQuoteStringLiteral recovers up to the closing quote
|
|
// if it encounters problems, so we can continue looking for
|
|
// more labels and eventually the block body even.
|
|
|
|
case TokenIdent:
|
|
tok = p.Read() // eat token
|
|
label, labelRange := string(tok.Bytes), tok.Range
|
|
labels = append(labels, label)
|
|
labelRanges = append(labelRanges, labelRange)
|
|
|
|
default:
|
|
switch tok.Type {
|
|
case TokenEqual:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block definition",
|
|
Detail: "The equals sign \"=\" indicates an argument definition, and must not be used when defining a block.",
|
|
Subject: &tok.Range,
|
|
Context: hcl.RangeBetween(ident.Range, tok.Range).Ptr(),
|
|
})
|
|
case TokenNewline:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block definition",
|
|
Detail: "A block definition must have block content delimited by \"{\" and \"}\", starting on the same line as the block header.",
|
|
Subject: &tok.Range,
|
|
Context: hcl.RangeBetween(ident.Range, tok.Range).Ptr(),
|
|
})
|
|
default:
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block definition",
|
|
Detail: "Either a quoted string block label or an opening brace (\"{\") is expected here.",
|
|
Subject: &tok.Range,
|
|
Context: hcl.RangeBetween(ident.Range, tok.Range).Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
p.recoverAfterBodyItem()
|
|
|
|
return &Block{
|
|
Type: blockType,
|
|
Labels: labels,
|
|
Body: &Body{
|
|
SrcRange: ident.Range,
|
|
EndRange: ident.Range,
|
|
},
|
|
|
|
TypeRange: ident.Range,
|
|
LabelRanges: labelRanges,
|
|
OpenBraceRange: ident.Range, // placeholder
|
|
CloseBraceRange: ident.Range, // placeholder
|
|
}, diags
|
|
}
|
|
}
|
|
|
|
// Once we fall out here, the peeker is pointed just after our opening
|
|
// brace, so we can begin our nested body parsing.
|
|
var body *Body
|
|
var bodyDiags hcl.Diagnostics
|
|
switch p.Peek().Type {
|
|
case TokenNewline, TokenEOF, TokenCBrace:
|
|
body, bodyDiags = p.ParseBody(TokenCBrace)
|
|
default:
|
|
// Special one-line, single-attribute block parsing mode.
|
|
body, bodyDiags = p.parseSingleAttrBody(TokenCBrace)
|
|
switch p.Peek().Type {
|
|
case TokenCBrace:
|
|
p.Read() // the happy path - just consume the closing brace
|
|
case TokenComma:
|
|
// User seems to be trying to use the object-constructor
|
|
// comma-separated style, which isn't permitted for blocks.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid single-argument block definition",
|
|
Detail: "Single-line block syntax can include only one argument definition. To define multiple arguments, use the multi-line block syntax with one argument definition per line.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
})
|
|
p.recover(TokenCBrace)
|
|
case TokenNewline:
|
|
// We don't allow weird mixtures of single and multi-line syntax.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid single-argument block definition",
|
|
Detail: "An argument definition on the same line as its containing block creates a single-line block definition, which must also be closed on the same line. Place the block's closing brace immediately after the argument definition.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
})
|
|
p.recover(TokenCBrace)
|
|
default:
|
|
// Some other weird thing is going on. Since we can't guess a likely
|
|
// user intent for this one, we'll skip it if we're already in
|
|
// recovery mode.
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid single-argument block definition",
|
|
Detail: "A single-line block definition must end with a closing brace immediately after its single argument definition.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
})
|
|
}
|
|
p.recover(TokenCBrace)
|
|
}
|
|
}
|
|
diags = append(diags, bodyDiags...)
|
|
cBraceRange := p.PrevRange()
|
|
|
|
eol := p.Peek()
|
|
if eol.Type == TokenNewline || eol.Type == TokenEOF {
|
|
p.Read() // eat newline
|
|
} else {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing newline after block definition",
|
|
Detail: "A block definition must end with a newline.",
|
|
Subject: &eol.Range,
|
|
Context: hcl.RangeBetween(ident.Range, eol.Range).Ptr(),
|
|
})
|
|
}
|
|
p.recoverAfterBodyItem()
|
|
}
|
|
|
|
// We must never produce a nil body, since the caller may attempt to
|
|
// do analysis of a partial result when there's an error, so we'll
|
|
// insert a placeholder if we otherwise failed to produce a valid
|
|
// body due to one of the syntax error paths above.
|
|
if body == nil && diags.HasErrors() {
|
|
body = &Body{
|
|
SrcRange: hcl.RangeBetween(oBrace.Range, cBraceRange),
|
|
EndRange: cBraceRange,
|
|
}
|
|
}
|
|
|
|
return &Block{
|
|
Type: blockType,
|
|
Labels: labels,
|
|
Body: body,
|
|
|
|
TypeRange: ident.Range,
|
|
LabelRanges: labelRanges,
|
|
OpenBraceRange: oBrace.Range,
|
|
CloseBraceRange: cBraceRange,
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) ParseExpression() (Expression, hcl.Diagnostics) {
|
|
return p.parseTernaryConditional()
|
|
}
|
|
|
|
func (p *parser) parseTernaryConditional() (Expression, hcl.Diagnostics) {
|
|
// The ternary conditional operator (.. ? .. : ..) behaves somewhat
|
|
// like a binary operator except that the "symbol" is itself
|
|
// an expression enclosed in two punctuation characters.
|
|
// The middle expression is parsed as if the ? and : symbols
|
|
// were parentheses. The "rhs" (the "false expression") is then
|
|
// treated right-associatively so it behaves similarly to the
|
|
// middle in terms of precedence.
|
|
|
|
startRange := p.NextRange()
|
|
var condExpr, trueExpr, falseExpr Expression
|
|
var diags hcl.Diagnostics
|
|
|
|
condExpr, condDiags := p.parseBinaryOps(binaryOps)
|
|
diags = append(diags, condDiags...)
|
|
if p.recovery && condDiags.HasErrors() {
|
|
return condExpr, diags
|
|
}
|
|
|
|
questionMark := p.Peek()
|
|
if questionMark.Type != TokenQuestion {
|
|
return condExpr, diags
|
|
}
|
|
|
|
p.Read() // eat question mark
|
|
|
|
trueExpr, trueDiags := p.ParseExpression()
|
|
diags = append(diags, trueDiags...)
|
|
if p.recovery && trueDiags.HasErrors() {
|
|
return condExpr, diags
|
|
}
|
|
|
|
colon := p.Peek()
|
|
if colon.Type != TokenColon {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing false expression in conditional",
|
|
Detail: "The conditional operator (...?...:...) requires a false expression, delimited by a colon.",
|
|
Subject: &colon.Range,
|
|
Context: hcl.RangeBetween(startRange, colon.Range).Ptr(),
|
|
})
|
|
return condExpr, diags
|
|
}
|
|
|
|
p.Read() // eat colon
|
|
|
|
falseExpr, falseDiags := p.ParseExpression()
|
|
diags = append(diags, falseDiags...)
|
|
if p.recovery && falseDiags.HasErrors() {
|
|
return condExpr, diags
|
|
}
|
|
|
|
return &ConditionalExpr{
|
|
Condition: condExpr,
|
|
TrueResult: trueExpr,
|
|
FalseResult: falseExpr,
|
|
|
|
SrcRange: hcl.RangeBetween(startRange, falseExpr.Range()),
|
|
}, diags
|
|
}
|
|
|
|
// parseBinaryOps calls itself recursively to work through all of the
|
|
// operator precedence groups, and then eventually calls parseExpressionTerm
|
|
// for each operand.
|
|
func (p *parser) parseBinaryOps(ops []map[TokenType]*Operation) (Expression, hcl.Diagnostics) {
|
|
if len(ops) == 0 {
|
|
// We've run out of operators, so now we'll just try to parse a term.
|
|
return p.parseExpressionWithTraversals()
|
|
}
|
|
|
|
thisLevel := ops[0]
|
|
remaining := ops[1:]
|
|
|
|
var lhs, rhs Expression
|
|
var operation *Operation
|
|
var diags hcl.Diagnostics
|
|
|
|
// Parse a term that might be the first operand of a binary
|
|
// operation or it might just be a standalone term.
|
|
// We won't know until we've parsed it and can look ahead
|
|
// to see if there's an operator token for this level.
|
|
lhs, lhsDiags := p.parseBinaryOps(remaining)
|
|
diags = append(diags, lhsDiags...)
|
|
if p.recovery && lhsDiags.HasErrors() {
|
|
return lhs, diags
|
|
}
|
|
|
|
// We'll keep eating up operators until we run out, so that operators
|
|
// with the same precedence will combine in a left-associative manner:
|
|
// a+b+c => (a+b)+c, not a+(b+c)
|
|
//
|
|
// Should we later want to have right-associative operators, a way
|
|
// to achieve that would be to call back up to ParseExpression here
|
|
// instead of iteratively parsing only the remaining operators.
|
|
for {
|
|
next := p.Peek()
|
|
var newOp *Operation
|
|
var ok bool
|
|
if newOp, ok = thisLevel[next.Type]; !ok {
|
|
break
|
|
}
|
|
|
|
// Are we extending an expression started on the previous iteration?
|
|
if operation != nil {
|
|
lhs = &BinaryOpExpr{
|
|
LHS: lhs,
|
|
Op: operation,
|
|
RHS: rhs,
|
|
|
|
SrcRange: hcl.RangeBetween(lhs.Range(), rhs.Range()),
|
|
}
|
|
}
|
|
|
|
operation = newOp
|
|
p.Read() // eat operator token
|
|
var rhsDiags hcl.Diagnostics
|
|
rhs, rhsDiags = p.parseBinaryOps(remaining)
|
|
diags = append(diags, rhsDiags...)
|
|
if p.recovery && rhsDiags.HasErrors() {
|
|
return lhs, diags
|
|
}
|
|
}
|
|
|
|
if operation == nil {
|
|
return lhs, diags
|
|
}
|
|
|
|
return &BinaryOpExpr{
|
|
LHS: lhs,
|
|
Op: operation,
|
|
RHS: rhs,
|
|
|
|
SrcRange: hcl.RangeBetween(lhs.Range(), rhs.Range()),
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) parseExpressionWithTraversals() (Expression, hcl.Diagnostics) {
|
|
term, diags := p.parseExpressionTerm()
|
|
ret, moreDiags := p.parseExpressionTraversals(term)
|
|
diags = append(diags, moreDiags...)
|
|
return ret, diags
|
|
}
|
|
|
|
func (p *parser) parseExpressionTraversals(from Expression) (Expression, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
ret := from
|
|
|
|
Traversal:
|
|
for {
|
|
next := p.Peek()
|
|
|
|
switch next.Type {
|
|
case TokenDot:
|
|
// Attribute access or splat
|
|
dot := p.Read()
|
|
attrTok := p.Peek()
|
|
|
|
switch attrTok.Type {
|
|
case TokenIdent:
|
|
attrTok = p.Read() // eat token
|
|
name := string(attrTok.Bytes)
|
|
rng := hcl.RangeBetween(dot.Range, attrTok.Range)
|
|
step := hcl.TraverseAttr{
|
|
Name: name,
|
|
SrcRange: rng,
|
|
}
|
|
|
|
ret = makeRelativeTraversal(ret, step, rng)
|
|
|
|
case TokenNumberLit:
|
|
// This is a weird form we inherited from HIL, allowing numbers
|
|
// to be used as attributes as a weird way of writing [n].
|
|
// This was never actually a first-class thing in HIL, but
|
|
// HIL tolerated sequences like .0. in its variable names and
|
|
// calling applications like Terraform exploited that to
|
|
// introduce indexing syntax where none existed.
|
|
numTok := p.Read() // eat token
|
|
attrTok = numTok
|
|
|
|
// This syntax is ambiguous if multiple indices are used in
|
|
// succession, like foo.0.1.baz: that actually parses as
|
|
// a fractional number 0.1. Since we're only supporting this
|
|
// syntax for compatibility with legacy Terraform
|
|
// configurations, and Terraform does not tend to have lists
|
|
// of lists, we'll choose to reject that here with a helpful
|
|
// error message, rather than failing later because the index
|
|
// isn't a whole number.
|
|
if dotIdx := bytes.IndexByte(numTok.Bytes, '.'); dotIdx >= 0 {
|
|
first := numTok.Bytes[:dotIdx]
|
|
second := numTok.Bytes[dotIdx+1:]
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid legacy index syntax",
|
|
Detail: fmt.Sprintf("When using the legacy index syntax, chaining two indexes together is not permitted. Use the proper index syntax instead, like [%s][%s].", first, second),
|
|
Subject: &attrTok.Range,
|
|
})
|
|
rng := hcl.RangeBetween(dot.Range, numTok.Range)
|
|
step := hcl.TraverseIndex{
|
|
Key: cty.DynamicVal,
|
|
SrcRange: rng,
|
|
}
|
|
ret = makeRelativeTraversal(ret, step, rng)
|
|
break
|
|
}
|
|
|
|
numVal, numDiags := p.numberLitValue(numTok)
|
|
diags = append(diags, numDiags...)
|
|
|
|
rng := hcl.RangeBetween(dot.Range, numTok.Range)
|
|
step := hcl.TraverseIndex{
|
|
Key: numVal,
|
|
SrcRange: rng,
|
|
}
|
|
|
|
ret = makeRelativeTraversal(ret, step, rng)
|
|
|
|
case TokenStar:
|
|
// "Attribute-only" splat expression.
|
|
// (This is a kinda weird construct inherited from HIL, which
|
|
// behaves a bit like a [*] splat except that it is only able
|
|
// to do attribute traversals into each of its elements,
|
|
// whereas foo[*] can support _any_ traversal.
|
|
marker := p.Read() // eat star
|
|
trav := make(hcl.Traversal, 0, 1)
|
|
var firstRange, lastRange hcl.Range
|
|
firstRange = p.NextRange()
|
|
for p.Peek().Type == TokenDot {
|
|
dot := p.Read()
|
|
|
|
if p.Peek().Type == TokenNumberLit {
|
|
// Continuing the "weird stuff inherited from HIL"
|
|
// theme, we also allow numbers as attribute names
|
|
// inside splats and interpret them as indexing
|
|
// into a list, for expressions like:
|
|
// foo.bar.*.baz.0.foo
|
|
numTok := p.Read()
|
|
|
|
// Weird special case if the user writes something
|
|
// like foo.bar.*.baz.0.0.foo, where 0.0 parses
|
|
// as a number.
|
|
if dotIdx := bytes.IndexByte(numTok.Bytes, '.'); dotIdx >= 0 {
|
|
first := numTok.Bytes[:dotIdx]
|
|
second := numTok.Bytes[dotIdx+1:]
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid legacy index syntax",
|
|
Detail: fmt.Sprintf("When using the legacy index syntax, chaining two indexes together is not permitted. Use the proper index syntax with a full splat expression [*] instead, like [%s][%s].", first, second),
|
|
Subject: &attrTok.Range,
|
|
})
|
|
trav = append(trav, hcl.TraverseIndex{
|
|
Key: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(dot.Range, numTok.Range),
|
|
})
|
|
lastRange = numTok.Range
|
|
continue
|
|
}
|
|
|
|
numVal, numDiags := p.numberLitValue(numTok)
|
|
diags = append(diags, numDiags...)
|
|
trav = append(trav, hcl.TraverseIndex{
|
|
Key: numVal,
|
|
SrcRange: hcl.RangeBetween(dot.Range, numTok.Range),
|
|
})
|
|
lastRange = numTok.Range
|
|
continue
|
|
}
|
|
|
|
if p.Peek().Type != TokenIdent {
|
|
if !p.recovery {
|
|
if p.Peek().Type == TokenStar {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Nested splat expression not allowed",
|
|
Detail: "A splat expression (*) cannot be used inside another attribute-only splat expression.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
})
|
|
} else {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid attribute name",
|
|
Detail: "An attribute name is required after a dot.",
|
|
Subject: &attrTok.Range,
|
|
})
|
|
}
|
|
}
|
|
p.setRecovery()
|
|
continue Traversal
|
|
}
|
|
|
|
attrTok := p.Read()
|
|
trav = append(trav, hcl.TraverseAttr{
|
|
Name: string(attrTok.Bytes),
|
|
SrcRange: hcl.RangeBetween(dot.Range, attrTok.Range),
|
|
})
|
|
lastRange = attrTok.Range
|
|
}
|
|
|
|
itemExpr := &AnonSymbolExpr{
|
|
SrcRange: hcl.RangeBetween(dot.Range, marker.Range),
|
|
}
|
|
var travExpr Expression
|
|
if len(trav) == 0 {
|
|
travExpr = itemExpr
|
|
} else {
|
|
travExpr = &RelativeTraversalExpr{
|
|
Source: itemExpr,
|
|
Traversal: trav,
|
|
SrcRange: hcl.RangeBetween(firstRange, lastRange),
|
|
}
|
|
}
|
|
|
|
ret = &SplatExpr{
|
|
Source: ret,
|
|
Each: travExpr,
|
|
Item: itemExpr,
|
|
|
|
SrcRange: hcl.RangeBetween(dot.Range, lastRange),
|
|
MarkerRange: hcl.RangeBetween(dot.Range, marker.Range),
|
|
}
|
|
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid attribute name",
|
|
Detail: "An attribute name is required after a dot.",
|
|
Subject: &attrTok.Range,
|
|
})
|
|
// This leaves the peeker in a bad place, so following items
|
|
// will probably be misparsed until we hit something that
|
|
// allows us to re-sync.
|
|
//
|
|
// We will probably need to do something better here eventually
|
|
// in order to support autocomplete triggered by typing a
|
|
// period.
|
|
p.setRecovery()
|
|
}
|
|
|
|
case TokenOBrack:
|
|
// Indexing of a collection.
|
|
// This may or may not be a hcl.Traverser, depending on whether
|
|
// the key value is something constant.
|
|
|
|
open := p.Read()
|
|
switch p.Peek().Type {
|
|
case TokenStar:
|
|
// This is a full splat expression, like foo[*], which consumes
|
|
// the rest of the traversal steps after it using a recursive
|
|
// call to this function.
|
|
p.Read() // consume star
|
|
close := p.Read()
|
|
if close.Type != TokenCBrack && !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing close bracket on splat index",
|
|
Detail: "The star for a full splat operator must be immediately followed by a closing bracket (\"]\").",
|
|
Subject: &close.Range,
|
|
})
|
|
close = p.recover(TokenCBrack)
|
|
}
|
|
// Splat expressions use a special "anonymous symbol" as a
|
|
// placeholder in an expression to be evaluated once for each
|
|
// item in the source expression.
|
|
itemExpr := &AnonSymbolExpr{
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}
|
|
// Now we'll recursively call this same function to eat any
|
|
// remaining traversal steps against the anonymous symbol.
|
|
travExpr, nestedDiags := p.parseExpressionTraversals(itemExpr)
|
|
diags = append(diags, nestedDiags...)
|
|
|
|
ret = &SplatExpr{
|
|
Source: ret,
|
|
Each: travExpr,
|
|
Item: itemExpr,
|
|
|
|
SrcRange: hcl.RangeBetween(open.Range, travExpr.Range()),
|
|
MarkerRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}
|
|
|
|
default:
|
|
|
|
var close Token
|
|
p.PushIncludeNewlines(false) // arbitrary newlines allowed in brackets
|
|
keyExpr, keyDiags := p.ParseExpression()
|
|
diags = append(diags, keyDiags...)
|
|
if p.recovery && keyDiags.HasErrors() {
|
|
close = p.recover(TokenCBrack)
|
|
} else {
|
|
close = p.Read()
|
|
if close.Type != TokenCBrack && !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing close bracket on index",
|
|
Detail: "The index operator must end with a closing bracket (\"]\").",
|
|
Subject: &close.Range,
|
|
})
|
|
close = p.recover(TokenCBrack)
|
|
}
|
|
}
|
|
p.PopIncludeNewlines()
|
|
|
|
if lit, isLit := keyExpr.(*LiteralValueExpr); isLit {
|
|
litKey, _ := lit.Value(nil)
|
|
rng := hcl.RangeBetween(open.Range, close.Range)
|
|
step := hcl.TraverseIndex{
|
|
Key: litKey,
|
|
SrcRange: rng,
|
|
}
|
|
ret = makeRelativeTraversal(ret, step, rng)
|
|
} else if tmpl, isTmpl := keyExpr.(*TemplateExpr); isTmpl && tmpl.IsStringLiteral() {
|
|
litKey, _ := tmpl.Value(nil)
|
|
rng := hcl.RangeBetween(open.Range, close.Range)
|
|
step := hcl.TraverseIndex{
|
|
Key: litKey,
|
|
SrcRange: rng,
|
|
}
|
|
ret = makeRelativeTraversal(ret, step, rng)
|
|
} else {
|
|
rng := hcl.RangeBetween(open.Range, close.Range)
|
|
ret = &IndexExpr{
|
|
Collection: ret,
|
|
Key: keyExpr,
|
|
|
|
SrcRange: rng,
|
|
OpenRange: open.Range,
|
|
}
|
|
}
|
|
}
|
|
|
|
default:
|
|
break Traversal
|
|
}
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
// makeRelativeTraversal takes an expression and a traverser and returns
|
|
// a traversal expression that combines the two. If the given expression
|
|
// is already a traversal, it is extended in place (mutating it) and
|
|
// returned. If it isn't, a new RelativeTraversalExpr is created and returned.
|
|
func makeRelativeTraversal(expr Expression, next hcl.Traverser, rng hcl.Range) Expression {
|
|
switch texpr := expr.(type) {
|
|
case *ScopeTraversalExpr:
|
|
texpr.Traversal = append(texpr.Traversal, next)
|
|
texpr.SrcRange = hcl.RangeBetween(texpr.SrcRange, rng)
|
|
return texpr
|
|
case *RelativeTraversalExpr:
|
|
texpr.Traversal = append(texpr.Traversal, next)
|
|
texpr.SrcRange = hcl.RangeBetween(texpr.SrcRange, rng)
|
|
return texpr
|
|
default:
|
|
return &RelativeTraversalExpr{
|
|
Source: expr,
|
|
Traversal: hcl.Traversal{next},
|
|
SrcRange: rng,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseExpressionTerm() (Expression, hcl.Diagnostics) {
|
|
start := p.Peek()
|
|
|
|
switch start.Type {
|
|
case TokenOParen:
|
|
p.Read() // eat open paren
|
|
|
|
p.PushIncludeNewlines(false)
|
|
|
|
expr, diags := p.ParseExpression()
|
|
if diags.HasErrors() {
|
|
// attempt to place the peeker after our closing paren
|
|
// before we return, so that the next parser has some
|
|
// chance of finding a valid expression.
|
|
p.recover(TokenCParen)
|
|
p.PopIncludeNewlines()
|
|
return expr, diags
|
|
}
|
|
|
|
close := p.Peek()
|
|
if close.Type != TokenCParen {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unbalanced parentheses",
|
|
Detail: "Expected a closing parenthesis to terminate the expression.",
|
|
Subject: &close.Range,
|
|
Context: hcl.RangeBetween(start.Range, close.Range).Ptr(),
|
|
})
|
|
p.setRecovery()
|
|
}
|
|
|
|
p.Read() // eat closing paren
|
|
p.PopIncludeNewlines()
|
|
|
|
return expr, diags
|
|
|
|
case TokenNumberLit:
|
|
tok := p.Read() // eat number token
|
|
|
|
numVal, diags := p.numberLitValue(tok)
|
|
return &LiteralValueExpr{
|
|
Val: numVal,
|
|
SrcRange: tok.Range,
|
|
}, diags
|
|
|
|
case TokenIdent:
|
|
tok := p.Read() // eat identifier token
|
|
|
|
if p.Peek().Type == TokenOParen {
|
|
return p.finishParsingFunctionCall(tok)
|
|
}
|
|
|
|
name := string(tok.Bytes)
|
|
switch name {
|
|
case "true":
|
|
return &LiteralValueExpr{
|
|
Val: cty.True,
|
|
SrcRange: tok.Range,
|
|
}, nil
|
|
case "false":
|
|
return &LiteralValueExpr{
|
|
Val: cty.False,
|
|
SrcRange: tok.Range,
|
|
}, nil
|
|
case "null":
|
|
return &LiteralValueExpr{
|
|
Val: cty.NullVal(cty.DynamicPseudoType),
|
|
SrcRange: tok.Range,
|
|
}, nil
|
|
default:
|
|
return &ScopeTraversalExpr{
|
|
Traversal: hcl.Traversal{
|
|
hcl.TraverseRoot{
|
|
Name: name,
|
|
SrcRange: tok.Range,
|
|
},
|
|
},
|
|
SrcRange: tok.Range,
|
|
}, nil
|
|
}
|
|
|
|
case TokenOQuote, TokenOHeredoc:
|
|
open := p.Read() // eat opening marker
|
|
closer := p.oppositeBracket(open.Type)
|
|
exprs, passthru, _, diags := p.parseTemplateInner(closer, tokenOpensFlushHeredoc(open))
|
|
|
|
closeRange := p.PrevRange()
|
|
|
|
if passthru {
|
|
if len(exprs) != 1 {
|
|
panic("passthru set with len(exprs) != 1")
|
|
}
|
|
return &TemplateWrapExpr{
|
|
Wrapped: exprs[0],
|
|
SrcRange: hcl.RangeBetween(open.Range, closeRange),
|
|
}, diags
|
|
}
|
|
|
|
return &TemplateExpr{
|
|
Parts: exprs,
|
|
SrcRange: hcl.RangeBetween(open.Range, closeRange),
|
|
}, diags
|
|
|
|
case TokenMinus:
|
|
tok := p.Read() // eat minus token
|
|
|
|
// Important to use parseExpressionWithTraversals rather than parseExpression
|
|
// here, otherwise we can capture a following binary expression into
|
|
// our negation.
|
|
// e.g. -46+5 should parse as (-46)+5, not -(46+5)
|
|
operand, diags := p.parseExpressionWithTraversals()
|
|
return &UnaryOpExpr{
|
|
Op: OpNegate,
|
|
Val: operand,
|
|
|
|
SrcRange: hcl.RangeBetween(tok.Range, operand.Range()),
|
|
SymbolRange: tok.Range,
|
|
}, diags
|
|
|
|
case TokenBang:
|
|
tok := p.Read() // eat bang token
|
|
|
|
// Important to use parseExpressionWithTraversals rather than parseExpression
|
|
// here, otherwise we can capture a following binary expression into
|
|
// our negation.
|
|
operand, diags := p.parseExpressionWithTraversals()
|
|
return &UnaryOpExpr{
|
|
Op: OpLogicalNot,
|
|
Val: operand,
|
|
|
|
SrcRange: hcl.RangeBetween(tok.Range, operand.Range()),
|
|
SymbolRange: tok.Range,
|
|
}, diags
|
|
|
|
case TokenOBrack:
|
|
return p.parseTupleCons()
|
|
|
|
case TokenOBrace:
|
|
return p.parseObjectCons()
|
|
|
|
default:
|
|
var diags hcl.Diagnostics
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid expression",
|
|
Detail: "Expected the start of an expression, but found an invalid expression token.",
|
|
Subject: &start.Range,
|
|
})
|
|
}
|
|
p.setRecovery()
|
|
|
|
// Return a placeholder so that the AST is still structurally sound
|
|
// even in the presence of parse errors.
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: start.Range,
|
|
}, diags
|
|
}
|
|
}
|
|
|
|
func (p *parser) numberLitValue(tok Token) (cty.Value, hcl.Diagnostics) {
|
|
// The cty.ParseNumberVal is always the same behavior as converting a
|
|
// string to a number, ensuring we always interpret decimal numbers in
|
|
// the same way.
|
|
numVal, err := cty.ParseNumberVal(string(tok.Bytes))
|
|
if err != nil {
|
|
ret := cty.UnknownVal(cty.Number)
|
|
return ret, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid number literal",
|
|
// FIXME: not a very good error message, but convert only
|
|
// gives us "a number is required", so not much help either.
|
|
Detail: "Failed to recognize the value of this number literal.",
|
|
Subject: &tok.Range,
|
|
},
|
|
}
|
|
}
|
|
return numVal, nil
|
|
}
|
|
|
|
// finishParsingFunctionCall parses a function call assuming that the function
|
|
// name was already read, and so the peeker should be pointing at the opening
|
|
// parenthesis after the name.
|
|
func (p *parser) finishParsingFunctionCall(name Token) (Expression, hcl.Diagnostics) {
|
|
openTok := p.Read()
|
|
if openTok.Type != TokenOParen {
|
|
// should never happen if callers behave
|
|
panic("finishParsingFunctionCall called with non-parenthesis as next token")
|
|
}
|
|
|
|
var args []Expression
|
|
var diags hcl.Diagnostics
|
|
var expandFinal bool
|
|
var closeTok Token
|
|
|
|
// Arbitrary newlines are allowed inside the function call parentheses.
|
|
p.PushIncludeNewlines(false)
|
|
|
|
Token:
|
|
for {
|
|
tok := p.Peek()
|
|
|
|
if tok.Type == TokenCParen {
|
|
closeTok = p.Read() // eat closing paren
|
|
break Token
|
|
}
|
|
|
|
arg, argDiags := p.ParseExpression()
|
|
args = append(args, arg)
|
|
diags = append(diags, argDiags...)
|
|
if p.recovery && argDiags.HasErrors() {
|
|
// if there was a parse error in the argument then we've
|
|
// probably been left in a weird place in the token stream,
|
|
// so we'll bail out with a partial argument list.
|
|
p.recover(TokenCParen)
|
|
break Token
|
|
}
|
|
|
|
sep := p.Read()
|
|
if sep.Type == TokenCParen {
|
|
closeTok = sep
|
|
break Token
|
|
}
|
|
|
|
if sep.Type == TokenEllipsis {
|
|
expandFinal = true
|
|
|
|
if p.Peek().Type != TokenCParen {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing closing parenthesis",
|
|
Detail: "An expanded function argument (with ...) must be immediately followed by closing parentheses.",
|
|
Subject: &sep.Range,
|
|
Context: hcl.RangeBetween(name.Range, sep.Range).Ptr(),
|
|
})
|
|
}
|
|
closeTok = p.recover(TokenCParen)
|
|
} else {
|
|
closeTok = p.Read() // eat closing paren
|
|
}
|
|
break Token
|
|
}
|
|
|
|
if sep.Type != TokenComma {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing argument separator",
|
|
Detail: "A comma is required to separate each function argument from the next.",
|
|
Subject: &sep.Range,
|
|
Context: hcl.RangeBetween(name.Range, sep.Range).Ptr(),
|
|
})
|
|
closeTok = p.recover(TokenCParen)
|
|
break Token
|
|
}
|
|
|
|
if p.Peek().Type == TokenCParen {
|
|
// A trailing comma after the last argument gets us in here.
|
|
closeTok = p.Read() // eat closing paren
|
|
break Token
|
|
}
|
|
|
|
}
|
|
|
|
p.PopIncludeNewlines()
|
|
|
|
return &FunctionCallExpr{
|
|
Name: string(name.Bytes),
|
|
Args: args,
|
|
|
|
ExpandFinal: expandFinal,
|
|
|
|
NameRange: name.Range,
|
|
OpenParenRange: openTok.Range,
|
|
CloseParenRange: closeTok.Range,
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) parseTupleCons() (Expression, hcl.Diagnostics) {
|
|
open := p.Read()
|
|
if open.Type != TokenOBrack {
|
|
// Should never happen if callers are behaving
|
|
panic("parseTupleCons called without peeker pointing to open bracket")
|
|
}
|
|
|
|
p.PushIncludeNewlines(false)
|
|
defer p.PopIncludeNewlines()
|
|
|
|
if forKeyword.TokenMatches(p.Peek()) {
|
|
return p.finishParsingForExpr(open)
|
|
}
|
|
|
|
var close Token
|
|
|
|
var diags hcl.Diagnostics
|
|
var exprs []Expression
|
|
|
|
for {
|
|
next := p.Peek()
|
|
if next.Type == TokenCBrack {
|
|
close = p.Read() // eat closer
|
|
break
|
|
}
|
|
|
|
expr, exprDiags := p.ParseExpression()
|
|
exprs = append(exprs, expr)
|
|
diags = append(diags, exprDiags...)
|
|
|
|
if p.recovery && exprDiags.HasErrors() {
|
|
// If expression parsing failed then we are probably in a strange
|
|
// place in the token stream, so we'll bail out and try to reset
|
|
// to after our closing bracket to allow parsing to continue.
|
|
close = p.recover(TokenCBrack)
|
|
break
|
|
}
|
|
|
|
next = p.Peek()
|
|
if next.Type == TokenCBrack {
|
|
close = p.Read() // eat closer
|
|
break
|
|
}
|
|
|
|
if next.Type != TokenComma {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing item separator",
|
|
Detail: "Expected a comma to mark the beginning of the next item.",
|
|
Subject: &next.Range,
|
|
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
|
|
})
|
|
}
|
|
close = p.recover(TokenCBrack)
|
|
break
|
|
}
|
|
|
|
p.Read() // eat comma
|
|
|
|
}
|
|
|
|
return &TupleConsExpr{
|
|
Exprs: exprs,
|
|
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
OpenRange: open.Range,
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
|
|
open := p.Read()
|
|
if open.Type != TokenOBrace {
|
|
// Should never happen if callers are behaving
|
|
panic("parseObjectCons called without peeker pointing to open brace")
|
|
}
|
|
|
|
// We must temporarily stop looking at newlines here while we check for
|
|
// a "for" keyword, since for expressions are _not_ newline-sensitive,
|
|
// even though object constructors are.
|
|
p.PushIncludeNewlines(false)
|
|
isFor := forKeyword.TokenMatches(p.Peek())
|
|
p.PopIncludeNewlines()
|
|
if isFor {
|
|
return p.finishParsingForExpr(open)
|
|
}
|
|
|
|
p.PushIncludeNewlines(true)
|
|
defer p.PopIncludeNewlines()
|
|
|
|
var close Token
|
|
|
|
var diags hcl.Diagnostics
|
|
var items []ObjectConsItem
|
|
|
|
for {
|
|
next := p.Peek()
|
|
if next.Type == TokenNewline {
|
|
p.Read() // eat newline
|
|
continue
|
|
}
|
|
|
|
if next.Type == TokenCBrace {
|
|
close = p.Read() // eat closer
|
|
break
|
|
}
|
|
|
|
// Wrapping parens are not explicitly represented in the AST, but
|
|
// we want to use them here to disambiguate intepreting a mapping
|
|
// key as a full expression rather than just a name, and so
|
|
// we'll remember this was present and use it to force the
|
|
// behavior of our final ObjectConsKeyExpr.
|
|
forceNonLiteral := (p.Peek().Type == TokenOParen)
|
|
|
|
var key Expression
|
|
var keyDiags hcl.Diagnostics
|
|
key, keyDiags = p.ParseExpression()
|
|
diags = append(diags, keyDiags...)
|
|
|
|
if p.recovery && keyDiags.HasErrors() {
|
|
// If expression parsing failed then we are probably in a strange
|
|
// place in the token stream, so we'll bail out and try to reset
|
|
// to after our closing brace to allow parsing to continue.
|
|
close = p.recover(TokenCBrace)
|
|
break
|
|
}
|
|
|
|
// We wrap up the key expression in a special wrapper that deals
|
|
// with our special case that naked identifiers as object keys
|
|
// are interpreted as literal strings.
|
|
key = &ObjectConsKeyExpr{
|
|
Wrapped: key,
|
|
ForceNonLiteral: forceNonLiteral,
|
|
}
|
|
|
|
next = p.Peek()
|
|
if next.Type != TokenEqual && next.Type != TokenColon {
|
|
if !p.recovery {
|
|
switch next.Type {
|
|
case TokenNewline, TokenComma:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing attribute value",
|
|
Detail: "Expected an attribute value, introduced by an equals sign (\"=\").",
|
|
Subject: &next.Range,
|
|
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
|
|
})
|
|
case TokenIdent:
|
|
// Although this might just be a plain old missing equals
|
|
// sign before a reference, one way to get here is to try
|
|
// to write an attribute name containing a period followed
|
|
// by a digit, which was valid in HCL1, like this:
|
|
// foo1.2_bar = "baz"
|
|
// We can't know exactly what the user intended here, but
|
|
// we'll augment our message with an extra hint in this case
|
|
// in case it is helpful.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing key/value separator",
|
|
Detail: "Expected an equals sign (\"=\") to mark the beginning of the attribute value. If you intended to given an attribute name containing periods or spaces, write the name in quotes to create a string literal.",
|
|
Subject: &next.Range,
|
|
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
|
|
})
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing key/value separator",
|
|
Detail: "Expected an equals sign (\"=\") to mark the beginning of the attribute value.",
|
|
Subject: &next.Range,
|
|
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
|
|
})
|
|
}
|
|
}
|
|
close = p.recover(TokenCBrace)
|
|
break
|
|
}
|
|
|
|
p.Read() // eat equals sign or colon
|
|
|
|
value, valueDiags := p.ParseExpression()
|
|
diags = append(diags, valueDiags...)
|
|
|
|
if p.recovery && valueDiags.HasErrors() {
|
|
// If expression parsing failed then we are probably in a strange
|
|
// place in the token stream, so we'll bail out and try to reset
|
|
// to after our closing brace to allow parsing to continue.
|
|
close = p.recover(TokenCBrace)
|
|
break
|
|
}
|
|
|
|
items = append(items, ObjectConsItem{
|
|
KeyExpr: key,
|
|
ValueExpr: value,
|
|
})
|
|
|
|
next = p.Peek()
|
|
if next.Type == TokenCBrace {
|
|
close = p.Read() // eat closer
|
|
break
|
|
}
|
|
|
|
if next.Type != TokenComma && next.Type != TokenNewline {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing attribute separator",
|
|
Detail: "Expected a newline or comma to mark the beginning of the next attribute.",
|
|
Subject: &next.Range,
|
|
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
|
|
})
|
|
}
|
|
close = p.recover(TokenCBrace)
|
|
break
|
|
}
|
|
|
|
p.Read() // eat comma or newline
|
|
|
|
}
|
|
|
|
return &ObjectConsExpr{
|
|
Items: items,
|
|
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
OpenRange: open.Range,
|
|
}, diags
|
|
}
|
|
|
|
func (p *parser) finishParsingForExpr(open Token) (Expression, hcl.Diagnostics) {
|
|
p.PushIncludeNewlines(false)
|
|
defer p.PopIncludeNewlines()
|
|
introducer := p.Read()
|
|
if !forKeyword.TokenMatches(introducer) {
|
|
// Should never happen if callers are behaving
|
|
panic("finishParsingForExpr called without peeker pointing to 'for' identifier")
|
|
}
|
|
|
|
var makeObj bool
|
|
var closeType TokenType
|
|
switch open.Type {
|
|
case TokenOBrace:
|
|
makeObj = true
|
|
closeType = TokenCBrace
|
|
case TokenOBrack:
|
|
makeObj = false // making a tuple
|
|
closeType = TokenCBrack
|
|
default:
|
|
// Should never happen if callers are behaving
|
|
panic("finishParsingForExpr called with invalid open token")
|
|
}
|
|
|
|
var diags hcl.Diagnostics
|
|
var keyName, valName string
|
|
|
|
if p.Peek().Type != TokenIdent {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "For expression requires variable name after 'for'.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
|
|
})
|
|
}
|
|
close := p.recover(closeType)
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
|
|
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, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "For expression requires value variable name after comma.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
|
|
})
|
|
}
|
|
close := p.recover(closeType)
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
|
|
valName = string(p.Read().Bytes)
|
|
}
|
|
|
|
if !inKeyword.TokenMatches(p.Peek()) {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "For expression requires the 'in' keyword after its name declarations.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
|
|
})
|
|
}
|
|
close := p.recover(closeType)
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
p.Read() // eat 'in' keyword
|
|
|
|
collExpr, collDiags := p.ParseExpression()
|
|
diags = append(diags, collDiags...)
|
|
if p.recovery && collDiags.HasErrors() {
|
|
close := p.recover(closeType)
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
|
|
if p.Peek().Type != TokenColon {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "For expression requires a colon after the collection expression.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
|
|
})
|
|
}
|
|
close := p.recover(closeType)
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
p.Read() // eat colon
|
|
|
|
var keyExpr, valExpr Expression
|
|
var keyDiags, valDiags hcl.Diagnostics
|
|
valExpr, valDiags = p.ParseExpression()
|
|
if p.Peek().Type == TokenFatArrow {
|
|
// What we just parsed was actually keyExpr
|
|
p.Read() // eat the fat arrow
|
|
keyExpr, keyDiags = valExpr, valDiags
|
|
|
|
valExpr, valDiags = p.ParseExpression()
|
|
}
|
|
diags = append(diags, keyDiags...)
|
|
diags = append(diags, valDiags...)
|
|
if p.recovery && (keyDiags.HasErrors() || valDiags.HasErrors()) {
|
|
close := p.recover(closeType)
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
|
|
group := false
|
|
var ellipsis Token
|
|
if p.Peek().Type == TokenEllipsis {
|
|
ellipsis = p.Read()
|
|
group = true
|
|
}
|
|
|
|
var condExpr Expression
|
|
var condDiags hcl.Diagnostics
|
|
if ifKeyword.TokenMatches(p.Peek()) {
|
|
p.Read() // eat "if"
|
|
condExpr, condDiags = p.ParseExpression()
|
|
diags = append(diags, condDiags...)
|
|
if p.recovery && condDiags.HasErrors() {
|
|
close := p.recover(p.oppositeBracket(open.Type))
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
}, diags
|
|
}
|
|
}
|
|
|
|
var close Token
|
|
if p.Peek().Type == closeType {
|
|
close = p.Read()
|
|
} else {
|
|
if !p.recovery {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "Extra characters after the end of the 'for' expression.",
|
|
Subject: p.Peek().Range.Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
|
|
})
|
|
}
|
|
close = p.recover(closeType)
|
|
}
|
|
|
|
if !makeObj {
|
|
if keyExpr != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "Key expression is not valid when building a tuple.",
|
|
Subject: keyExpr.Range().Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, close.Range).Ptr(),
|
|
})
|
|
}
|
|
|
|
if group {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "Grouping ellipsis (...) cannot be used when building a tuple.",
|
|
Subject: &ellipsis.Range,
|
|
Context: hcl.RangeBetween(open.Range, close.Range).Ptr(),
|
|
})
|
|
}
|
|
} else {
|
|
if keyExpr == nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' expression",
|
|
Detail: "Key expression is required when building an object.",
|
|
Subject: valExpr.Range().Ptr(),
|
|
Context: hcl.RangeBetween(open.Range, close.Range).Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return &ForExpr{
|
|
KeyVar: keyName,
|
|
ValVar: valName,
|
|
CollExpr: collExpr,
|
|
KeyExpr: keyExpr,
|
|
ValExpr: valExpr,
|
|
CondExpr: condExpr,
|
|
Group: group,
|
|
|
|
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
OpenRange: open.Range,
|
|
CloseRange: close.Range,
|
|
}, diags
|
|
}
|
|
|
|
// parseQuotedStringLiteral is a helper for parsing quoted strings that
|
|
// aren't allowed to contain any interpolations, such as block labels.
|
|
func (p *parser) parseQuotedStringLiteral() (string, hcl.Range, hcl.Diagnostics) {
|
|
oQuote := p.Read()
|
|
if oQuote.Type != TokenOQuote {
|
|
return "", oQuote.Range, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid string literal",
|
|
Detail: "A quoted string is required here.",
|
|
Subject: &oQuote.Range,
|
|
},
|
|
}
|
|
}
|
|
|
|
var diags hcl.Diagnostics
|
|
ret := &bytes.Buffer{}
|
|
var cQuote Token
|
|
|
|
Token:
|
|
for {
|
|
tok := p.Read()
|
|
switch tok.Type {
|
|
|
|
case TokenCQuote:
|
|
cQuote = tok
|
|
break Token
|
|
|
|
case TokenQuotedLit:
|
|
s, sDiags := ParseStringLiteralToken(tok)
|
|
diags = append(diags, sDiags...)
|
|
ret.WriteString(s)
|
|
|
|
case TokenTemplateControl, TokenTemplateInterp:
|
|
which := "$"
|
|
if tok.Type == TokenTemplateControl {
|
|
which = "%"
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid string literal",
|
|
Detail: fmt.Sprintf(
|
|
"Template sequences are not allowed in this string. To include a literal %q, double it (as \"%s%s\") to escape it.",
|
|
which, which, which,
|
|
),
|
|
Subject: &tok.Range,
|
|
Context: hcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
|
|
})
|
|
|
|
// Now that we're returning an error callers won't attempt to use
|
|
// the result for any real operations, but they might try to use
|
|
// the partial AST for other analyses, so we'll leave a marker
|
|
// to indicate that there was something invalid in the string to
|
|
// help avoid misinterpretation of the partial result
|
|
ret.WriteString(which)
|
|
ret.WriteString("{ ... }")
|
|
|
|
p.recover(TokenTemplateSeqEnd) // we'll try to keep parsing after the sequence ends
|
|
|
|
case TokenEOF:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unterminated string literal",
|
|
Detail: "Unable to find the closing quote mark before the end of the file.",
|
|
Subject: &tok.Range,
|
|
Context: hcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
|
|
})
|
|
break Token
|
|
|
|
default:
|
|
// Should never happen, as long as the scanner is behaving itself
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid string literal",
|
|
Detail: "This item is not valid in a string literal.",
|
|
Subject: &tok.Range,
|
|
Context: hcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
|
|
})
|
|
p.recover(TokenCQuote)
|
|
break Token
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret.String(), hcl.RangeBetween(oQuote.Range, cQuote.Range), diags
|
|
}
|
|
|
|
// ParseStringLiteralToken processes the given token, which must be either a
|
|
// TokenQuotedLit or a TokenStringLit, returning the string resulting from
|
|
// resolving any escape sequences.
|
|
//
|
|
// If any error diagnostics are returned, the returned string may be incomplete
|
|
// or otherwise invalid.
|
|
func ParseStringLiteralToken(tok Token) (string, hcl.Diagnostics) {
|
|
var quoted bool
|
|
switch tok.Type {
|
|
case TokenQuotedLit:
|
|
quoted = true
|
|
case TokenStringLit:
|
|
quoted = false
|
|
default:
|
|
panic("ParseStringLiteralToken can only be used with TokenStringLit and TokenQuotedLit tokens")
|
|
}
|
|
var diags hcl.Diagnostics
|
|
|
|
ret := make([]byte, 0, len(tok.Bytes))
|
|
slices := scanStringLit(tok.Bytes, quoted)
|
|
|
|
// We will mutate rng constantly as we walk through our token slices below.
|
|
// Any diagnostics must take a copy of this rng rather than simply pointing
|
|
// to it, e.g. by using rng.Ptr() rather than &rng.
|
|
rng := tok.Range
|
|
rng.End = rng.Start
|
|
|
|
Slices:
|
|
for _, slice := range slices {
|
|
if len(slice) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Advance the start of our range to where the previous token ended
|
|
rng.Start = rng.End
|
|
|
|
// Advance the end of our range to after our token.
|
|
b := slice
|
|
for len(b) > 0 {
|
|
adv, ch, _ := textseg.ScanGraphemeClusters(b, true)
|
|
rng.End.Byte += adv
|
|
switch ch[0] {
|
|
case '\r', '\n':
|
|
rng.End.Line++
|
|
rng.End.Column = 1
|
|
default:
|
|
rng.End.Column++
|
|
}
|
|
b = b[adv:]
|
|
}
|
|
|
|
TokenType:
|
|
switch slice[0] {
|
|
case '\\':
|
|
if !quoted {
|
|
// If we're not in quoted mode then just treat this token as
|
|
// normal. (Slices can still start with backslash even if we're
|
|
// not specifically looking for backslash sequences.)
|
|
break TokenType
|
|
}
|
|
if len(slice) < 2 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid escape sequence",
|
|
Detail: "Backslash must be followed by an escape sequence selector character.",
|
|
Subject: rng.Ptr(),
|
|
})
|
|
break TokenType
|
|
}
|
|
|
|
switch slice[1] {
|
|
|
|
case 'n':
|
|
ret = append(ret, '\n')
|
|
continue Slices
|
|
case 'r':
|
|
ret = append(ret, '\r')
|
|
continue Slices
|
|
case 't':
|
|
ret = append(ret, '\t')
|
|
continue Slices
|
|
case '"':
|
|
ret = append(ret, '"')
|
|
continue Slices
|
|
case '\\':
|
|
ret = append(ret, '\\')
|
|
continue Slices
|
|
case 'u', 'U':
|
|
if slice[1] == 'u' && len(slice) != 6 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid escape sequence",
|
|
Detail: "The \\u escape sequence must be followed by four hexadecimal digits.",
|
|
Subject: rng.Ptr(),
|
|
})
|
|
break TokenType
|
|
} else if slice[1] == 'U' && len(slice) != 10 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid escape sequence",
|
|
Detail: "The \\U escape sequence must be followed by eight hexadecimal digits.",
|
|
Subject: rng.Ptr(),
|
|
})
|
|
break TokenType
|
|
}
|
|
|
|
numHex := string(slice[2:])
|
|
num, err := strconv.ParseUint(numHex, 16, 32)
|
|
if err != nil {
|
|
// Should never happen because the scanner won't match
|
|
// a sequence of digits that isn't valid.
|
|
panic(err)
|
|
}
|
|
|
|
r := rune(num)
|
|
l := utf8.RuneLen(r)
|
|
if l == -1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid escape sequence",
|
|
Detail: fmt.Sprintf("Cannot encode character U+%04x in UTF-8.", num),
|
|
Subject: rng.Ptr(),
|
|
})
|
|
break TokenType
|
|
}
|
|
for i := 0; i < l; i++ {
|
|
ret = append(ret, 0)
|
|
}
|
|
rb := ret[len(ret)-l:]
|
|
utf8.EncodeRune(rb, r)
|
|
|
|
continue Slices
|
|
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid escape sequence",
|
|
Detail: fmt.Sprintf("The symbol %q is not a valid escape sequence selector.", slice[1:]),
|
|
Subject: rng.Ptr(),
|
|
})
|
|
ret = append(ret, slice[1:]...)
|
|
continue Slices
|
|
}
|
|
|
|
case '$', '%':
|
|
if len(slice) != 3 {
|
|
// Not long enough to be our escape sequence, so it's literal.
|
|
break TokenType
|
|
}
|
|
|
|
if slice[1] == slice[0] && slice[2] == '{' {
|
|
ret = append(ret, slice[0])
|
|
ret = append(ret, '{')
|
|
continue Slices
|
|
}
|
|
|
|
break TokenType
|
|
}
|
|
|
|
// If we fall out here or break out of here from the switch above
|
|
// then this slice is just a literal.
|
|
ret = append(ret, slice...)
|
|
}
|
|
|
|
return string(ret), diags
|
|
}
|
|
|
|
// setRecovery turns on recovery mode without actually doing any recovery.
|
|
// This can be used when a parser knowingly leaves the peeker in a useless
|
|
// place and wants to suppress errors that might result from that decision.
|
|
func (p *parser) setRecovery() {
|
|
p.recovery = true
|
|
}
|
|
|
|
// recover seeks forward in the token stream until it finds TokenType "end",
|
|
// then returns with the peeker pointed at the following token.
|
|
//
|
|
// If the given token type is a bracketer, this function will additionally
|
|
// count nested instances of the brackets to try to leave the peeker at
|
|
// the end of the _current_ instance of that bracketer, skipping over any
|
|
// nested instances. This is a best-effort operation and may have
|
|
// unpredictable results on input with bad bracketer nesting.
|
|
func (p *parser) recover(end TokenType) Token {
|
|
start := p.oppositeBracket(end)
|
|
p.recovery = true
|
|
|
|
nest := 0
|
|
for {
|
|
tok := p.Read()
|
|
ty := tok.Type
|
|
if end == TokenTemplateSeqEnd && ty == TokenTemplateControl {
|
|
// normalize so that our matching behavior can work, since
|
|
// TokenTemplateControl/TokenTemplateInterp are asymmetrical
|
|
// with TokenTemplateSeqEnd and thus we need to count both
|
|
// openers if that's the closer we're looking for.
|
|
ty = TokenTemplateInterp
|
|
}
|
|
|
|
switch ty {
|
|
case start:
|
|
nest++
|
|
case end:
|
|
if nest < 1 {
|
|
return tok
|
|
}
|
|
|
|
nest--
|
|
case TokenEOF:
|
|
return tok
|
|
}
|
|
}
|
|
}
|
|
|
|
// recoverOver seeks forward in the token stream until it finds a block
|
|
// starting with TokenType "start", then finds the corresponding end token,
|
|
// leaving the peeker pointed at the token after that end token.
|
|
//
|
|
// The given token type _must_ be a bracketer. For example, if the given
|
|
// start token is TokenOBrace then the parser will be left at the _end_ of
|
|
// the next brace-delimited block encountered, or at EOF if no such block
|
|
// is found or it is unclosed.
|
|
func (p *parser) recoverOver(start TokenType) {
|
|
end := p.oppositeBracket(start)
|
|
|
|
// find the opening bracket first
|
|
Token:
|
|
for {
|
|
tok := p.Read()
|
|
switch tok.Type {
|
|
case start, TokenEOF:
|
|
break Token
|
|
}
|
|
}
|
|
|
|
// Now use our existing recover function to locate the _end_ of the
|
|
// container we've found.
|
|
p.recover(end)
|
|
}
|
|
|
|
func (p *parser) recoverAfterBodyItem() {
|
|
p.recovery = true
|
|
var open []TokenType
|
|
|
|
Token:
|
|
for {
|
|
tok := p.Read()
|
|
|
|
switch tok.Type {
|
|
|
|
case TokenNewline:
|
|
if len(open) == 0 {
|
|
break Token
|
|
}
|
|
|
|
case TokenEOF:
|
|
break Token
|
|
|
|
case TokenOBrace, TokenOBrack, TokenOParen, TokenOQuote, TokenOHeredoc, TokenTemplateInterp, TokenTemplateControl:
|
|
open = append(open, tok.Type)
|
|
|
|
case TokenCBrace, TokenCBrack, TokenCParen, TokenCQuote, TokenCHeredoc:
|
|
opener := p.oppositeBracket(tok.Type)
|
|
for len(open) > 0 && open[len(open)-1] != opener {
|
|
open = open[:len(open)-1]
|
|
}
|
|
if len(open) > 0 {
|
|
open = open[:len(open)-1]
|
|
}
|
|
|
|
case TokenTemplateSeqEnd:
|
|
for len(open) > 0 && open[len(open)-1] != TokenTemplateInterp && open[len(open)-1] != TokenTemplateControl {
|
|
open = open[:len(open)-1]
|
|
}
|
|
if len(open) > 0 {
|
|
open = open[:len(open)-1]
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// oppositeBracket finds the bracket that opposes the given bracketer, or
|
|
// NilToken if the given token isn't a bracketer.
|
|
//
|
|
// "Bracketer", for the sake of this function, is one end of a matching
|
|
// open/close set of tokens that establish a bracketing context.
|
|
func (p *parser) oppositeBracket(ty TokenType) TokenType {
|
|
switch ty {
|
|
|
|
case TokenOBrace:
|
|
return TokenCBrace
|
|
case TokenOBrack:
|
|
return TokenCBrack
|
|
case TokenOParen:
|
|
return TokenCParen
|
|
case TokenOQuote:
|
|
return TokenCQuote
|
|
case TokenOHeredoc:
|
|
return TokenCHeredoc
|
|
|
|
case TokenCBrace:
|
|
return TokenOBrace
|
|
case TokenCBrack:
|
|
return TokenOBrack
|
|
case TokenCParen:
|
|
return TokenOParen
|
|
case TokenCQuote:
|
|
return TokenOQuote
|
|
case TokenCHeredoc:
|
|
return TokenOHeredoc
|
|
|
|
case TokenTemplateControl:
|
|
return TokenTemplateSeqEnd
|
|
case TokenTemplateInterp:
|
|
return TokenTemplateSeqEnd
|
|
case TokenTemplateSeqEnd:
|
|
// This is ambigous, but we return Interp here because that's
|
|
// what's assumed by the "recover" method.
|
|
return TokenTemplateInterp
|
|
|
|
default:
|
|
return TokenNil
|
|
}
|
|
}
|
|
|
|
func errPlaceholderExpr(rng hcl.Range) Expression {
|
|
return &LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
SrcRange: rng,
|
|
}
|
|
}
|