// Copyright 2014 The Bazel Authors. All rights reserved. // // 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 com.google.devtools.build.lib.syntax; import static com.google.devtools.build.lib.syntax.Parser.ParsingMode.BUILD; import static com.google.devtools.build.lib.syntax.Parser.ParsingMode.SKYLARK; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.ProfilerTask; import com.google.devtools.build.lib.syntax.DictionaryLiteral.DictionaryEntryLiteral; import com.google.devtools.build.lib.syntax.IfStatement.ConditionalStatements; import com.google.devtools.build.lib.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Recursive descent parser for LL(2) BUILD language. * Loosely based on Python 2 grammar. * See https://docs.python.org/2/reference/grammar.html */ @VisibleForTesting public class Parser { /** * Combines the parser result into a single value object. */ public static final class ParseResult { /** The statements (rules, basically) from the parsed file. */ public final List statements; /** The comments from the parsed file. */ public final List comments; /** Represents every statement in the file. */ public final Location location; /** Whether the file contained any errors. */ public final boolean containsErrors; public ParseResult(List statements, List comments, Location location, boolean containsErrors) { // No need to copy here; when the object is created, the parser instance is just about to go // out of scope and be garbage collected. this.statements = Preconditions.checkNotNull(statements); this.comments = Preconditions.checkNotNull(comments); this.location = location; this.containsErrors = containsErrors; } } /** * ParsingMode is used to select which features the parser should accept. */ public enum ParsingMode { /** Used for parsing BUILD files */ BUILD, /** Used for parsing .bzl files */ SKYLARK, } private static final EnumSet STATEMENT_TERMINATOR_SET = EnumSet.of(TokenKind.EOF, TokenKind.NEWLINE, TokenKind.SEMI); private static final EnumSet LIST_TERMINATOR_SET = EnumSet.of(TokenKind.EOF, TokenKind.RBRACKET, TokenKind.SEMI); private static final EnumSet DICT_TERMINATOR_SET = EnumSet.of(TokenKind.EOF, TokenKind.RBRACE, TokenKind.SEMI); private static final EnumSet EXPR_LIST_TERMINATOR_SET = EnumSet.of( TokenKind.EOF, TokenKind.NEWLINE, TokenKind.EQUALS, TokenKind.RBRACE, TokenKind.RBRACKET, TokenKind.RPAREN, TokenKind.SEMI); private static final EnumSet BLOCK_STARTING_SET = EnumSet.of( TokenKind.CLASS, TokenKind.DEF, TokenKind.ELSE, TokenKind.FOR, TokenKind.IF, TokenKind.TRY); private static final EnumSet EXPR_TERMINATOR_SET = EnumSet.of( TokenKind.COLON, TokenKind.COMMA, TokenKind.EOF, TokenKind.FOR, TokenKind.MINUS, TokenKind.PERCENT, TokenKind.PLUS, TokenKind.RBRACKET, TokenKind.RPAREN, TokenKind.SLASH); /** * Keywords that are forbidden in both Skylark and BUILD parsing modes. * *

