package actionlint import ( "fmt" "strconv" "strings" ) func errorAtToken(t *Token, msg string) *ExprError { return &ExprError{ Message: msg, Offset: t.Offset, Line: t.Line, Column: t.Column, } } // ExprParser is a parser for expression syntax. To know the details, see // https://docs.github.com/en/actions/learn-github-actions/expressions type ExprParser struct { cur *Token lexer *ExprLexer err *ExprError } // NewExprParser creates new ExprParser instance. func NewExprParser() *ExprParser { return &ExprParser{} } func (p *ExprParser) error(msg string) { if p.err == nil { p.err = errorAtToken(p.cur, msg) } } func (p *ExprParser) errorf(format string, args ...interface{}) { p.error(fmt.Sprintf(format, args...)) } func (p *ExprParser) unexpected(where string, expected []TokenKind) { if p.err != nil { return } qb := quotesBuilder{} for _, e := range expected { qb.append(e.String()) } var what string if p.cur.Kind == TokenKindEnd { what = "end of input" } else { what = fmt.Sprintf("token %q", p.cur.Kind.String()) } msg := fmt.Sprintf("unexpected %s while parsing %s. expecting %s", what, where, qb.build()) p.error(msg) } func (p *ExprParser) next() *Token { ret := p.cur p.cur = p.lexer.Next() return ret } func (p *ExprParser) peek() *Token { return p.cur } func (p *ExprParser) parseIdent() ExprNode { ident := p.next() // eat ident switch p.peek().Kind { case TokenKindLeftParen: // Parse function call as primary expression though generally function call is parsed as // postfix expression. The reason is that only built-in function call is allowed in workflow // expression syntax, meant that callee is always built-in function name, not a general // expression. p.next() // eat '(' args := []ExprNode{} if p.peek().Kind == TokenKindRightParen { // no arguments p.next() // eat ')' } else { LoopArgs: for { arg := p.parseLogicalOr() if arg == nil { return nil } args = append(args, arg) switch p.peek().Kind { case TokenKindComma: p.next() // eat ',' // continue to next argument case TokenKindRightParen: p.next() // eat ')' break LoopArgs default: p.unexpected("arguments of function call", []TokenKind{TokenKindComma, TokenKindRightParen}) return nil } } } return &FuncCallNode{ident.Value, args, ident} default: // Handle keywords. Note that keywords are case sensitive. TRUE, FALSE, NULL are invalid named value. switch ident.Value { case "null": return &NullNode{ident} case "true": return &BoolNode{true, ident} case "false": return &BoolNode{false, ident} default: // Variable name access is case insensitive. github.event and GITHUB.event are the same. return &VariableNode{strings.ToLower(ident.Value), ident} } } } func (p *ExprParser) parseNestedExpr() ExprNode { p.next() // eat '(' nested := p.parseLogicalOr() if nested == nil { return nil } if p.peek().Kind == TokenKindRightParen { p.next() // eat ')' } else { p.unexpected("closing ')' of nested expression (...)", []TokenKind{TokenKindRightParen}) return nil } return nested } func (p *ExprParser) parseInt() ExprNode { t := p.peek() i, err := strconv.ParseInt(t.Value, 0, 32) if err != nil { p.errorf("parsing invalid integer literal %q: %s", t.Value, err) return nil } p.next() // eat int return &IntNode{int(i), t} } func (p *ExprParser) parseFloat() ExprNode { t := p.peek() f, err := strconv.ParseFloat(t.Value, 64) if err != nil { p.errorf("parsing invalid float literal %q: %s", t.Value, err) return nil } p.next() // eat float return &FloatNode{f, t} } func (p *ExprParser) parseString() ExprNode { t := p.next() // eat string s := t.Value s = s[1 : len(s)-1] // strip first and last single quotes s = strings.ReplaceAll(s, "''", "'") // unescape '' return &StringNode{s, t} } func (p *ExprParser) parsePrimaryExpr() ExprNode { switch p.peek().Kind { case TokenKindIdent: return p.parseIdent() case TokenKindLeftParen: return p.parseNestedExpr() case TokenKindInt: return p.parseInt() case TokenKindFloat: return p.parseFloat() case TokenKindString: return p.parseString() default: p.unexpected( "variable access, function call, null, bool, int, float or string", []TokenKind{ TokenKindIdent, TokenKindLeftParen, TokenKindInt, TokenKindFloat, TokenKindString, }, ) return nil } } func (p *ExprParser) parsePostfixOp() ExprNode { ret := p.parsePrimaryExpr() if ret == nil { return nil } for { switch p.peek().Kind { case TokenKindDot: p.next() // eat '.' switch p.peek().Kind { case TokenKindStar: p.next() // eat '*' ret = &ArrayDerefNode{ret} case TokenKindIdent: t := p.next() // eat 'b' of 'a.b' // Property name is case insensitive. github.event and github.EVENT are the same ret = &ObjectDerefNode{ret, strings.ToLower(t.Value)} default: p.unexpected( "object property dereference like 'a.b' or array element dereference like 'a.*'", []TokenKind{TokenKindIdent, TokenKindStar}, ) return nil } case TokenKindLeftBracket: p.next() // eat '[' idx := p.parseLogicalOr() if idx == nil { return nil } ret = &IndexAccessNode{ret, idx} if p.peek().Kind != TokenKindRightBracket { p.unexpected("closing bracket ']' for index access", []TokenKind{TokenKindRightBracket}) return nil } p.next() // eat ']' default: return ret } } } func (p *ExprParser) parsePrefixOp() ExprNode { t := p.peek() if t.Kind != TokenKindNot { return p.parsePostfixOp() } p.next() // eat '!' token o := p.parsePrefixOp() if o == nil { return nil } return &NotOpNode{o, t} } func (p *ExprParser) parseCompareBinOp() ExprNode { l := p.parsePrefixOp() if l == nil { return nil } k := CompareOpNodeKindInvalid switch p.peek().Kind { case TokenKindLess: k = CompareOpNodeKindLess case TokenKindLessEq: k = CompareOpNodeKindLessEq case TokenKindGreater: k = CompareOpNodeKindGreater case TokenKindGreaterEq: k = CompareOpNodeKindGreaterEq case TokenKindEq: k = CompareOpNodeKindEq case TokenKindNotEq: k = CompareOpNodeKindNotEq default: return l } p.next() // eat the operator token r := p.parseCompareBinOp() if r == nil { return nil } return &CompareOpNode{k, l, r} } func (p *ExprParser) parseLogicalAnd() ExprNode { l := p.parseCompareBinOp() if l == nil { return nil } if p.peek().Kind != TokenKindAnd { return l } p.next() // eat && r := p.parseLogicalAnd() if r == nil { return nil } return &LogicalOpNode{LogicalOpNodeKindAnd, l, r} } func (p *ExprParser) parseLogicalOr() ExprNode { l := p.parseLogicalAnd() if l == nil { return nil } if p.peek().Kind != TokenKindOr { return l } p.next() // eat || r := p.parseLogicalOr() if r == nil { return nil } return &LogicalOpNode{LogicalOpNodeKindOr, l, r} } // Err returns an error which was caused while previous parsing. func (p *ExprParser) Err() *ExprError { if err := p.lexer.Err(); err != nil { return err } return p.err } // Parse parses token sequence lexed by a given lexer into syntax tree. func (p *ExprParser) Parse(l *ExprLexer) (ExprNode, *ExprError) { // Init p.err = nil p.lexer = l p.cur = l.Next() root := p.parseLogicalOr() if err := p.Err(); err != nil { return nil, err } if t := p.peek(); t.Kind != TokenKindEnd { // It did not reach the end of sequence qb := quotesBuilder{} qb.append(t.Kind.String()) c := 1 for { t := l.Next() if t.Kind == TokenKindEnd { break } qb.append(t.Kind.String()) c++ } p.errorf("parser did not reach end of input after parsing the expression. %d remaining token(s) in the input: %s", c, qb.build()) return nil, p.err } return root, nil }