/* * Copyright 2015-present Facebook, Inc. * * 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. */ // Copyright 2014 Google Inc. 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.facebook.buck.query; import static com.facebook.buck.query.Lexer.BINARY_OPERATORS; import static com.facebook.buck.query.Lexer.TokenKind; import com.facebook.buck.query.QueryEnvironment.Argument; import com.facebook.buck.query.QueryEnvironment.ArgumentType; import com.facebook.buck.query.QueryEnvironment.QueryFunction; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Nullable; /** * LL(1) recursive descent parser for the Blaze query language, revision 2. * * <p>In the grammar below, non-terminals are lowercase and terminals are uppercase, or character * literals. * * <pre> * expr ::= WORD * | '(' expr ')' * | WORD '(' expr ( ',' expr ) * ')' * | expr INTERSECT expr * | expr '^' expr * | expr UNION expr * | expr '+' expr * | expr EXCEPT expr * | expr '-' expr * | SET '(' WORD * ')' * </pre> */ final class QueryParser { private Lexer.Token token; // current lookahead token private final List<Lexer.Token> tokens; private final Iterator<Lexer.Token> tokenIterator; private final Map<String, QueryFunction> functions; /** Scan and parse the specified query expression. */ static QueryExpression parse(String query, QueryEnvironment env) throws QueryException { QueryParser parser = new QueryParser(Lexer.scan(query.toCharArray()), env); QueryExpression expr = parser.parseExpression(); if (parser.token.kind != TokenKind.EOF) { throw new QueryException( "Unexpected token '%s' after query expression '%s'", parser.token, expr); } return expr; } private QueryParser(List<Lexer.Token> tokens, QueryEnvironment env) { this.functions = new HashMap<>(); for (QueryFunction queryFunction : env.getFunctions()) { this.functions.put(queryFunction.getName(), queryFunction); } this.tokens = tokens; this.tokenIterator = tokens.iterator(); nextToken(); } /** Returns an exception. Don't forget to throw it. */ private QueryException syntaxError(Lexer.Token token) { String message = "premature end of input"; if (token.kind != TokenKind.EOF) { StringBuilder buf = new StringBuilder("syntax error at '"); String sep = ""; for (int index = tokens.indexOf(token), max = Math.min(tokens.size() - 1, index + 3); // 3 tokens of context index < max; ++index) { buf.append(sep).append(tokens.get(index)); sep = " "; } buf.append("'"); message = buf.toString(); } return new QueryException(message); } private QueryException syntaxError(QueryException cause, QueryFunction function) { ImmutableList<ArgumentType> mandatoryArguments = function.getArgumentTypes().subList(0, function.getMandatoryArguments()); ImmutableList<ArgumentType> optionalArguments = function .getArgumentTypes() .subList(function.getMandatoryArguments(), function.getArgumentTypes().size()); StringBuilder argumentsString = new StringBuilder(); Joiner.on(", ").appendTo(argumentsString, mandatoryArguments); if (optionalArguments.size() > 0) { argumentsString.append(" [, "); Joiner.on(" [, ").appendTo(argumentsString, optionalArguments); for (int i = 0; i < optionalArguments.size(); i++) { argumentsString.append(" ]"); } } return new QueryException( cause, "`%s` when parsing call to the function `%s(%s)`. Please see " + "https://buckbuild.com/command/query.html#%s for complete documentation.", cause.getMessage(), function.getName(), argumentsString.toString(), function.getName()); } /** * Consumes the current token. If it is not of the specified (expected) kind, throws * QueryException. Returns the value associated with the consumed token, if any. */ @Nullable private String consume(TokenKind kind) throws QueryException { if (token.kind != kind) { throw syntaxError(token); } String word = token.word; nextToken(); return word; } /** * Consumes the current token, which must be a WORD containing an integer literal. Returns that * integer, or throws a QueryException otherwise. */ private int consumeIntLiteral() throws QueryException { String intString = consume(TokenKind.WORD); try { return Integer.parseInt(intString); } catch (NumberFormatException e) { throw new QueryException("expected an integer literal but got '%s'", intString); } } private void nextToken() { if (token == null || token.kind != TokenKind.EOF) { token = tokenIterator.next(); } } /** * expr ::= primary | expr INTERSECT expr | expr '^' expr | expr UNION expr | expr '+' expr | expr * EXCEPT expr | expr '-' expr */ private QueryExpression parseExpression() throws QueryException { // All operators are left-associative and of equal precedence. return parseBinaryOperatorTail(parsePrimary()); } /** * tail ::= ( <op> <primary> )* All operators have equal precedence. This factoring is required * for left-associative binary operators in LL(1). */ private QueryExpression parseBinaryOperatorTail(QueryExpression lhs) throws QueryException { if (!BINARY_OPERATORS.contains(token.kind)) { return lhs; } List<QueryExpression> operands = new ArrayList<>(); operands.add(lhs); TokenKind lastOperator = token.kind; while (BINARY_OPERATORS.contains(token.kind)) { TokenKind operator = token.kind; consume(operator); if (operator != lastOperator) { lhs = new BinaryOperatorExpression(lastOperator, operands); operands.clear(); operands.add(lhs); lastOperator = operator; } QueryExpression rhs = parsePrimary(); operands.add(rhs); } return new BinaryOperatorExpression(lastOperator, operands); } /** * primary ::= WORD | LET WORD = expr IN expr | '(' expr ')' | WORD '(' expr ( ',' expr ) * ')' | * DEPS '(' expr ')' | DEPS '(' expr ',' WORD ')' | RDEPS '(' expr ',' expr ')' | RDEPS '(' expr * ',' expr ',' WORD ')' | SET '(' WORD * ')' */ private QueryExpression parsePrimary() throws QueryException { switch (token.kind) { case WORD: { String word = consume(TokenKind.WORD); if (token.kind == TokenKind.LPAREN) { QueryFunction function = functions.get(word); if (function == null) { throw new QueryException(syntaxError(token), "Unknown function '%s'", word); } ImmutableList.Builder<Argument> argsBuilder = ImmutableList.builder(); consume(TokenKind.LPAREN); int argsSeen = 0; for (ArgumentType type : function.getArgumentTypes()) { // If the next token is a `)` and we've seen all mandatory args, then break out. if (token.kind == TokenKind.RPAREN && argsSeen >= function.getMandatoryArguments()) { break; } // Parse the individual arguments. try { switch (type) { case EXPRESSION: argsBuilder.add(Argument.of(parseExpression())); break; case WORD: argsBuilder.add( Argument.of(Preconditions.checkNotNull(consume(TokenKind.WORD)))); break; case INTEGER: argsBuilder.add(Argument.of(consumeIntLiteral())); break; default: throw new IllegalStateException(); } } catch (QueryException e) { throw syntaxError(e, function); } // If the next argument is a `,`, consume it before continuing to parsing the next // argument. if (token.kind == TokenKind.COMMA) { consume(TokenKind.COMMA); } argsSeen++; } consume(TokenKind.RPAREN); return new FunctionExpression(function, argsBuilder.build()); } else { return new TargetLiteral(Preconditions.checkNotNull(word)); } } case LPAREN: { consume(TokenKind.LPAREN); QueryExpression expr = parseExpression(); consume(TokenKind.RPAREN); return expr; } case SET: { nextToken(); consume(TokenKind.LPAREN); ImmutableList.Builder<TargetLiteral> wordsBuilder = ImmutableList.builder(); while (token.kind == TokenKind.WORD) { wordsBuilder.add( new TargetLiteral(Preconditions.checkNotNull(consume(TokenKind.WORD)))); } consume(TokenKind.RPAREN); return new SetExpression(wordsBuilder.build()); } //$CASES-OMITTED$ default: throw syntaxError(token); } } }