(Mapping: token -> human-readable string description) */ private static final ImmutableMap ILLEGAL_BLOCK_KEYWORDS = ImmutableMap.of(TokenKind.CLASS, "Class definition", TokenKind.TRY, "Try statement"); private Token token; // current lookahead token private Token pushedToken = null; // used to implement LL(2) private int loopCount; // break/continue keywords can be used only inside a loop private static final boolean DEBUGGING = false; private final Lexer lexer; private final EventHandler eventHandler; private final List comments; private final ParsingMode parsingMode; private static final Map binaryOperators = new ImmutableMap.Builder() .put(TokenKind.AND, Operator.AND) .put(TokenKind.EQUALS_EQUALS, Operator.EQUALS_EQUALS) .put(TokenKind.GREATER, Operator.GREATER) .put(TokenKind.GREATER_EQUALS, Operator.GREATER_EQUALS) .put(TokenKind.IN, Operator.IN) .put(TokenKind.LESS, Operator.LESS) .put(TokenKind.LESS_EQUALS, Operator.LESS_EQUALS) .put(TokenKind.MINUS, Operator.MINUS) .put(TokenKind.NOT_EQUALS, Operator.NOT_EQUALS) .put(TokenKind.NOT_IN, Operator.NOT_IN) .put(TokenKind.OR, Operator.OR) .put(TokenKind.PERCENT, Operator.PERCENT) .put(TokenKind.SLASH, Operator.DIVIDE) .put(TokenKind.PLUS, Operator.PLUS) .put(TokenKind.PIPE, Operator.PIPE) .put(TokenKind.STAR, Operator.MULT) .build(); // TODO(bazel-team): add support for |= private static final Map augmentedAssignmentMethods = new ImmutableMap.Builder() .put(TokenKind.PLUS_EQUALS, Operator.PLUS) .put(TokenKind.MINUS_EQUALS, Operator.MINUS) .put(TokenKind.STAR_EQUALS, Operator.MULT) .put(TokenKind.SLASH_EQUALS, Operator.DIVIDE) .put(TokenKind.PERCENT_EQUALS, Operator.PERCENT) .build(); /** Highest precedence goes last. * Based on: http://docs.python.org/2/reference/expressions.html#operator-precedence **/ private static final List> operatorPrecedence = ImmutableList.of( EnumSet.of(Operator.OR), EnumSet.of(Operator.AND), EnumSet.of(Operator.NOT), EnumSet.of(Operator.EQUALS_EQUALS, Operator.NOT_EQUALS, Operator.LESS, Operator.LESS_EQUALS, Operator.GREATER, Operator.GREATER_EQUALS, Operator.IN, Operator.NOT_IN), EnumSet.of(Operator.PIPE), EnumSet.of(Operator.MINUS, Operator.PLUS), EnumSet.of(Operator.DIVIDE, Operator.MULT, Operator.PERCENT)); private final Iterator tokens; private int errorsCount; private boolean recoveryMode; // stop reporting errors until next statement private Parser(Lexer lexer, EventHandler eventHandler, ParsingMode parsingMode) { this.lexer = lexer; this.eventHandler = eventHandler; this.parsingMode = parsingMode; this.tokens = lexer.getTokens().iterator(); this.comments = new ArrayList<>(); nextToken(); } private static Location locationFromStatements(Lexer lexer, List statements) { if (!statements.isEmpty()) { return lexer.createLocation( statements.get(0).getLocation().getStartOffset(), Iterables.getLast(statements).getLocation().getEndOffset()); } else { return Location.fromPathFragment(lexer.getFilename()); } } /** * Entry-point to parser that parses a build file with comments. All errors encountered during * parsing are reported via "reporter". */ public static ParseResult parseFile(ParserInputSource input, EventHandler eventHandler) { Lexer lexer = new Lexer(input, eventHandler); Parser parser = new Parser(lexer, eventHandler, BUILD); List statements = parser.parseFileInput(); return new ParseResult(statements, parser.comments, locationFromStatements(lexer, statements), parser.errorsCount > 0 || lexer.containsErrors()); } /** * Entry-point to parser that parses a build file with comments. All errors encountered during * parsing are reported via "reporter". Enable Skylark extensions that are not part of the core * BUILD language. */ public static ParseResult parseFileForSkylark( ParserInputSource input, EventHandler eventHandler) { Lexer lexer = new Lexer(input, eventHandler); Parser parser = new Parser(lexer, eventHandler, SKYLARK); List statements = parser.parseFileInput(); return new ParseResult( statements, parser.comments, locationFromStatements(lexer, statements), parser.errorsCount > 0 || lexer.containsErrors()); } /** * Entry-point to parser that parses an expression. All errors encountered * during parsing are reported via "reporter". The expression may be followed * by newline tokens. */ @VisibleForTesting public static Expression parseExpression(ParserInputSource input, EventHandler eventHandler) { Lexer lexer = new Lexer(input, eventHandler); Parser parser = new Parser(lexer, eventHandler, null); Expression result = parser.parseExpression(); while (parser.token.kind == TokenKind.NEWLINE) { parser.nextToken(); } parser.expect(TokenKind.EOF); return result; } private void reportError(Location location, String message) { errorsCount++; // Limit the number of reported errors to avoid spamming output. if (errorsCount <= 5) { eventHandler.handle(Event.error(location, message)); } } private void syntaxError(Token token, String message) { if (!recoveryMode) { String msg = token.kind == TokenKind.INDENT ? "indentation error" : "syntax error at '" + token + "': " + message; reportError(lexer.createLocation(token.left, token.right), msg); recoveryMode = true; } } /** * Consumes the current token. If it is not of the specified (expected) * kind, reports a syntax error. */ private boolean expect(TokenKind kind) { boolean expected = token.kind == kind; if (!expected) { syntaxError(token, "expected " + kind.getPrettyName()); } nextToken(); return expected; } /** * Same as expect, but stop the recovery mode if the token was expected. */ private void expectAndRecover(TokenKind kind) { if (expect(kind)) { recoveryMode = false; } } /** * Consume tokens past the first token that has a kind that is in the set of * teminatingTokens. * @param terminatingTokens * @return the end offset of the terminating token. */ private int syncPast(EnumSet terminatingTokens) { Preconditions.checkState(terminatingTokens.contains(TokenKind.EOF)); while (!terminatingTokens.contains(token.kind)) { nextToken(); } int end = token.right; // read past the synchronization token nextToken(); return end; } /** * Consume tokens until we reach the first token that has a kind that is in * the set of teminatingTokens. * @param terminatingTokens * @return the end offset of the terminating token. */ private int syncTo(EnumSet terminatingTokens) { // EOF must be in the set to prevent an infinite loop Preconditions.checkState(terminatingTokens.contains(TokenKind.EOF)); // read past the problematic token int previous = token.right; nextToken(); int current = previous; while (!terminatingTokens.contains(token.kind)) { nextToken(); previous = current; current = token.right; } return previous; } // Keywords that exist in Python and that we don't parse. private static final EnumSet FORBIDDEN_KEYWORDS = EnumSet.of(TokenKind.AS, TokenKind.ASSERT, TokenKind.DEL, TokenKind.EXCEPT, TokenKind.FINALLY, TokenKind.FROM, TokenKind.GLOBAL, TokenKind.IMPORT, TokenKind.IS, TokenKind.LAMBDA, TokenKind.NONLOCAL, TokenKind.RAISE, TokenKind.TRY, TokenKind.WITH, TokenKind.WHILE, TokenKind.YIELD); private void checkForbiddenKeywords(Token token) { if (!FORBIDDEN_KEYWORDS.contains(token.kind)) { return; } String error; switch (token.kind) { case ASSERT: error = "'assert' not supported, use 'fail' instead"; break; case DEL: error = "'del' not supported, use '.pop()' to delete an item from a dictionary or a list"; break; case IMPORT: error = "'import' not supported, use 'load' instead"; break; case IS: error = "'is' not supported, use '==' instead"; break; case LAMBDA: error = "'lambda' not supported, declare a function instead"; break; case RAISE: error = "'raise' not supported, use 'fail' instead"; break; case TRY: error = "'try' not supported, all exceptions are fatal"; break; case WHILE: error = "'while' not supported, use 'for' instead"; break; default: error = "keyword '" + token.kind.getPrettyName() + "' not supported"; break; } reportError(lexer.createLocation(token.left, token.right), error); } private void nextToken() { if (pushedToken != null) { token = pushedToken; pushedToken = null; } else { if (token == null || token.kind != TokenKind.EOF) { token = tokens.next(); // transparently handle comment tokens while (token.kind == TokenKind.COMMENT) { makeComment(token); token = tokens.next(); } } } checkForbiddenKeywords(token); if (DEBUGGING) { System.err.print(token); } } private void pushToken(Token tokenToPush) { if (pushedToken != null) { throw new IllegalStateException("Exceeded LL(2) lookahead!"); } pushedToken = token; token = tokenToPush; } // create an error expression private Identifier makeErrorExpression(int start, int end) { return setLocation(new Identifier("$error$"), start, end); } // Convenience wrapper around ASTNode.setLocation that returns the node. private NODE setLocation(NODE node, Location location) { return ASTNode.setLocation(location, node); } // Another convenience wrapper method around ASTNode.setLocation private NODE setLocation(NODE node, int startOffset, int endOffset) { return setLocation(node, lexer.createLocation(startOffset, endOffset)); } // Convenience method that uses end offset from the last node. private NODE setLocation(NODE node, int startOffset, ASTNode lastNode) { Preconditions.checkNotNull(lastNode, "can't extract end offset from a null node"); Preconditions.checkNotNull(lastNode.getLocation(), "lastNode doesn't have a location"); return setLocation(node, startOffset, lastNode.getLocation().getEndOffset()); } // create a funcall expression private Expression makeFuncallExpression(Expression receiver, Identifier function, List args, int start, int end) { if (function.getLocation() == null) { function = setLocation(function, start, end); } return setLocation(new FuncallExpression(receiver, function, args), start, end); } // arg ::= IDENTIFIER '=' nontupleexpr // | expr // | *args (only in Skylark mode) // | **kwargs (only in Skylark mode) // To keep BUILD files declarative and easy to process, *args and **kwargs // arguments are allowed only in Skylark mode. private Argument.Passed parseFuncallArgument() { final int start = token.left; // parse **expr if (token.kind == TokenKind.STAR_STAR) { if (parsingMode != SKYLARK) { reportError( lexer.createLocation(token.left, token.right), "**kwargs arguments are not allowed in BUILD files"); } nextToken(); Expression expr = parseNonTupleExpression(); return setLocation(new Argument.StarStar(expr), start, expr); } // parse *expr if (token.kind == TokenKind.STAR) { if (parsingMode != SKYLARK) { reportError( lexer.createLocation(token.left, token.right), "*args arguments are not allowed in BUILD files"); } nextToken(); Expression expr = parseNonTupleExpression(); return setLocation(new Argument.Star(expr), start, expr); } // parse keyword = expr if (token.kind == TokenKind.IDENTIFIER) { Token identToken = token; String name = (String) token.value; nextToken(); if (token.kind == TokenKind.EQUALS) { // it's a named argument nextToken(); Expression expr = parseNonTupleExpression(); return setLocation(new Argument.Keyword(name, expr), start, expr); } else { // oops, back up! pushToken(identToken); } } // parse a positional argument Expression expr = parseNonTupleExpression(); return setLocation(new Argument.Positional(expr), start, expr); } // arg ::= IDENTIFIER '=' nontupleexpr // | IDENTIFIER private Parameter parseFunctionParameter() { // TODO(bazel-team): optionally support type annotations int start = token.left; if (token.kind == TokenKind.STAR_STAR) { // kwarg nextToken(); Identifier ident = parseIdent(); return setLocation(new Parameter.StarStar( ident.getName()), start, ident); } else if (token.kind == TokenKind.STAR) { // stararg int end = token.right; nextToken(); if (token.kind == TokenKind.IDENTIFIER) { Identifier ident = parseIdent(); return setLocation(new Parameter.Star(ident.getName()), start, ident); } else { return setLocation(new Parameter.Star(null), start, end); } } else { Identifier ident = parseIdent(); if (token.kind == TokenKind.EQUALS) { // there's a default value nextToken(); Expression expr = parseNonTupleExpression(); return setLocation(new Parameter.Optional( ident.getName(), expr), start, expr); } else { return setLocation(new Parameter.Mandatory( ident.getName()), start, ident); } } } // funcall_suffix ::= '(' arg_list? ')' private Expression parseFuncallSuffix(int start, Expression receiver, Identifier function) { List args = Collections.emptyList(); expect(TokenKind.LPAREN); int end; if (token.kind == TokenKind.RPAREN) { end = token.right; nextToken(); // RPAREN } else { args = parseFuncallArguments(); // (includes optional trailing comma) end = token.right; expect(TokenKind.RPAREN); } return makeFuncallExpression(receiver, function, args, start, end); } // selector_suffix ::= '.' IDENTIFIER // |'.' IDENTIFIER funcall_suffix private Expression parseSelectorSuffix(int start, Expression receiver) { expect(TokenKind.DOT); if (token.kind == TokenKind.IDENTIFIER) { Identifier ident = parseIdent(); if (token.kind == TokenKind.LPAREN) { return parseFuncallSuffix(start, receiver, ident); } else { return setLocation(new DotExpression(receiver, ident), start, token.right); } } else { syntaxError(token, "expected identifier after dot"); int end = syncTo(EXPR_TERMINATOR_SET); return makeErrorExpression(start, end); } } // arg_list ::= ( (arg ',')* arg ','? )? private List parseFuncallArguments() { List arguments = parseFunctionArguments(new Supplier() { @Override public Argument.Passed get() { return parseFuncallArgument(); } }); try { Argument.validateFuncallArguments(arguments); } catch (Argument.ArgumentException e) { reportError(lexer.createLocation(token.left, token.right), e.getMessage()); } return arguments; } // expr_list parses a comma-separated list of expression. It assumes that the // first expression was already parsed, so it starts with a comma. // It is used to parse tuples and list elements. // expr_list ::= ( ',' expr )* ','? private List parseExprList(boolean trailingColonAllowed) { List list = new ArrayList<>(); // terminating tokens for an expression list while (token.kind == TokenKind.COMMA) { expect(TokenKind.COMMA); if (EXPR_LIST_TERMINATOR_SET.contains(token.kind)) { if (!trailingColonAllowed) { reportError( lexer.createLocation(token.left, token.right), "Trailing comma is allowed only in parenthesized tuples."); } break; } list.add(parseNonTupleExpression()); } return list; } // dict_entry_list ::= ( (dict_entry ',')* dict_entry ','? )? private List parseDictEntryList() { List list = new ArrayList<>(); // the terminating token for a dict entry list while (token.kind != TokenKind.RBRACE) { list.add(parseDictEntry()); if (token.kind == TokenKind.COMMA) { nextToken(); } else { break; } } return list; } // dict_entry ::= nontupleexpr ':' nontupleexpr private DictionaryEntryLiteral parseDictEntry() { int start = token.left; Expression key = parseNonTupleExpression(); expect(TokenKind.COLON); Expression value = parseNonTupleExpression(); return setLocation(new DictionaryEntryLiteral(key, value), start, value); } /** * Parse a String literal value, e.g. "str". */ private StringLiteral parseStringLiteral() { Preconditions.checkState(token.kind == TokenKind.STRING); int end = token.right; StringLiteral literal = setLocation(new StringLiteral((String) token.value), token.left, end); nextToken(); if (token.kind == TokenKind.STRING) { reportError(lexer.createLocation(end, token.left), "Implicit string concatenation is forbidden, use the + operator"); } return literal; } // primary ::= INTEGER // | STRING // | STRING '.' IDENTIFIER funcall_suffix // | IDENTIFIER // | IDENTIFIER funcall_suffix // | IDENTIFIER '.' selector_suffix // | list_expression // | '(' ')' // a tuple with zero elements // | '(' expr ')' // a parenthesized expression // | dict_expression // | '-' primary_with_suffix private Expression parsePrimary() { int start = token.left; switch (token.kind) { case INT: { IntegerLiteral literal = new IntegerLiteral((Integer) token.value); setLocation(literal, start, token.right); nextToken(); return literal; } case STRING: return parseStringLiteral(); case IDENTIFIER: { Identifier ident = parseIdent(); if (token.kind == TokenKind.LPAREN) { // it's a function application return parseFuncallSuffix(start, null, ident); } else { return ident; } } case LBRACKET: // it's a list return parseListMaker(); case LBRACE: // it's a dictionary return parseDictExpression(); case LPAREN: { nextToken(); // check for the empty tuple literal if (token.kind == TokenKind.RPAREN) { ListLiteral literal = ListLiteral.makeTuple(Collections.emptyList()); setLocation(literal, start, token.right); nextToken(); return literal; } // parse the first expression Expression expression = parseExpression(true); setLocation(expression, start, token.right); if (token.kind == TokenKind.RPAREN) { nextToken(); return expression; } expect(TokenKind.RPAREN); int end = syncTo(EXPR_TERMINATOR_SET); return makeErrorExpression(start, end); } case MINUS: { nextToken(); List args = new ArrayList<>(); Expression expr = parsePrimaryWithSuffix(); args.add(setLocation(new Argument.Positional(expr), start, expr)); return makeFuncallExpression(null, new Identifier("-"), args, start, token.right); } default: { syntaxError(token, "expected expression"); int end = syncTo(EXPR_TERMINATOR_SET); return makeErrorExpression(start, end); } } } // primary_with_suffix ::= primary selector_suffix* // | primary substring_suffix private Expression parsePrimaryWithSuffix() { int start = token.left; Expression receiver = parsePrimary(); while (true) { if (token.kind == TokenKind.DOT) { receiver = parseSelectorSuffix(start, receiver); } else if (token.kind == TokenKind.LBRACKET) { receiver = parseSubstringSuffix(start, receiver); } else { break; } } return receiver; } // substring_suffix ::= '[' expression? ':' expression? ':' expression? ']' private Expression parseSubstringSuffix(int start, Expression receiver) { Expression startExpr; expect(TokenKind.LBRACKET); if (token.kind == TokenKind.COLON) { startExpr = setLocation(new Identifier("None"), token.left, token.right); } else { startExpr = parseExpression(); } // This is an index/key access if (token.kind == TokenKind.RBRACKET) { expect(TokenKind.RBRACKET); return setLocation(new IndexExpression(receiver, startExpr), start, token.right); } // This is a slice (or substring) Expression endExpr = parseSliceArgument(new Identifier("None")); Expression stepExpr = parseSliceArgument(new IntegerLiteral(1)); expect(TokenKind.RBRACKET); return setLocation(new SliceExpression(receiver, startExpr, endExpr, stepExpr), start, token.right); } /** * Parses {@code [':' [expr]]} which can either be the end or the step argument of a slice * operation. If no such expression is found, this method returns an argument that represents * {@code defaultValue}. */ private Expression parseSliceArgument(Expression defaultValue) { Expression explicitArg = getSliceEndOrStepExpression(); if (explicitArg == null) { return setLocation(defaultValue, token.left, token.right); } return explicitArg; } private Expression getSliceEndOrStepExpression() { // There has to be a colon before any end or slice argument. // However, if the next token thereafter is another colon or a right bracket, no argument value // was specified. if (token.kind == TokenKind.COLON) { expect(TokenKind.COLON); if (token.kind != TokenKind.COLON && token.kind != TokenKind.RBRACKET) { return parseNonTupleExpression(); } } return null; } // Equivalent to 'exprlist' rule in Python grammar. // loop_variables ::= primary_with_suffix ( ',' primary_with_suffix )* ','? private Expression parseForLoopVariables() { // We cannot reuse parseExpression because it would parse the 'in' operator. // e.g. "for i in e: pass" -> we want to parse only "i" here. int start = token.left; Expression e1 = parsePrimaryWithSuffix(); if (token.kind != TokenKind.COMMA) { return e1; } // It's a tuple List tuple = new ArrayList<>(); tuple.add(e1); while (token.kind == TokenKind.COMMA) { expect(TokenKind.COMMA); if (EXPR_LIST_TERMINATOR_SET.contains(token.kind)) { break; } tuple.add(parsePrimaryWithSuffix()); } return setLocation(ListLiteral.makeTuple(tuple), start, token.right); } // comprehension_suffix ::= 'FOR' loop_variables 'IN' expr comprehension_suffix // | 'IF' expr comprehension_suffix // | ']' private Expression parseComprehensionSuffix( AbstractComprehension comprehension, TokenKind closingBracket) { while (true) { if (token.kind == TokenKind.FOR) { nextToken(); Expression loopVar = parseForLoopVariables(); expect(TokenKind.IN); // The expression cannot be a ternary expression ('x if y else z') due to // conflicts in Python grammar ('if' is used by the comprehension). Expression listExpression = parseNonTupleExpression(0); comprehension.addFor(loopVar, listExpression); } else if (token.kind == TokenKind.IF) { nextToken(); comprehension.addIf(parseExpression()); } else if (token.kind == closingBracket) { nextToken(); return comprehension; } else { syntaxError(token, "expected '" + closingBracket.getPrettyName() + "', 'for' or 'if'"); syncPast(LIST_TERMINATOR_SET); return makeErrorExpression(token.left, token.right); } } } // list_maker ::= '[' ']' // |'[' expr ']' // |'[' expr expr_list ']' // |'[' expr ('FOR' loop_variables 'IN' expr)+ ']' private Expression parseListMaker() { int start = token.left; expect(TokenKind.LBRACKET); if (token.kind == TokenKind.RBRACKET) { // empty List ListLiteral literal = ListLiteral.emptyList(); setLocation(literal, start, token.right); nextToken(); return literal; } Expression expression = parseNonTupleExpression(); Preconditions.checkNotNull(expression, "null element in list in AST at %s:%s", token.left, token.right); switch (token.kind) { case RBRACKET: // singleton List { ListLiteral literal = ListLiteral.makeList(Collections.singletonList(expression)); setLocation(literal, start, token.right); nextToken(); return literal; } case FOR: { // list comprehension Expression result = parseComprehensionSuffix(new ListComprehension(expression), TokenKind.RBRACKET); return setLocation(result, start, token.right); } case COMMA: { List list = parseExprList(true); Preconditions.checkState( !list.contains(null), "null element in list in AST at %s:%s", token.left, token.right); list.add(0, expression); if (token.kind == TokenKind.RBRACKET) { ListLiteral literal = ListLiteral.makeList(list); setLocation(literal, start, token.right); nextToken(); return literal; } expect(TokenKind.RBRACKET); int end = syncPast(LIST_TERMINATOR_SET); return makeErrorExpression(start, end); } default: { syntaxError(token, "expected ',', 'for' or ']'"); int end = syncPast(LIST_TERMINATOR_SET); return makeErrorExpression(start, end); } } } // dict_expression ::= '{' '}' // |'{' dict_entry_list '}' // |'{' dict_entry 'FOR' loop_variables 'IN' expr '}' private Expression parseDictExpression() { int start = token.left; expect(TokenKind.LBRACE); if (token.kind == TokenKind.RBRACE) { // empty Dict DictionaryLiteral literal = DictionaryLiteral.emptyDict(); setLocation(literal, start, token.right); nextToken(); return literal; } DictionaryEntryLiteral entry = parseDictEntry(); if (token.kind == TokenKind.FOR) { // Dict comprehension Expression result = parseComprehensionSuffix( new DictComprehension(entry.getKey(), entry.getValue()), TokenKind.RBRACE); return setLocation(result, start, token.right); } List entries = new ArrayList<>(); entries.add(entry); if (token.kind == TokenKind.COMMA) { expect(TokenKind.COMMA); entries.addAll(parseDictEntryList()); } if (token.kind == TokenKind.RBRACE) { DictionaryLiteral literal = new DictionaryLiteral(entries); setLocation(literal, start, token.right); nextToken(); return literal; } expect(TokenKind.RBRACE); int end = syncPast(DICT_TERMINATOR_SET); return makeErrorExpression(start, end); } private Identifier parseIdent() { if (token.kind != TokenKind.IDENTIFIER) { expect(TokenKind.IDENTIFIER); return makeErrorExpression(token.left, token.right); } Identifier ident = new Identifier(((String) token.value)); setLocation(ident, token.left, token.right); nextToken(); return ident; } // binop_expression ::= binop_expression OP binop_expression // | parsePrimaryWithSuffix // This function takes care of precedence between operators (see operatorPrecedence for // the order), and it assumes left-to-right associativity. private Expression parseBinOpExpression(int prec) { int start = token.left; Expression expr = parseNonTupleExpression(prec + 1); // The loop is not strictly needed, but it prevents risks of stack overflow. Depth is // limited to number of different precedence levels (operatorPrecedence.size()). for (;;) { if (token.kind == TokenKind.NOT) { // If NOT appears when we expect a binary operator, it must be followed by IN. // Since the code expects every operator to be a single token, we push a NOT_IN token. expect(TokenKind.NOT); expect(TokenKind.IN); pushToken(new Token(TokenKind.NOT_IN, token.left, token.right)); } if (!binaryOperators.containsKey(token.kind)) { return expr; } Operator operator = binaryOperators.get(token.kind); if (!operatorPrecedence.get(prec).contains(operator)) { return expr; } nextToken(); Expression secondary = parseNonTupleExpression(prec + 1); expr = optimizeBinOpExpression(operator, expr, secondary); setLocation(expr, start, secondary); } } // Optimize binary expressions. // string literal + string literal can be concatenated into one string literal // so we don't have to do the expensive string concatenation at runtime. private Expression optimizeBinOpExpression( Operator operator, Expression expr, Expression secondary) { if (operator == Operator.PLUS) { if (expr instanceof StringLiteral && secondary instanceof StringLiteral) { StringLiteral left = (StringLiteral) expr; StringLiteral right = (StringLiteral) secondary; return new StringLiteral(left.getValue() + right.getValue()); } } return new BinaryOperatorExpression(operator, expr, secondary); } private Expression parseExpression() { return parseExpression(false); } // Equivalent to 'testlist' rule in Python grammar. It can parse every kind of // expression. In many cases, we need to use parseNonTupleExpression to avoid ambiguity: // e.g. fct(x, y) vs fct((x, y)) // // Tuples can have a trailing comma only when insideParens is true. This prevents bugs // where a one-element tuple is surprisingly created: // e.g. foo = f(x), private Expression parseExpression(boolean insideParens) { int start = token.left; Expression expression = parseNonTupleExpression(); if (token.kind != TokenKind.COMMA) { return expression; } // It's a tuple List tuple = parseExprList(insideParens); tuple.add(0, expression); // add the first expression to the front of the tuple return setLocation(ListLiteral.makeTuple(tuple), start, token.right); } // Equivalent to 'test' rule in Python grammar. private Expression parseNonTupleExpression() { int start = token.left; Expression expr = parseNonTupleExpression(0); if (token.kind == TokenKind.IF) { nextToken(); Expression condition = parseNonTupleExpression(0); if (token.kind == TokenKind.ELSE) { nextToken(); Expression elseClause = parseNonTupleExpression(); return setLocation(new ConditionalExpression(expr, condition, elseClause), start, elseClause); } else { reportError(lexer.createLocation(start, token.left), "missing else clause in conditional expression or semicolon before if"); return expr; // Try to recover from error: drop the if and the expression after it. Ouch. } } return expr; } private Expression parseNonTupleExpression(int prec) { if (prec >= operatorPrecedence.size()) { return parsePrimaryWithSuffix(); } if (token.kind == TokenKind.NOT && operatorPrecedence.get(prec).contains(Operator.NOT)) { return parseNotExpression(prec); } return parseBinOpExpression(prec); } // not_expr :== 'not' expr private Expression parseNotExpression(int prec) { int start = token.left; expect(TokenKind.NOT); Expression expression = parseNonTupleExpression(prec + 1); NotExpression notExpression = new NotExpression(expression); return setLocation(notExpression, start, token.right); } // file_input ::= ('\n' | stmt)* EOF private List parseFileInput() { long startTime = Profiler.nanoTimeMaybe(); List list = new ArrayList<>(); while (token.kind != TokenKind.EOF) { if (token.kind == TokenKind.NEWLINE) { expectAndRecover(TokenKind.NEWLINE); } else if (recoveryMode) { // If there was a parse error, we want to recover here // before starting a new top-level statement. syncTo(STATEMENT_TERMINATOR_SET); recoveryMode = false; } else { parseTopLevelStatement(list); } } Profiler.instance().logSimpleTask(startTime, ProfilerTask.SKYLARK_PARSER, ""); return list; } // load '(' STRING (COMMA [IDENTIFIER EQUALS] STRING)* COMMA? ')' private void parseLoad(List list) { int start = token.left; if (token.kind != TokenKind.STRING) { expect(TokenKind.STRING); return; } StringLiteral importString = parseStringLiteral(); expect(TokenKind.COMMA); Map symbols = new HashMap<>(); parseLoadSymbol(symbols); // At least one symbol is required while (token.kind != TokenKind.RPAREN && token.kind != TokenKind.EOF) { expect(TokenKind.COMMA); if (token.kind == TokenKind.RPAREN) { break; } parseLoadSymbol(symbols); } expect(TokenKind.RPAREN); LoadStatement stmt = new LoadStatement(importString, symbols); list.add(setLocation(stmt, start, token.left)); } /** * Parses the next symbol argument of a load statement and puts it into the output map. * *

