blob: 5712c93b721f46fb999a0c6889babab14d1166d9 [file] [log] [blame]
// Copyright (C) 2019 The Android Open Source Project
//
// 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.gerrit.plugins.checks;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.index.query.QueryParser.AND;
import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
import static com.google.gerrit.index.query.QueryParser.NOT;
import static com.google.gerrit.index.query.QueryParser.OR;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.QueryParser;
import org.antlr.runtime.tree.Tree;
public class CheckerQuery {
// Note that this list contains *operators*, not predicates. If there are multiple operators
// aliased together to the same predicate ("f:", "file:"), they all need to be listed explicitly.
//
// We chose to list operators instead of predicates because:
// * It insulates us from changes in implementation details of the query system, such as
// predicate classes being renamed, or additional predicates being ORed into existing
// operators.
// * It's easier to keep in sync with the documentation.
//
// This doesn't rule out switching to predicates in the future, particularly if the predicate
// classes gain some informative methods like "boolean matchMethodNeedsToQueryIndex()".
//
// Predicates that definitely cannot be allowed:
// * Anything where match() needs to query the index, i.e. any full-text fields. This includes
// the default field.
// * Anything where post-filtering is unreasonably expensive.
// * Any predicate over projects, since that may conflict with the projects field.
//
// Beyond that, this set is mostly based on what we subjectively consider useful for limiting the
// changes that a checker runs on. It will probably grow, based on user feedback.
private static final ImmutableSortedSet<String> ALLOWED_OPERATORS =
ImmutableSortedSet.of(
"added",
"after",
"age",
"assignee",
"author",
"before",
"branch",
"committer",
"deleted",
"delta",
"destination",
"dir",
"directory",
"ext",
"extension",
"f",
"file",
"footer",
"hashtag",
"intopic",
// TODO(dborowitz): Support some is: operators but not others. Definitely not is:starred.
"label",
"onlyextensions",
"onlyexts",
"ownerin",
"path",
"r",
"ref",
"reviewer",
"reviewerin",
"size",
"status",
"submittable",
"topic",
"unresolved",
"wip");
/**
* Cleans a query string for storage in the checker configuration.
*
* <p>The query string is interpreted as a change query. Only a subset of query operators are
* supported, as listed in the REST API documentation and {@link #ALLOWED_OPERATORS}.
*
* @param query a change query string.
* @return the query string, trimmed. May be empty, which indicates all changes match.
* @throws BadRequestException if the query is not a valid query, or it uses operators outside of
* the allowed set.
*/
public static String clean(String query) throws BadRequestException {
String trimmed = requireNonNull(query).trim();
if (trimmed.isEmpty()) {
return trimmed;
}
try {
checkOperators(QueryParser.parse(query));
} catch (QueryParseException e) {
throw new BadRequestException("Invalid query: " + query + "\n" + e.getMessage(), e);
}
return trimmed;
}
private static void checkOperators(Tree node) throws BadRequestException {
switch (node.getType()) {
case AND:
case OR:
case NOT:
for (int i = 0; i < node.getChildCount(); i++) {
checkOperators(node.getChild(i));
}
break;
case FIELD_NAME:
if (!ALLOWED_OPERATORS.contains(node.getText())) {
throw new BadRequestException("Unsupported operator: " + node);
}
break;
case DEFAULT_FIELD:
throw new BadRequestException("Specific search operator required: " + getOnlyChild(node));
default:
throw new BadRequestException("Unsupported query: " + node);
}
}
private static Tree getOnlyChild(Tree node) {
checkState(node.getChildCount() == 1, "expected 1 child: %s", node);
return node.getChild(0);
}
private CheckerQuery() {}
}