mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-09-07 09:36:44 +00:00
523 lines
12 KiB
Go
523 lines
12 KiB
Go
package parser
|
|
|
|
import (
|
|
"strconv"
|
|
"unicode/utf8"
|
|
|
|
"github.com/hashicorp/hil/ast"
|
|
"github.com/hashicorp/hil/scanner"
|
|
)
|
|
|
|
func Parse(ch <-chan *scanner.Token) (ast.Node, error) {
|
|
peeker := scanner.NewPeeker(ch)
|
|
parser := &parser{peeker}
|
|
output, err := parser.ParseTopLevel()
|
|
peeker.Close()
|
|
return output, err
|
|
}
|
|
|
|
type parser struct {
|
|
peeker *scanner.Peeker
|
|
}
|
|
|
|
func (p *parser) ParseTopLevel() (ast.Node, error) {
|
|
return p.parseInterpolationSeq(false)
|
|
}
|
|
|
|
func (p *parser) ParseQuoted() (ast.Node, error) {
|
|
return p.parseInterpolationSeq(true)
|
|
}
|
|
|
|
// parseInterpolationSeq parses either the top-level sequence of literals
|
|
// and interpolation expressions or a similar sequence within a quoted
|
|
// string inside an interpolation expression. The latter case is requested
|
|
// by setting 'quoted' to true.
|
|
func (p *parser) parseInterpolationSeq(quoted bool) (ast.Node, error) {
|
|
literalType := scanner.LITERAL
|
|
endType := scanner.EOF
|
|
if quoted {
|
|
// exceptions for quoted sequences
|
|
literalType = scanner.STRING
|
|
endType = scanner.CQUOTE
|
|
}
|
|
|
|
startPos := p.peeker.Peek().Pos
|
|
|
|
if quoted {
|
|
tok := p.peeker.Read()
|
|
if tok.Type != scanner.OQUOTE {
|
|
return nil, ExpectationError("open quote", tok)
|
|
}
|
|
}
|
|
|
|
var exprs []ast.Node
|
|
for {
|
|
tok := p.peeker.Read()
|
|
|
|
if tok.Type == endType {
|
|
break
|
|
}
|
|
|
|
switch tok.Type {
|
|
case literalType:
|
|
val, err := p.parseStringToken(tok)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
exprs = append(exprs, &ast.LiteralNode{
|
|
Value: val,
|
|
Typex: ast.TypeString,
|
|
Posx: tok.Pos,
|
|
})
|
|
case scanner.BEGIN:
|
|
expr, err := p.ParseInterpolation()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
exprs = append(exprs, expr)
|
|
default:
|
|
return nil, ExpectationError(`"${"`, tok)
|
|
}
|
|
}
|
|
|
|
if len(exprs) == 0 {
|
|
// If we have no parts at all then the input must've
|
|
// been an empty string.
|
|
exprs = append(exprs, &ast.LiteralNode{
|
|
Value: "",
|
|
Typex: ast.TypeString,
|
|
Posx: startPos,
|
|
})
|
|
}
|
|
|
|
// As a special case, if our "Output" contains only one expression
|
|
// and it's a literal string then we'll hoist it up to be our
|
|
// direct return value, so callers can easily recognize a string
|
|
// that has no interpolations at all.
|
|
if len(exprs) == 1 {
|
|
if lit, ok := exprs[0].(*ast.LiteralNode); ok {
|
|
if lit.Typex == ast.TypeString {
|
|
return lit, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return &ast.Output{
|
|
Exprs: exprs,
|
|
Posx: startPos,
|
|
}, nil
|
|
}
|
|
|
|
// parseStringToken takes a token of either LITERAL or STRING type and
|
|
// returns the interpreted string, after processing any relevant
|
|
// escape sequences.
|
|
func (p *parser) parseStringToken(tok *scanner.Token) (string, error) {
|
|
var backslashes bool
|
|
switch tok.Type {
|
|
case scanner.LITERAL:
|
|
backslashes = false
|
|
case scanner.STRING:
|
|
backslashes = true
|
|
default:
|
|
panic("unsupported string token type")
|
|
}
|
|
|
|
raw := []byte(tok.Content)
|
|
buf := make([]byte, 0, len(raw))
|
|
|
|
for i := 0; i < len(raw); i++ {
|
|
b := raw[i]
|
|
more := len(raw) > (i + 1)
|
|
|
|
if b == '$' {
|
|
if more && raw[i+1] == '$' {
|
|
// skip over the second dollar sign
|
|
i++
|
|
}
|
|
} else if backslashes && b == '\\' {
|
|
if !more {
|
|
return "", Errorf(
|
|
ast.Pos{
|
|
Column: tok.Pos.Column + utf8.RuneCount(raw[:i]),
|
|
Line: tok.Pos.Line,
|
|
},
|
|
`unfinished backslash escape sequence`,
|
|
)
|
|
}
|
|
escapeType := raw[i+1]
|
|
switch escapeType {
|
|
case '\\':
|
|
// skip over the second slash
|
|
i++
|
|
case 'n':
|
|
b = '\n'
|
|
i++
|
|
case '"':
|
|
b = '"'
|
|
i++
|
|
default:
|
|
return "", Errorf(
|
|
ast.Pos{
|
|
Column: tok.Pos.Column + utf8.RuneCount(raw[:i]),
|
|
Line: tok.Pos.Line,
|
|
},
|
|
`invalid backslash escape sequence`,
|
|
)
|
|
}
|
|
}
|
|
|
|
buf = append(buf, b)
|
|
}
|
|
|
|
return string(buf), nil
|
|
}
|
|
|
|
func (p *parser) ParseInterpolation() (ast.Node, error) {
|
|
// By the time we're called, we're already "inside" the ${ sequence
|
|
// because the caller consumed the ${ token.
|
|
|
|
expr, err := p.ParseExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = p.requireTokenType(scanner.END, `"}"`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return expr, nil
|
|
}
|
|
|
|
func (p *parser) ParseExpression() (ast.Node, error) {
|
|
return p.parseTernaryCond()
|
|
}
|
|
|
|
func (p *parser) parseTernaryCond() (ast.Node, error) {
|
|
// The ternary condition operator (.. ? .. : ..) behaves somewhat
|
|
// like a binary operator except that the "operator" 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.
|
|
|
|
startPos := p.peeker.Peek().Pos
|
|
|
|
var cond, trueExpr, falseExpr ast.Node
|
|
var err error
|
|
|
|
cond, err = p.parseBinaryOps(binaryOps)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
next := p.peeker.Peek()
|
|
if next.Type != scanner.QUESTION {
|
|
return cond, nil
|
|
}
|
|
|
|
p.peeker.Read() // eat question mark
|
|
|
|
trueExpr, err = p.ParseExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
colon := p.peeker.Read()
|
|
if colon.Type != scanner.COLON {
|
|
return nil, ExpectationError(":", colon)
|
|
}
|
|
|
|
falseExpr, err = p.ParseExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ast.Conditional{
|
|
CondExpr: cond,
|
|
TrueExpr: trueExpr,
|
|
FalseExpr: falseExpr,
|
|
Posx: startPos,
|
|
}, nil
|
|
}
|
|
|
|
// 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[scanner.TokenType]ast.ArithmeticOp) (ast.Node, error) {
|
|
if len(ops) == 0 {
|
|
// We've run out of operators, so now we'll just try to parse a term.
|
|
return p.ParseExpressionTerm()
|
|
}
|
|
|
|
thisLevel := ops[0]
|
|
remaining := ops[1:]
|
|
|
|
startPos := p.peeker.Peek().Pos
|
|
|
|
var lhs, rhs ast.Node
|
|
operator := ast.ArithmeticOpInvalid
|
|
var err error
|
|
|
|
// parse a term that might be the first operand of a binary
|
|
// expression or it might just be a standalone term, but
|
|
// we won't know until we've parsed it and can look ahead
|
|
// to see if there's an operator token.
|
|
lhs, err = p.parseBinaryOps(remaining)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll keep eating up arithmetic 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.peeker.Peek()
|
|
var newOperator ast.ArithmeticOp
|
|
var ok bool
|
|
if newOperator, ok = thisLevel[next.Type]; !ok {
|
|
break
|
|
}
|
|
|
|
// Are we extending an expression started on
|
|
// the previous iteration?
|
|
if operator != ast.ArithmeticOpInvalid {
|
|
lhs = &ast.Arithmetic{
|
|
Op: operator,
|
|
Exprs: []ast.Node{lhs, rhs},
|
|
Posx: startPos,
|
|
}
|
|
}
|
|
|
|
operator = newOperator
|
|
p.peeker.Read() // eat operator token
|
|
rhs, err = p.parseBinaryOps(remaining)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if operator != ast.ArithmeticOpInvalid {
|
|
return &ast.Arithmetic{
|
|
Op: operator,
|
|
Exprs: []ast.Node{lhs, rhs},
|
|
Posx: startPos,
|
|
}, nil
|
|
} else {
|
|
return lhs, nil
|
|
}
|
|
}
|
|
|
|
func (p *parser) ParseExpressionTerm() (ast.Node, error) {
|
|
|
|
next := p.peeker.Peek()
|
|
|
|
switch next.Type {
|
|
|
|
case scanner.OPAREN:
|
|
p.peeker.Read()
|
|
expr, err := p.ParseExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = p.requireTokenType(scanner.CPAREN, `")"`)
|
|
return expr, err
|
|
|
|
case scanner.OQUOTE:
|
|
return p.ParseQuoted()
|
|
|
|
case scanner.INTEGER:
|
|
tok := p.peeker.Read()
|
|
val, err := strconv.Atoi(tok.Content)
|
|
if err != nil {
|
|
return nil, TokenErrorf(tok, "invalid integer: %s", err)
|
|
}
|
|
return &ast.LiteralNode{
|
|
Value: val,
|
|
Typex: ast.TypeInt,
|
|
Posx: tok.Pos,
|
|
}, nil
|
|
|
|
case scanner.FLOAT:
|
|
tok := p.peeker.Read()
|
|
val, err := strconv.ParseFloat(tok.Content, 64)
|
|
if err != nil {
|
|
return nil, TokenErrorf(tok, "invalid float: %s", err)
|
|
}
|
|
return &ast.LiteralNode{
|
|
Value: val,
|
|
Typex: ast.TypeFloat,
|
|
Posx: tok.Pos,
|
|
}, nil
|
|
|
|
case scanner.BOOL:
|
|
tok := p.peeker.Read()
|
|
// the scanner guarantees that tok.Content is either "true" or "false"
|
|
var val bool
|
|
if tok.Content[0] == 't' {
|
|
val = true
|
|
} else {
|
|
val = false
|
|
}
|
|
return &ast.LiteralNode{
|
|
Value: val,
|
|
Typex: ast.TypeBool,
|
|
Posx: tok.Pos,
|
|
}, nil
|
|
|
|
case scanner.MINUS:
|
|
opTok := p.peeker.Read()
|
|
// important to use ParseExpressionTerm rather than ParseExpression
|
|
// here, otherwise we can capture a following binary expression into
|
|
// our negation.
|
|
// e.g. -46+5 should parse as (0-46)+5, not 0-(46+5)
|
|
operand, err := p.ParseExpressionTerm()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The AST currently represents negative numbers as
|
|
// a binary subtraction of the number from zero.
|
|
return &ast.Arithmetic{
|
|
Op: ast.ArithmeticOpSub,
|
|
Exprs: []ast.Node{
|
|
&ast.LiteralNode{
|
|
Value: 0,
|
|
Typex: ast.TypeInt,
|
|
Posx: opTok.Pos,
|
|
},
|
|
operand,
|
|
},
|
|
Posx: opTok.Pos,
|
|
}, nil
|
|
|
|
case scanner.BANG:
|
|
opTok := p.peeker.Read()
|
|
// important to use ParseExpressionTerm rather than ParseExpression
|
|
// here, otherwise we can capture a following binary expression into
|
|
// our negation.
|
|
operand, err := p.ParseExpressionTerm()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The AST currently represents binary negation as an equality
|
|
// test with "false".
|
|
return &ast.Arithmetic{
|
|
Op: ast.ArithmeticOpEqual,
|
|
Exprs: []ast.Node{
|
|
&ast.LiteralNode{
|
|
Value: false,
|
|
Typex: ast.TypeBool,
|
|
Posx: opTok.Pos,
|
|
},
|
|
operand,
|
|
},
|
|
Posx: opTok.Pos,
|
|
}, nil
|
|
|
|
case scanner.IDENTIFIER:
|
|
return p.ParseScopeInteraction()
|
|
|
|
default:
|
|
return nil, ExpectationError("expression", next)
|
|
}
|
|
}
|
|
|
|
// ParseScopeInteraction parses the expression types that interact
|
|
// with the evaluation scope: variable access, function calls, and
|
|
// indexing.
|
|
//
|
|
// Indexing should actually be a distinct operator in its own right,
|
|
// so that e.g. it can be applied to the result of a function call,
|
|
// but for now we're preserving the behavior of the older yacc-based
|
|
// parser.
|
|
func (p *parser) ParseScopeInteraction() (ast.Node, error) {
|
|
first := p.peeker.Read()
|
|
startPos := first.Pos
|
|
if first.Type != scanner.IDENTIFIER {
|
|
return nil, ExpectationError("identifier", first)
|
|
}
|
|
|
|
next := p.peeker.Peek()
|
|
if next.Type == scanner.OPAREN {
|
|
// function call
|
|
funcName := first.Content
|
|
p.peeker.Read() // eat paren
|
|
var args []ast.Node
|
|
|
|
for {
|
|
if p.peeker.Peek().Type == scanner.CPAREN {
|
|
break
|
|
}
|
|
|
|
arg, err := p.ParseExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
args = append(args, arg)
|
|
|
|
if p.peeker.Peek().Type == scanner.COMMA {
|
|
p.peeker.Read() // eat comma
|
|
continue
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
err := p.requireTokenType(scanner.CPAREN, `")"`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ast.Call{
|
|
Func: funcName,
|
|
Args: args,
|
|
Posx: startPos,
|
|
}, nil
|
|
}
|
|
|
|
varNode := &ast.VariableAccess{
|
|
Name: first.Content,
|
|
Posx: startPos,
|
|
}
|
|
|
|
if p.peeker.Peek().Type == scanner.OBRACKET {
|
|
// index operator
|
|
startPos := p.peeker.Read().Pos // eat bracket
|
|
indexExpr, err := p.ParseExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = p.requireTokenType(scanner.CBRACKET, `"]"`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ast.Index{
|
|
Target: varNode,
|
|
Key: indexExpr,
|
|
Posx: startPos,
|
|
}, nil
|
|
}
|
|
|
|
return varNode, nil
|
|
}
|
|
|
|
// requireTokenType consumes the next token an returns an error if its
|
|
// type does not match the given type. nil is returned if the type matches.
|
|
//
|
|
// This is a helper around peeker.Read() for situations where the parser just
|
|
// wants to assert that a particular token type must be present.
|
|
func (p *parser) requireTokenType(wantType scanner.TokenType, wantName string) error {
|
|
token := p.peeker.Read()
|
|
if token.Type != wantType {
|
|
return ExpectationError(wantName, token)
|
|
}
|
|
return nil
|
|
}
|