The symbol is either "name" (STRING) or name = "declared" (IDENTIFIER EQUALS STRING). * If no alias is used, "name" and "declared" will be identical. "Declared" refers to the * original name in the Bazel file that should be loaded, while "name" will be the key of the * entry in the map. */ private void parseLoadSymbol(Map symbols) { Token nameToken; Token declaredToken; if (token.kind == TokenKind.STRING) { nameToken = token; declaredToken = nameToken; } else { if (token.kind != TokenKind.IDENTIFIER) { syntaxError(token, "Expected either a literal string or an identifier"); } nameToken = token; expect(TokenKind.IDENTIFIER); expect(TokenKind.EQUALS); declaredToken = token; } expect(TokenKind.STRING); try { Identifier identifier = new Identifier(nameToken.value.toString()); if (symbols.containsKey(identifier)) { syntaxError( nameToken, String.format("Identifier '%s' is used more than once", identifier.getName())); } else { symbols.put( setLocation(identifier, nameToken.left, nameToken.right), declaredToken.value.toString()); } } catch (NullPointerException npe) { // This means that the value of at least one token is null. In this case, the previous // expect() call has already logged an error. } } private void parseTopLevelStatement(List list) { // In Python grammar, there is no "top-level statement" and imports are // considered as "small statements". We are a bit stricter than Python here. // Check if there is an include if (token.kind == TokenKind.IDENTIFIER) { Token identToken = token; Identifier ident = parseIdent(); if (ident.getName().equals("load") && token.kind == TokenKind.LPAREN) { expect(TokenKind.LPAREN); parseLoad(list); return; } pushToken(identToken); // push the ident back to parse it as a statement } parseStatement(list, true); } // small_stmt | 'pass' private void parseSmallStatementOrPass(List list) { if (token.kind == TokenKind.PASS) { // Skip the token, don't add it to the list. // It has no existence in the AST. expect(TokenKind.PASS); } else { list.add(parseSmallStatement()); } } // simple_stmt ::= small_stmt (';' small_stmt)* ';'? NEWLINE private void parseSimpleStatement(List list) { parseSmallStatementOrPass(list); while (token.kind == TokenKind.SEMI) { nextToken(); if (token.kind == TokenKind.NEWLINE) { break; } parseSmallStatementOrPass(list); } expectAndRecover(TokenKind.NEWLINE); } // small_stmt ::= assign_stmt // | expr // | RETURN expr // | flow_stmt // assign_stmt ::= expr ('=' | augassign) expr // augassign ::= ('+=' | '-=' | '*=' | '/=' | '%=') // Note that these are in Python, but not implemented here (at least for now): // '&=' | '|=' | '^=' |'<<=' | '>>=' | '**=' | '//=' // Semantic difference from Python: // In Skylark, x += y is simple syntactic sugar for x = x + y. // In Python, x += y is more or less equivalent to x = x + y, but if a method is defined // on x.__iadd__(y), then it takes precedence, and in the case of lists it side-effects // the original list (it doesn't do that on tuples); if no such method is defined it falls back // to the x.__add__(y) method that backs x + y. In Skylark, we don't support this side-effect. // Note also that there is a special casing to translate 'ident[key] = value' // to 'ident = ident + {key: value}'. This is needed to support the pure version of Python-like // dictionary assignment syntax. private Statement parseSmallStatement() { int start = token.left; if (token.kind == TokenKind.RETURN) { return parseReturnStatement(); } else if (token.kind == TokenKind.BREAK || token.kind == TokenKind.CONTINUE) { return parseFlowStatement(token.kind); } Expression expression = parseExpression(); if (token.kind == TokenKind.EQUALS) { nextToken(); Expression rvalue = parseExpression(); return setLocation(new AssignmentStatement(expression, rvalue), start, rvalue); } else if (augmentedAssignmentMethods.containsKey(token.kind)) { Operator operator = augmentedAssignmentMethods.get(token.kind); nextToken(); Expression operand = parseExpression(); int end = operand.getLocation().getEndOffset(); return setLocation( new AugmentedAssignmentStatement(operator, expression, operand), start, end); } else { return setLocation(new ExpressionStatement(expression), start, expression); } } // if_stmt ::= IF expr ':' suite [ELIF expr ':' suite]* [ELSE ':' suite]? private IfStatement parseIfStatement() { int start = token.left; List thenBlocks = new ArrayList<>(); thenBlocks.add(parseConditionalStatements(TokenKind.IF)); while (token.kind == TokenKind.ELIF) { thenBlocks.add(parseConditionalStatements(TokenKind.ELIF)); } List elseBlock; if (token.kind == TokenKind.ELSE) { expect(TokenKind.ELSE); expect(TokenKind.COLON); elseBlock = parseSuite(); } else { elseBlock = ImmutableList.of(); } return setLocation(new IfStatement(thenBlocks, elseBlock), start, token.right); } // cond_stmts ::= [EL]IF expr ':' suite private ConditionalStatements parseConditionalStatements(TokenKind tokenKind) { int start = token.left; expect(tokenKind); Expression expr = parseNonTupleExpression(); expect(TokenKind.COLON); List thenBlock = parseSuite(); ConditionalStatements stmt = new ConditionalStatements(expr, thenBlock); return setLocation(stmt, start, token.right); } // for_stmt ::= FOR IDENTIFIER IN expr ':' suite private void parseForStatement(List list) { int start = token.left; expect(TokenKind.FOR); Expression loopVar = parseForLoopVariables(); expect(TokenKind.IN); Expression collection = parseExpression(); expect(TokenKind.COLON); enterLoop(); try { List block = parseSuite(); Statement stmt = new ForStatement(loopVar, collection, block); list.add(setLocation(stmt, start, token.right)); } finally { exitLoop(); } } // def foo(bar1, bar2): private void parseFunctionDefStatement(List list) { int start = token.left; expect(TokenKind.DEF); Identifier ident = parseIdent(); expect(TokenKind.LPAREN); List> params = parseParameters(); FunctionSignature.WithValues signature = functionSignature(params); expect(TokenKind.RPAREN); expect(TokenKind.COLON); List block = parseSuite(); FunctionDefStatement stmt = new FunctionDefStatement(ident, params, signature, block); list.add(setLocation(stmt, start, token.right)); } private FunctionSignature.WithValues functionSignature( List> parameters) { try { return FunctionSignature.WithValues.of(parameters); } catch (FunctionSignature.SignatureException e) { reportError(e.getParameter().getLocation(), e.getMessage()); // return bogus empty signature return FunctionSignature.WithValues.create(FunctionSignature.of()); } } private List> parseParameters() { return parseFunctionArguments( new Supplier>() { @Override public Parameter get() { return parseFunctionParameter(); } }); } /** * Parse a list of Argument-s. The arguments can be of class Argument.Passed or Parameter, * as returned by the Supplier parseArgument (that, taking no argument, must be closed over * the mutable input data structures). * *

This parser does minimal validation: it ensures the proper python use of the comma (that * can terminate before a star but not after) and the fact that a **kwarg must appear last. * It does NOT validate further ordering constraints for a {@code List}, such as * all positional preceding keyword arguments in a call, nor does it check the more subtle * constraints for Parameter-s. This validation must happen afterwards in an appropriate method. */ private ImmutableList parseFunctionArguments(Supplier parseArgument) { boolean hasArg = false; boolean hasStar = false; boolean hasStarStar = false; ArrayList arguments = new ArrayList<>(); while (token.kind != TokenKind.RPAREN && token.kind != TokenKind.EOF) { if (hasStarStar) { reportError(lexer.createLocation(token.left, token.right), "unexpected tokens after kwarg"); break; } if (hasArg) { expect(TokenKind.COMMA); } if (token.kind == TokenKind.RPAREN && !hasStar) { // list can end with a COMMA if there is neither * nor ** break; } V arg = parseArgument.get(); hasArg = true; if (arg.isStar()) { hasStar = true; } else if (arg.isStarStar()) { hasStarStar = true; } arguments.add(arg); } return ImmutableList.copyOf(arguments); } // suite is typically what follows a colon (e.g. after def or for). // suite ::= simple_stmt // | NEWLINE INDENT stmt+ OUTDENT private List parseSuite() { List list = new ArrayList<>(); if (token.kind == TokenKind.NEWLINE) { expect(TokenKind.NEWLINE); if (token.kind != TokenKind.INDENT) { reportError(lexer.createLocation(token.left, token.right), "expected an indented block"); return list; } expect(TokenKind.INDENT); while (token.kind != TokenKind.OUTDENT && token.kind != TokenKind.EOF) { parseStatement(list, false); } expectAndRecover(TokenKind.OUTDENT); } else { parseSimpleStatement(list); } return list; } // skipSuite does not check that the code is syntactically correct, it // just skips based on indentation levels. private void skipSuite() { if (token.kind == TokenKind.NEWLINE) { expect(TokenKind.NEWLINE); if (token.kind != TokenKind.INDENT) { reportError(lexer.createLocation(token.left, token.right), "expected an indented block"); return; } expect(TokenKind.INDENT); // Don't try to parse all the Python syntax, just skip the block // until the corresponding outdent token. int depth = 1; while (depth > 0) { // Because of the way the lexer works, this should never happen Preconditions.checkState(token.kind != TokenKind.EOF); if (token.kind == TokenKind.INDENT) { depth++; } if (token.kind == TokenKind.OUTDENT) { depth--; } nextToken(); } } else { // the block ends at the newline token // e.g. if x == 3: print "three" syncTo(STATEMENT_TERMINATOR_SET); } } // stmt ::= simple_stmt // | compound_stmt private void parseStatement(List list, boolean isTopLevel) { if (token.kind == TokenKind.DEF && parsingMode == SKYLARK) { if (!isTopLevel) { reportError(lexer.createLocation(token.left, token.right), "nested functions are not allowed. Move the function to top-level"); } parseFunctionDefStatement(list); } else if (token.kind == TokenKind.IF && parsingMode == SKYLARK) { list.add(parseIfStatement()); } else if (token.kind == TokenKind.FOR && parsingMode == SKYLARK) { if (isTopLevel) { reportError( lexer.createLocation(token.left, token.right), "for loops are not allowed on top-level. Put it into a function"); } parseForStatement(list); } else if (BLOCK_STARTING_SET.contains(token.kind)) { skipBlock(); } else { parseSimpleStatement(list); } } // flow_stmt ::= break_stmt | continue_stmt private FlowStatement parseFlowStatement(TokenKind kind) { int start = token.left; int end = token.right; expect(kind); if (loopCount == 0) { reportError( lexer.createLocation(start, end), kind.getPrettyName() + " statement must be inside a for loop"); } FlowStatement.Kind flowKind = kind == TokenKind.BREAK ? FlowStatement.Kind.BREAK : FlowStatement.Kind.CONTINUE; return setLocation(new FlowStatement(flowKind), start, end); } // return_stmt ::= RETURN [expr] private ReturnStatement parseReturnStatement() { int start = token.left; int end = token.right; expect(TokenKind.RETURN); Expression expression; if (STATEMENT_TERMINATOR_SET.contains(token.kind)) { // this None makes the AST not correspond to the source exactly anymore expression = new Identifier("None"); setLocation(expression, start, end); } else { expression = parseExpression(); } return setLocation(new ReturnStatement(expression), start, expression); } // block ::= ('if' | 'for' | 'class' | 'try' | 'def') expr ':' suite private void skipBlock() { int start = token.left; Token blockToken = token; syncTo(EnumSet.of(TokenKind.COLON, TokenKind.EOF)); // skip over expression or name if (blockToken.kind == TokenKind.ELSE) { reportError( lexer.createLocation(blockToken.left, blockToken.right), "syntax error at 'else': not allowed here."); } else { String msg = ILLEGAL_BLOCK_KEYWORDS.containsKey(blockToken.kind) ? String.format("%ss are not supported.", ILLEGAL_BLOCK_KEYWORDS.get(blockToken.kind)) : "This is not supported in BUILD files. Move the block to a .bzl file and load it"; reportError( lexer.createLocation(start, token.right), String.format("syntax error at '%s': %s", blockToken, msg)); } expect(TokenKind.COLON); skipSuite(); } // create a comment node private void makeComment(Token token) { comments.add(setLocation(new Comment((String) token.value), token.left, token.right)); } private void enterLoop() { loopCount++; } private void exitLoop() { Preconditions.checkState(loopCount > 0); loopCount--; } }