/* * Copyright 2011 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.google.errorprone.matchers; import static com.google.errorprone.suppliers.Suppliers.typeFromClass; import com.google.errorprone.VisitorState; import com.google.errorprone.dataflow.nullnesspropagation.Nullness; import com.google.errorprone.matchers.ChildMultiMatcher.MatchType; import com.google.errorprone.matchers.MethodVisibility.Visibility; import com.google.errorprone.matchers.method.MethodMatchers; import com.google.errorprone.matchers.method.MethodMatchers.AnyMethodMatcher; import com.google.errorprone.matchers.method.MethodMatchers.ConstructorMatcher; import com.google.errorprone.matchers.method.MethodMatchers.InstanceMethodMatcher; import com.google.errorprone.matchers.method.MethodMatchers.StaticMethodMatcher; import com.google.errorprone.suppliers.Supplier; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.AssertTree; import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ContinueTree; import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.SynchronizedTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCExpression; import com.sun.tools.javac.tree.JCTree.JCFieldAccess; import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.JCTree.JCNewClass; import com.sun.tools.javac.tree.TreeInfo; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.element.NestingKind; /** * Static factory methods which make the DSL read more fluently. * Since matchers are run in a tight loop during compilation, performance is important. When assembling a matcher * from the DSL, it's best to construct it only once, by saving the resulting matcher as a static variable for example. * * @author alexeagle@google.com (Alex Eagle) */ public class Matchers { private Matchers() {} /** * A matcher that matches any AST node. */ public static <T extends Tree> Matcher<T> anything() { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { return true; } }; } /** * A matcher that matches no AST node. */ public static <T extends Tree> Matcher<T> nothing() { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { return false; } }; } /** * Matches an AST node iff it does not match the given matcher. */ public static <T extends Tree> Matcher<T> not(final Matcher<T> matcher) { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { return !matcher.matches(t, state); } }; } /** * Compose several matchers together, such that the composite matches an AST node iff all the given matchers do. */ @SafeVarargs public static <T extends Tree> Matcher<T> allOf(final Matcher<? super T>... matchers) { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { for (Matcher<? super T> matcher : matchers) { if (!matcher.matches(t, state)) { return false; } } return true; } }; } /** * Compose several matchers together, such that the composite matches an AST node if any of the given matchers do. */ public static <T extends Tree> Matcher<T> anyOf( final Iterable<? extends Matcher<? super T>> matchers) { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { for (Matcher<? super T> matcher : matchers) { if (matcher.matches(t, state)) { return true; } } return false; } }; } @SafeVarargs public static <T extends Tree> Matcher<T> anyOf(final Matcher<? super T>... matchers) { return anyOf(Arrays.<Matcher<? super T>>asList(matchers)); } /** * Matches if an AST node is an instance of the given class. */ public static <T extends Tree> Matcher<T> isInstance(final java.lang.Class<?> klass) { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { return klass.isInstance(t); } }; } /** * Matches an AST node of a given kind, for example, an Annotation or a switch block. */ public static <T extends Tree> Matcher<T> kindIs(final Kind kind) { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { return tree.getKind() == kind; } }; } /** * Matches an AST node which is the same object reference as the given node. */ public static <T extends Tree> Matcher<T> isSame(final Tree t) { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { return tree == t; } }; } /** Matches a static method. */ public static StaticMethodMatcher staticMethod() { return MethodMatchers.staticMethod(); } /** Matches an instance method. */ public static InstanceMethodMatcher instanceMethod() { return MethodMatchers.instanceMethod(); } /** Matches a static or instance method. */ public static AnyMethodMatcher anyMethod() { return MethodMatchers.anyMethod(); } /** Matches a constructor. */ public static ConstructorMatcher constructor() { return MethodMatchers.constructor(); } /** @deprecated prefer {@link MethodMatchers#instanceMethod} */ @Deprecated // TODO(cushon): expunge public static InstanceMethod instanceMethod( Matcher<? super ExpressionTree> receiverMatcher, String methodName) { return new InstanceMethod(receiverMatcher, methodName); } /** * Matches an AST node that represents a non-static field. */ public static Matcher<ExpressionTree> isInstanceField() { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol symbol = ASTHelpers.getSymbol(expressionTree); return symbol != null && symbol.getKind() == ElementKind.FIELD && !symbol.isStatic(); } }; } /** * Matches an AST node that represents a local variable or parameter. */ public static Matcher<ExpressionTree> isVariable() { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol symbol = ASTHelpers.getSymbol(expressionTree); if (symbol == null) { return false; } return symbol.getKind() == ElementKind.LOCAL_VARIABLE || symbol.getKind() == ElementKind.PARAMETER; } }; } /** * Matches a compound assignment operator AST node which matches a given left-operand matcher, a * given right-operand matcher, and a specific compound assignment operator. * * @param operator Which compound assignment operator to match against. * @param leftOperandMatcher The matcher to apply to the left operand. * @param rightOperandMatcher The matcher to apply to the right operand. */ public static CompoundAssignment compoundAssignment( Kind operator, Matcher<ExpressionTree> leftOperandMatcher, Matcher<ExpressionTree> rightOperandMatcher) { Set<Kind> operators = new HashSet<>(1); operators.add(operator); return new CompoundAssignment(operators, leftOperandMatcher, rightOperandMatcher); } /** * Matches a compound assignment operator AST node which matches a given left-operand matcher, a * given right-operand matcher, and is one of a set of compound assignment operators. Does not * match compound assignment operators. * * @param operators Which compound assignment operators to match against. * @param receiverMatcher The matcher to apply to the receiver. * @param expressionMatcher The matcher to apply to the expression. */ public static CompoundAssignment compoundAssignment( Set<Kind> operators, Matcher<ExpressionTree> receiverMatcher, Matcher<ExpressionTree> expressionMatcher) { return new CompoundAssignment(operators, receiverMatcher, expressionMatcher); } /** * Matches when the receiver of an instance method is the same reference as a particular argument to the method. * For example, receiverSameAsArgument(1) would match {@code obj.method("", obj)} * * @param argNum The number of the argument to compare against (zero-based. */ public static Matcher<? super MethodInvocationTree> receiverSameAsArgument(final int argNum) { return new Matcher<MethodInvocationTree>() { @Override public boolean matches(MethodInvocationTree t, VisitorState state) { List<? extends ExpressionTree> args = t.getArguments(); if (args.size() <= argNum) { return false; } ExpressionTree arg = args.get(argNum); JCExpression methodSelect = (JCExpression) t.getMethodSelect(); if (methodSelect instanceof JCFieldAccess) { JCFieldAccess fieldAccess = (JCFieldAccess) methodSelect; return ASTHelpers.sameVariable(fieldAccess.getExpression(), arg); } else if (methodSelect instanceof JCIdent) { // A bare method call: "equals(foo)". Receiver is implicitly "this". return "this".equals(arg.toString()); } return false; } }; } public static Matcher<MethodInvocationTree> receiverOfInvocation( final Matcher<ExpressionTree> expressionTreeMatcher) { return new Matcher<MethodInvocationTree>() { @Override public boolean matches(MethodInvocationTree methodInvocationTree, VisitorState state) { return expressionTreeMatcher.matches(ASTHelpers.getReceiver(methodInvocationTree), state); } }; } /** * Matches if the given annotation matcher matches all of or any of the annotations on this tree * node. * * @param matchType Whether to match if the matchers match any of or all of the annotations on * this tree. * @param annotationMatcher The annotation matcher to use. */ public static <T extends Tree> MultiMatcher<T, AnnotationTree> annotations(MatchType matchType, Matcher<AnnotationTree> annotationMatcher) { return new AnnotationMatcher<>(matchType, annotationMatcher); } /** * Matches a class in which any of/all of its constructors match the given constructorMatcher. */ public static MultiMatcher<ClassTree, MethodTree> constructor(MatchType matchType, Matcher<MethodTree> constructorMatcher) { return new ConstructorOfClass(matchType, constructorMatcher); } // TODO(cushon): expunge public static Matcher<MethodInvocationTree> methodSelect(Matcher<ExpressionTree> methodSelectMatcher) { return new MethodInvocationMethodSelect(methodSelectMatcher); } public static Matcher<MethodInvocationTree> argument( final int position, final Matcher<ExpressionTree> argumentMatcher) { return new MethodInvocationArgument(position, argumentMatcher); } /** * Matches if the given matcher matches all of/any of the arguments to this method invocation. */ public static MultiMatcher<MethodInvocationTree, ExpressionTree> hasArguments( MatchType matchType, Matcher<ExpressionTree> argumentMatcher) { return new HasArguments(matchType, argumentMatcher); } /** * Matches an AST node if it is a method invocation and the given matchers match. * * @param methodSelectMatcher matcher identifying the method being called * @param matchType how to match method arguments with {@code methodArgumentMatcher} * @param methodArgumentMatcher matcher applied to each method argument */ public static Matcher<ExpressionTree> methodInvocation( Matcher<ExpressionTree> methodSelectMatcher, MatchType matchType, Matcher<ExpressionTree> methodArgumentMatcher) { return new MethodInvocation(methodSelectMatcher, matchType, methodArgumentMatcher); } /** * Matches an AST node if it is a method invocation and the method select matches * {@code methodSelectMatcher}. Ignores any arguments. */ public static Matcher<ExpressionTree> methodInvocation( final Matcher<ExpressionTree> methodSelectMatcher) { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { if (!(expressionTree instanceof MethodInvocationTree)) { return false; } MethodInvocationTree tree = (MethodInvocationTree) expressionTree; return methodSelectMatcher.matches(tree.getMethodSelect(), state); } }; } public static Matcher<MethodInvocationTree> argumentCount(final int argumentCount) { return new Matcher<MethodInvocationTree>() { @Override public boolean matches(MethodInvocationTree t, VisitorState state) { return t.getArguments().size() == argumentCount; } }; } /** * Matches an AST node if its parent node is matched by the given matcher. * For example, {@code parentNode(kindIs(Kind.RETURN))} * would match the {@code this} expression in {@code return this;} */ public static Matcher<Tree> parentNode(Matcher<? extends Tree> treeMatcher) { @SuppressWarnings("unchecked") // Safe contravariant cast Matcher<Tree> matcher = (Matcher<Tree>) treeMatcher; return new ParentNode(matcher); } /** * Matches an AST node if its type is a subtype of the given type. * * @param typeStr a string representation of the type, e.g., "java.util.AbstractList" */ public static <T extends Tree> Matcher<T> isSubtypeOf(String typeStr) { return new IsSubtypeOf<>(typeStr); } /** * Matches an AST node if its type is a subtype of the given type. * * @param type the type to check against */ public static <T extends Tree> Matcher<T> isSubtypeOf(Supplier<Type> type) { return new IsSubtypeOf<>(type); } /** * Matches an AST node if its type is a subtype of the given type. * * @param clazz a class representation of the type, e.g., Action.class. */ public static <T extends Tree> Matcher<T> isSubtypeOf(Class<?> clazz) { return new IsSubtypeOf<>(typeFromClass(clazz)); } /** Matches an AST node if it has the same erased type as the given type. */ public static <T extends Tree> Matcher<T> isSameType(Supplier<Type> type) { return new IsSameType<>(type); } /** Matches an AST node if it has the same erased type as the given type. */ public static <T extends Tree> Matcher<T> isSameType(String typeString) { return new IsSameType<>(typeString); } /** Matches an AST node if it has the same erased type as the given class. */ public static <T extends Tree> Matcher<T> isSameType(Class<?> clazz) { return new IsSameType<>(typeFromClass(clazz)); } /** Matches an AST node if its type is an array type. */ public static <T extends Tree> Matcher<T> isArrayType() { return new Matcher<T>() { @Override public boolean matches(Tree t, VisitorState state) { return state.getTypes().isArray(((JCTree) t).type); } }; } /** * Matches an AST node if its type is a primitive array type. */ public static <T extends Tree> Matcher<T> isPrimitiveArrayType() { return new Matcher<T>() { @Override public boolean matches(Tree t, VisitorState state) { Type type = ((JCTree) t).type; return state.getTypes().isArray(type) && state.getTypes().elemtype(type).isPrimitive(); } }; } /** * Matches an AST node if its type is a primitive type. */ public static <T extends Tree> Matcher<T> isPrimitiveType() { return new Matcher<T>() { @Override public boolean matches(Tree t, VisitorState state) { return ((JCTree) t).type.isPrimitive(); } }; } /** Matches an AST node if its type is either a primitive type or a {@code void} type. */ public static <T extends Tree> Matcher<T> isPrimitiveOrVoidType() { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { return ((JCTree) t).type.isPrimitiveOrVoid(); } }; } /** Matches an AST node if its type is a {@code void} type. */ public static <T extends Tree> Matcher<T> isVoidType() { return new Matcher<T>() { @Override public boolean matches(T t, VisitorState state) { return state.getTypes().isSameType(((JCTree) t).type, state.getSymtab().voidType); } }; } /** * Matches an AST node if its type is a primitive type, or a boxed version of a primitive type. */ public static <T extends Tree> Matcher<T> isPrimitiveOrBoxedPrimitiveType() { return new Matcher<T>() { @Override public boolean matches(Tree t, VisitorState state) { return state.getTypes().unboxedTypeOrType(((JCTree) t).type).isPrimitive(); } }; } /** * Matches an AST node which is enclosed by a block node that matches the given matcher. */ public static <T extends Tree> Enclosing.Block<T> enclosingBlock(Matcher<BlockTree> matcher) { return new Enclosing.Block<>(matcher); } /** * Matches an AST node which is enclosed by a class node that matches the given matcher. */ public static <T extends Tree> Enclosing.Class<T> enclosingClass(Matcher<ClassTree> matcher) { return new Enclosing.Class<>(matcher); } /** Matches an AST node which is enclosed by a method node that matches the given matcher. */ public static <T extends Tree> Enclosing.Method<T> enclosingMethod(Matcher<MethodTree> matcher) { return new Enclosing.Method<>(matcher); } /** * Matches an AST node that is enclosed by some node that matches the given matcher. * * TODO(eaftan): This could be used instead of enclosingBlock and enclosingClass. */ public static <T extends Tree> Matcher<Tree> enclosingNode(final Matcher<T> matcher) { return new Matcher<Tree>() { @SuppressWarnings("unchecked") // TODO(cushon): this should take a Class<T> @Override public boolean matches(Tree t, VisitorState state) { TreePath path = state.getPath().getParentPath(); while (path != null) { Tree node = path.getLeaf(); state = state.withPath(path); if (matcher.matches((T) node, state)) { return true; } path = path.getParentPath(); } return false; } }; } /** * Matches a statement AST node if the following statement in the enclosing block matches the given matcher. */ public static <T extends StatementTree> NextStatement<T> nextStatement( Matcher<StatementTree> matcher) { return new NextStatement<>(matcher); } /** Matches a statement AST node if the statement is the last statement in the block. */ public static Matcher<StatementTree> isLastStatementInBlock() { return new IsLastStatementInBlock<>(); } /** * Matches an AST node if it is a literal other than null. */ public static Matcher<ExpressionTree> nonNullLiteral() { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree tree, VisitorState state) { switch (tree.getKind()) { case MEMBER_SELECT: return ((MemberSelectTree) tree).getIdentifier().contentEquals("class"); case INT_LITERAL: case LONG_LITERAL: case FLOAT_LITERAL: case DOUBLE_LITERAL: case BOOLEAN_LITERAL: case CHAR_LITERAL: // fall through case STRING_LITERAL: return true; default: return false; } } }; } /** * Matches a Literal AST node if it is a string literal with the given value. * For example, {@code stringLiteral("thing")} matches the literal {@code "thing"} */ public static Matcher<ExpressionTree> stringLiteral(String value) { return new StringLiteral(value); } /** * Matches a Literal AST node if it is a string literal which matches the given {@link Pattern}. * @see #stringLiteral(String) */ public static Matcher<ExpressionTree> stringLiteral(Pattern pattern) { return new StringLiteral(pattern); } public static Matcher<ExpressionTree> booleanLiteral(final boolean value) { return new Matcher<ExpressionTree>() { // Matcher of a boolean literal @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { if (expressionTree.getKind() == Tree.Kind.BOOLEAN_LITERAL) { return value == (Boolean) (((LiteralTree) expressionTree).getValue()); } return false; } }; } /** * Matches the boolean constant ({@link Boolean#TRUE} or {@link Boolean#FALSE}) corresponding to * the given value. */ public static Matcher<ExpressionTree> booleanConstant(final boolean value) { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { if (expressionTree instanceof JCFieldAccess) { Symbol symbol = ASTHelpers.getSymbol(expressionTree); if (symbol.isStatic() && state.getTypes().unboxedTypeOrType(symbol.type).getTag() == TypeTag.BOOLEAN) { return ((value && symbol.getSimpleName().contentEquals("TRUE")) || symbol.getSimpleName().contentEquals("FALSE")); } } return false; } }; } /** * Ignores any number of parenthesis wrapping an expression and then applies the passed matcher to * that expression. For example, the passed matcher would be applied to {@code value} in * {@code (((value)))}. */ public static Matcher<ExpressionTree> ignoreParens(final Matcher<ExpressionTree> innerMatcher) { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { return innerMatcher.matches( (ExpressionTree) TreeInfo.skipParens((JCTree) expressionTree), state); } }; } public static Matcher<ExpressionTree> intLiteral(final int value) { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { if (expressionTree.getKind() == Kind.INT_LITERAL) { return ((Integer) ((LiteralTree) expressionTree).getValue()).equals(value); } return false; } }; } public static Matcher<ExpressionTree> classLiteral( final Matcher<? super ExpressionTree> classMatcher) { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { if (expressionTree.getKind() == Kind.MEMBER_SELECT) { MemberSelectTree select = (MemberSelectTree) expressionTree; return select.getIdentifier().contentEquals("class") && classMatcher.matches(select.getExpression(), state); } return false; } }; } /** * Matches an Annotation AST node if the argument to the annotation with the given name has a value which * matches the given matcher. * For example, {@code hasArgumentWithValue("value", stringLiteral("one"))} matches {@code @Thing("one")} * or {@code @Thing({"one", "two"})} or {@code @Thing(value = "one")} */ public static Matcher<AnnotationTree> hasArgumentWithValue( String argumentName, Matcher<ExpressionTree> valueMatcher) { return new AnnotationHasArgumentWithValue(argumentName, valueMatcher); } /** * Matches an Annotation AST node if an argument to the annotation does not exist. */ public static Matcher<AnnotationTree> doesNotHaveArgument(String argumentName) { return new AnnotationDoesNotHaveArgument(argumentName); } public static Matcher<AnnotationTree> isType(final String annotationClassName) { return new AnnotationType(annotationClassName); } /** * Matches a MethodInvocation AST node when the arguments at the two given indices are both the same identifier. */ public static Matcher<? super MethodInvocationTree> sameArgument( final int index1, final int index2) { return new Matcher<MethodInvocationTree>() { @Override public boolean matches(MethodInvocationTree methodInvocationTree, VisitorState state) { List<? extends ExpressionTree> args = methodInvocationTree.getArguments(); return ASTHelpers.sameVariable(args.get(index1), args.get(index2)); } }; } /** * Determines whether an expression has an annotation of the given type. This includes annotations * inherited from superclasses due to @Inherited. * * @param annotationClass the binary class name of the annotation (e.g. * "javax.annotation.Nullable", or "some.package.OuterClassName$InnerClassName") */ public static <T extends Tree> Matcher<T> hasAnnotation(final String annotationClass) { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { return ASTHelpers.hasAnnotation(ASTHelpers.getSymbol(tree), annotationClass, state); } }; } /** * Determines whether an expression has an annotation of the given class. * This includes annotations inherited from superclasses due to @Inherited. * * @param inputClass The class of the annotation to look for (e.g, Produces.class). */ public static <T extends Tree> Matcher<T> hasAnnotation( final Class<? extends Annotation> inputClass) { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { return ASTHelpers.hasAnnotation(ASTHelpers.getSymbol(tree), inputClass, state); } }; } /** * Matches if a method or any method it overrides has an annotation of the given type. JUnit 4's * {@code @Test}, {@code @Before}, and {@code @After} annotations behave this way. * * @param annotationClass the binary class name of the annotation (e.g. * "javax.annotation.Nullable", or "some.package.OuterClassName$InnerClassName") */ public static Matcher<MethodTree> hasAnnotationOnAnyOverriddenMethod( final String annotationClass) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree tree, VisitorState state) { MethodSymbol methodSym = ASTHelpers.getSymbol(tree); if (methodSym == null) { return false; } if (ASTHelpers.hasAnnotation(methodSym, annotationClass, state)) { return true; } for (MethodSymbol method : ASTHelpers.findSuperMethods(methodSym, state.getTypes())) { if (ASTHelpers.hasAnnotation(method, annotationClass, state)) { return true; } } return false; } }; } /** * Matches a whitelisted method invocation that is known to never return null */ public static Matcher<ExpressionTree> methodReturnsNonNull() { return anyOf( instanceMethod().onDescendantOf("java.lang.Object").named("toString"), instanceMethod().onExactClass("java.lang.String"), staticMethod().onClass("java.lang.String"), instanceMethod().onExactClass("java.util.StringTokenizer").named("nextToken") ); } public static Matcher<MethodTree> methodReturns(final Matcher<? super Tree> returnTypeMatcher) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { Tree returnTree = methodTree.getReturnType(); if (returnTree == null) { // This is a constructor, it has no return type. return false; } return returnTypeMatcher.matches(returnTree, state); } }; } public static Matcher<MethodTree> methodReturns(final Supplier<Type> returnType) { return methodReturns(isSameType(returnType)); } /** * Match a method that returns a non-primitive type. */ public static Matcher<MethodTree> methodReturnsNonPrimitiveType() { return methodReturns(not(isPrimitiveOrVoidType())); } /** * Match a method declaration with a specific name. * * @param methodName The name of the method to match, e.g., "equals" */ // TODO(cushon): expunge public static Matcher<MethodTree> methodIsNamed(final String methodName) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { return methodTree.getName().contentEquals(methodName); } }; } /** * Match a method declaration that starts with a given string. * * @param prefix The prefix. */ public static Matcher<MethodTree> methodNameStartsWith(final String prefix) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { return methodTree.getName().toString().startsWith(prefix); } }; } /** * Match a method declaration with a specific enclosing class and method name. * * @param className The fully-qualified name of the enclosing class, e.g. * "com.google.common.base.Preconditions" * @param methodName The name of the method to match, e.g., "checkNotNull" */ public static Matcher<MethodTree> methodWithClassAndName( final String className, final String methodName) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { return ASTHelpers.getSymbol(methodTree) .getEnclosingElement() .getQualifiedName() .contentEquals(className) && methodTree.getName().contentEquals(methodName); } }; } /** * Matches an AST node that represents a method declaration, based on the list of * variableMatchers. Applies the variableMatcher at index n to the parameter at index n * and returns true iff they all match. Returns false if the number of variableMatchers provided * does not match the number of parameters. * * <p>If you pass no variableMatchers, this will match methods with no parameters. * * @param variableMatcher an array of matchers to apply to the parameters of the method */ @SafeVarargs public static Matcher<MethodTree> methodHasParameters(final Matcher<VariableTree>... variableMatcher) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { if (methodTree.getParameters().size() != variableMatcher.length) { return false; } int paramIndex = 0; for (Matcher<VariableTree> eachVariableMatcher : variableMatcher) { if (!eachVariableMatcher.matches(methodTree.getParameters().get(paramIndex++), state)) { return false; } } return true; } }; } /** * Matches if the given matcher matches all of/any of the parameters to this method. */ public static MultiMatcher<MethodTree, VariableTree> methodHasParameters(MatchType matchType, Matcher<VariableTree> parameterMatcher) { return new MethodHasParameters(matchType, parameterMatcher); } public static Matcher<MethodTree> methodHasVisibility(final Visibility visibility) { return new MethodVisibility(visibility); } public static Matcher<MethodTree> methodIsConstructor() { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { return ASTHelpers.getSymbol(methodTree).isConstructor(); } }; } /** * Matches a constructor declaration in a specific enclosing class. * * @param className The fully-qualified name of the enclosing class, e.g. * "com.google.common.base.Preconditions" */ public static Matcher<MethodTree> constructorOfClass(final String className) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { Symbol symbol = ASTHelpers.getSymbol(methodTree); return symbol.getEnclosingElement().getQualifiedName().contentEquals(className) && symbol.isConstructor(); } }; } /** * Matches a class in which at least one method matches the given methodMatcher. * * @param methodMatcher A matcher on MethodTrees to run against all methods in this class. * @return True if some method in the class matches the given methodMatcher. */ public static Matcher<ClassTree> hasMethod(final Matcher<MethodTree> methodMatcher) { return new Matcher<ClassTree>() { @Override public boolean matches(ClassTree t, VisitorState state) { for (Tree member : t.getMembers()) { if (member instanceof MethodTree) { if (methodMatcher.matches((MethodTree)member, state)) { return true; } } } return false; } }; } /** * Matches on the type of a VariableTree AST node. * * @param treeMatcher A matcher on the type of the variable. */ public static Matcher<VariableTree> variableType(final Matcher<Tree> treeMatcher) { return new Matcher<VariableTree>() { @Override public boolean matches(VariableTree variableTree, VisitorState state) { return treeMatcher.matches(variableTree.getType(), state); } }; } /** * Matches on the initializer of a VariableTree AST node. * * @param expressionTreeMatcher A matcher on the initializer of the variable. */ public static Matcher<VariableTree> variableInitializer(final Matcher<ExpressionTree> expressionTreeMatcher) { return new Matcher<VariableTree>() { @Override public boolean matches(VariableTree variableTree, VisitorState state) { ExpressionTree initializer = variableTree.getInitializer(); return initializer == null ? false : expressionTreeMatcher.matches(initializer, state); } }; } /** * Matches if a {@link VariableTree} is a field declaration, as opposed to a local variable, enum * constant, parameter to a method, etc. */ public static Matcher<VariableTree> isField() { return new Matcher<VariableTree>() { @Override public boolean matches(VariableTree variableTree, VisitorState state) { Element element = ASTHelpers.getSymbol(variableTree); return element.getKind() == ElementKind.FIELD; } }; } /** * Matches an class based on whether it is nested in another class or method. * * @param kind The kind of nesting to match, eg ANONYMOUS, LOCAL, MEMBER, TOP_LEVEL */ public static Matcher<ClassTree> nestingKind(final NestingKind kind) { return new Matcher<ClassTree>() { @Override public boolean matches(ClassTree classTree, VisitorState state) { ClassSymbol sym = ASTHelpers.getSymbol(classTree); return sym.getNestingKind() == kind; } }; } /** * Matches an instance method that is a descendant of the instance method specified by the * class name and method name. * * @param fullClassName The name of the class whose instance method to match, e.g., * "java.util.Map" * @param methodName The name of the method to match, including arguments, e.g., * "get(java.lang.Object)" */ // TODO(cushon): expunge public static Matcher<ExpressionTree> isDescendantOfMethod(String fullClassName, String methodName) { return new DescendantOf(fullClassName, methodName); } /** * Matches a binary tree if the given matchers match the operands in either order. That is, * returns true if either: * matcher1 matches the left operand and matcher2 matches the right operand * or * matcher2 matches the left operand and matcher1 matches the right operand */ public static Matcher<BinaryTree> binaryTree(final Matcher<ExpressionTree> matcher1, final Matcher<ExpressionTree> matcher2) { return new Matcher<BinaryTree>() { @Override public boolean matches(BinaryTree t, VisitorState state) { return null != ASTHelpers.matchBinaryTree(t, Arrays.asList(matcher1, matcher2), state); } }; } /** * Matches any AST that contains an identifier with a certain property. This matcher can be used, * for instance, to locate identifiers with a certain name or which is defined in a certain class. * * @param nodeMatcher Which identifiers to look for */ public static Matcher<Tree> hasIdentifier(Matcher<IdentifierTree> nodeMatcher) { return new HasIdentifier(nodeMatcher); } /** * Returns true if the expression is a member access on an instance, rather than a static type. * Supports member method invocations and field accesses. */ public static Matcher<ExpressionTree> selectedIsInstance() { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree expr, VisitorState state) { if (!(expr instanceof JCFieldAccess)) { // TODO(cushon): throw IllegalArgumentException? return false; } JCExpression selected = ((JCFieldAccess) expr).getExpression(); if (selected instanceof JCNewClass) { return true; } Symbol sym = ASTHelpers.getSymbol(selected); return sym instanceof VarSymbol; } }; } /** * Returns true if the Tree node has the expected {@code Modifier}. */ public static <T extends Tree> Matcher<T> hasModifier(final Modifier modifier) { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); return sym != null && sym.getModifiers().contains(modifier); } }; } /** * Matches an AST node which is an expression yielding the indicated static field access. */ public static Matcher<ExpressionTree> staticFieldAccess() { return allOf(isStatic(), isSymbol(VarSymbol.class)); } /** * Matches an AST node that is static. */ public static <T extends Tree> Matcher<T> isStatic() { return new Matcher<T>() { @Override public boolean matches(Tree tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); return sym != null && sym.isStatic(); } }; } /** * Matches a {@code throw} statement where the thrown item is matched by the passed * {@code thrownMatcher}. */ public static Matcher<StatementTree> throwStatement( Matcher<? super ExpressionTree> thrownMatcher) { return new Throws(thrownMatcher); } /** * Matches a {@code return} statement where the returned expression is matched by the passed * {@code returnedMatcher}. */ public static Matcher<StatementTree> returnStatement( Matcher<? super ExpressionTree> returnedMatcher) { return new Returns(returnedMatcher); } /** * Matches an {@code assert} statement where the condition is matched by the passed * {@code conditionMatcher}. */ public static Matcher<StatementTree> assertStatement(Matcher<ExpressionTree> conditionMatcher) { return new Asserts(conditionMatcher); } /** * Matches a {@code continue} statement. */ public static Matcher<StatementTree> continueStatement() { return new Matcher<StatementTree>() { @Override public boolean matches(StatementTree statementTree, VisitorState state) { return statementTree instanceof ContinueTree; } }; } /** Matches an {@link ExpressionStatementTree} based on its {@link ExpressionTree}. */ public static Matcher<StatementTree> expressionStatement(final Matcher<ExpressionTree> matcher) { return new Matcher<StatementTree>() { @Override public boolean matches(StatementTree statementTree, VisitorState state) { return statementTree instanceof ExpressionStatementTree && matcher.matches(((ExpressionStatementTree) statementTree).getExpression(), state); } }; } static Matcher<Tree> isSymbol(java.lang.Class<? extends Symbol> symbolClass) { return new IsSymbol(symbolClass); } /** * Converts the given matcher to one that can be applied to any tree but is only executed when run * on a tree of {@code type} and returns {@code false} for all other tree types. */ public static <S extends Tree, T extends Tree> Matcher<T> toType( final Class<S> type, final Matcher<? super S> matcher) { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { return type.isInstance(tree) && matcher.matches(type.cast(tree), state); } }; } /** * Matches if this Tree is enclosed by either a synchronized block or a synchronized method. */ public static final <T extends Tree> Matcher<T> inSynchronized() { return new Matcher<T>() { @Override public boolean matches(T tree, VisitorState state) { SynchronizedTree synchronizedTree = ASTHelpers.findEnclosingNode(state.getPath(), SynchronizedTree.class); if (synchronizedTree != null) { return true; } MethodTree methodTree = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class); return methodTree != null && methodTree.getModifiers().getFlags().contains(Modifier.SYNCHRONIZED); } }; } /** * Matches if this ExpressionTree refers to the same variable as the one passed into the * matcher. */ public static Matcher<ExpressionTree> sameVariable(final ExpressionTree expr) { return new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree tree, VisitorState state) { return ASTHelpers.sameVariable(tree, expr); } }; } /** * Matches if the expression is provably non-null. */ public static Matcher<ExpressionTree> isNonNull() { return new NullnessMatcher(Nullness.NONNULL); } /** * Matches if the expression is provably null. */ public static Matcher<ExpressionTree> isNull() { return new NullnessMatcher(Nullness.NULL); } /** * Matches an enhanced for loop if all the given matchers match. * * @param variableMatcher The matcher to apply to the variable. * @param expressionMatcher The matcher to apply to the expression. * @param statementMatcher The matcher to apply to the statement. */ public static Matcher<EnhancedForLoopTree> enhancedForLoop(final Matcher<VariableTree> variableMatcher, final Matcher<ExpressionTree> expressionMatcher, final Matcher<StatementTree> statementMatcher) { return new Matcher<EnhancedForLoopTree>() { @Override public boolean matches(EnhancedForLoopTree t, VisitorState state) { return variableMatcher.matches(t.getVariable(), state) && expressionMatcher.matches(t.getExpression(), state) && statementMatcher.matches(t.getStatement(), state); } }; } /** * Matches if the given tree is inside a loop. */ public static <T extends Tree> Matcher<T> inLoop() { return new Matcher<T>() { @Override public boolean matches(Tree tree, VisitorState state) { TreePath path = state.getPath().getParentPath(); Tree node = path.getLeaf(); while (path != null) { switch (node.getKind()) { case METHOD: case CLASS: return false; case WHILE_LOOP: case FOR_LOOP: case ENHANCED_FOR_LOOP: case DO_WHILE_LOOP: return true; default: path = path.getParentPath(); node = path.getLeaf(); break; } } return false; } }; } /** * Matches an assignment operator AST node if both of the given matchers match. * * @param variableMatcher The matcher to apply to the variable. * @param expressionMatcher The matcher to apply to the expression. */ public static Matcher<AssignmentTree> assignment(final Matcher<ExpressionTree> variableMatcher, final Matcher<? super ExpressionTree> expressionMatcher) { return new Matcher<AssignmentTree>() { @Override public boolean matches(AssignmentTree t, VisitorState state) { return variableMatcher.matches(t.getVariable(), state) && expressionMatcher.matches(t.getExpression(), state); } }; } /** * Matches a type cast AST node if both of the given matchers match. * * @param typeMatcher The matcher to apply to the type. * @param expressionMatcher The matcher to apply to the expression. */ public static Matcher<TypeCastTree> typeCast(final Matcher<Tree> typeMatcher, final Matcher<ExpressionTree> expressionMatcher) { return new Matcher<TypeCastTree>() { @Override public boolean matches(TypeCastTree t, VisitorState state) { return typeMatcher.matches(t.getType(), state) && expressionMatcher.matches(t.getExpression(), state); } }; } /** * Matches an assertion AST node if the given matcher matches its condition. * * @param conditionMatcher The matcher to apply to the condition in the assertion, e.g. in * "assert false", the "false" part of the statement */ public static Matcher<AssertTree> assertionWithCondition( final Matcher<ExpressionTree> conditionMatcher) { return new Matcher<AssertTree>() { @Override public boolean matches(AssertTree tree, VisitorState state) { return conditionMatcher.matches(tree.getCondition(), state); } }; } /** * Applies the given matcher recursively to all descendants of an AST node, and matches if * any matching descendant node is found. * * @param treeMatcher The matcher to apply recursively to the tree. */ public static Matcher<Tree> contains(Matcher<Tree> treeMatcher) { return new Contains(treeMatcher); } /** * Applies the given matcher recursively to all descendants of an AST node, and matches if any * matching descendant node is found. * * @param clazz The type of node to be matched. * @param treeMatcher The matcher to apply recursively to the tree. */ public static <T extends Tree, V extends Tree> Matcher<T> contains( Class<V> clazz, Matcher<V> treeMatcher) { final Matcher<Tree> contains = new Contains(toType(clazz, treeMatcher)); return contains::matches; } /** * Matches if the method accepts the given number of arguments. * * @param arity the number of arguments the method should accept */ public static Matcher<MethodTree> methodHasArity(final int arity) { return new Matcher<MethodTree>() { @Override public boolean matches(MethodTree methodTree, VisitorState state) { return methodTree.getParameters().size() == arity; } }; } }