/*
* Copyright 2013 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.common.base.MoreObjects.firstNonNull;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.hasAnnotationOnAnyOverriddenMethod;
import static com.google.errorprone.matchers.Matchers.hasArgumentWithValue;
import static com.google.errorprone.matchers.Matchers.hasMethod;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
import static com.google.errorprone.matchers.Matchers.methodHasVisibility;
import static com.google.errorprone.matchers.Matchers.methodIsNamed;
import static com.google.errorprone.matchers.Matchers.methodNameStartsWith;
import static com.google.errorprone.matchers.Matchers.methodReturns;
import static com.google.errorprone.matchers.Matchers.nestingKind;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.suppliers.Suppliers.VOID_TYPE;
import static javax.lang.model.element.NestingKind.TOP_LEVEL;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ClassType;
import com.sun.tools.javac.tree.JCTree;
import java.util.Arrays;
import java.util.Collection;
import javax.lang.model.element.Modifier;
/**
* Matchers for code patterns which appear to be JUnit-based tests.
* @author alexeagle@google.com (Alex Eagle)
* @author eaftan@google.com (Eddie Aftandillian)
*/
public class JUnitMatchers {
public static final String JUNIT4_TEST_ANNOTATION = "org.junit.Test";
public static final String JUNIT_BEFORE_ANNOTATION = "org.junit.Before";
public static final String JUNIT_AFTER_ANNOTATION = "org.junit.After";
public static final String JUNIT_BEFORE_CLASS_ANNOTATION = "org.junit.BeforeClass";
public static final String JUNIT_AFTER_CLASS_ANNOTATION = "org.junit.AfterClass";
public static final String JUNIT4_RUN_WITH_ANNOTATION = "org.junit.runner.RunWith";
private static final String JUNIT3_TEST_CASE_CLASS = "junit.framework.TestCase";
private static final String JUNIT4_IGNORE_ANNOTATION = "org.junit.Ignore";
public static final Matcher<MethodTree> hasJUnitAnnotation = anyOf(
/* @Test, @Before, and @After are inherited by methods that override a base method with the
* annotation. @BeforeClass and @AfterClass can only be applied to static methods, so they
* cannot be inherited. */
hasAnnotationOnAnyOverriddenMethod(JUNIT4_TEST_ANNOTATION),
hasAnnotationOnAnyOverriddenMethod(JUNIT_BEFORE_ANNOTATION),
hasAnnotationOnAnyOverriddenMethod(JUNIT_AFTER_ANNOTATION),
hasAnnotation(JUNIT_BEFORE_CLASS_ANNOTATION),
hasAnnotation(JUNIT_AFTER_CLASS_ANNOTATION));
public static final Matcher<MethodTree> hasJUnit4BeforeAnnotations = anyOf(
hasAnnotationOnAnyOverriddenMethod(JUNIT_BEFORE_ANNOTATION),
hasAnnotation(JUNIT_BEFORE_CLASS_ANNOTATION));
public static final Matcher<MethodTree> hasJUnit4AfterAnnotations = anyOf(
hasAnnotationOnAnyOverriddenMethod(JUNIT_AFTER_ANNOTATION),
hasAnnotation(JUNIT_AFTER_CLASS_ANNOTATION));
/**
* Matches a class that inherits from TestCase.
*/
public static final Matcher<ClassTree> isTestCaseDescendant =
isSubtypeOf(JUNIT3_TEST_CASE_CLASS);
/**
* Match a class which appears to be missing a @RunWith annotation.
*
* Matches if:
* 1) The class does not have a JUnit 4 @RunWith annotation.
* 2) The class is concrete.
* 3) The class is a top-level class.
*/
public static final Matcher<ClassTree> isConcreteClassWithoutRunWith = allOf(
not(hasAnnotation(JUNIT4_RUN_WITH_ANNOTATION)),
not(Matchers.<ClassTree>hasModifier(Modifier.ABSTRACT)),
nestingKind(TOP_LEVEL));
/**
* Match a class which has one or more methods with a JUnit 4 @Test annotation.
*/
public static final Matcher<ClassTree> hasJUnit4TestCases =
hasMethod(hasAnnotationOnAnyOverriddenMethod(JUNIT4_TEST_ANNOTATION));
/**
* Match a class which appears to be a JUnit 3 test class.
*
* Matches if:
* 1) The class does inherit from TestCase.
* 2) The class does not have a JUnit 4 @RunWith annotation.
* 3) The class is concrete.
* 4) This class is a top-level class.
*/
public static final Matcher<ClassTree> isJUnit3TestClass = allOf(
isTestCaseDescendant,
isConcreteClassWithoutRunWith);
/**
* Match a method which appears to be a JUnit 3 test case.
*
* Matches if:
* 1) The method's name begins with "test".
* 2) The method has no parameters.
* 3) The method is public.
* 4) The method returns void
*/
public static final Matcher<MethodTree> isJunit3TestCase = allOf(
methodNameStartsWith("test"),
methodHasParameters(),
Matchers.<MethodTree>hasModifier(Modifier.PUBLIC),
methodReturns(VOID_TYPE)
);
/**
* Match a method which appears to be a JUnit 3 setUp method
*
* Matches if:
* 1) The method is named "setUp"
* 2) The method has no parameters
* 3) The method is a public or protected instance method that is not abstract
* 4) The method returns void
*/
public static final Matcher<MethodTree> looksLikeJUnit3SetUp = allOf(
methodIsNamed("setUp"),
methodHasParameters(),
anyOf(
methodHasVisibility(MethodVisibility.Visibility.PUBLIC),
methodHasVisibility(MethodVisibility.Visibility.PROTECTED)
),
not(Matchers.<MethodTree>hasModifier(Modifier.ABSTRACT)),
not(Matchers.<MethodTree>hasModifier(Modifier.STATIC)),
methodReturns(VOID_TYPE)
);
/**
* Match a method which appears to be a JUnit 3 tearDown method
*
* Matches if:
* 1) The method is named "tearDown"
* 2) The method has no parameters
* 3) The method is a public or protected instance method that is not abstract
* 4) The method returns void
*/
public static final Matcher<MethodTree> looksLikeJUnit3TearDown = allOf(
methodIsNamed("tearDown"),
methodHasParameters(),
anyOf(
methodHasVisibility(MethodVisibility.Visibility.PUBLIC),
methodHasVisibility(MethodVisibility.Visibility.PROTECTED)
),
not(Matchers.<MethodTree>hasModifier(Modifier.ABSTRACT)),
not(Matchers.<MethodTree>hasModifier(Modifier.STATIC)),
methodReturns(VOID_TYPE)
);
/**
* Matches a method annotated with @Test but not @Ignore.
*/
public static final Matcher<MethodTree> wouldRunInJUnit4 = allOf(
hasAnnotationOnAnyOverriddenMethod(JUNIT4_TEST_ANNOTATION),
not(hasAnnotationOnAnyOverriddenMethod(JUNIT4_IGNORE_ANNOTATION)));
public static class JUnit4TestClassMatcher implements Matcher<ClassTree> {
/**
* A list of test runners that this matcher should look for in the @RunWith annotation.
* Subclasses of the test runners are also matched.
*/
private static final Collection<String> TEST_RUNNERS = Arrays.asList(
"org.mockito.runners.MockitoJUnitRunner",
"org.junit.runners.BlockJUnit4ClassRunner");
/**
* Matches an argument of type Class<T>, where T is a subtype of one of the test runners listed
* in the TEST_RUNNERS field.
*
* TODO(eaftan): Support checking for an annotation that tells us whether this test runner
* expects tests to be annotated with @Test.
*/
private static final Matcher<ExpressionTree> isJUnit4TestRunner =
new Matcher<ExpressionTree>() {
@Override
public boolean matches(ExpressionTree t, VisitorState state) {
Type type = ((JCTree) t).type;
// Expect a class type.
if (!(type instanceof ClassType)) {
return false;
}
// Expect one type argument, the type of the JUnit class runner to use.
com.sun.tools.javac.util.List<Type> typeArgs = ((ClassType) type).getTypeArguments();
if (typeArgs.size() != 1) {
return false;
}
Type runnerType = typeArgs.get(0);
for (String testRunner : TEST_RUNNERS) {
Symbol parent = state.getSymbolFromString(testRunner);
if (parent == null) {
continue;
}
if (runnerType.tsym.isSubClass(parent, state.getTypes())) {
return true;
}
}
return false;
}
};
private static final Matcher<ClassTree> isJUnit4TestClass = allOf(
not(isTestCaseDescendant),
annotations(AT_LEAST_ONE, hasArgumentWithValue("value", isJUnit4TestRunner)));
@Override
public boolean matches(ClassTree classTree, VisitorState state) {
return isJUnit4TestClass.matches(classTree, state);
}
}
/** Returns true if the tree contains a method invocation that looks like a test assertion. */
public static boolean containsTestMethod(Tree tree) {
return firstNonNull(
tree.accept(
new TreeScanner<Boolean, Void>() {
@Override
public Boolean visitMethodInvocation(MethodInvocationTree node, Void unused) {
String name = ASTHelpers.getSymbol(node).getSimpleName().toString();
return name.contains("assert")
|| name.contains("verify")
|| name.contains("check")
|| name.contains("fail")
|| name.contains("expect")
|| firstNonNull(super.visitMethodInvocation(node, null), false);
}
@Override
public Boolean reduce(Boolean a, Boolean b) {
return firstNonNull(a, false) || firstNonNull(b, false);
}
},
null),
false);
}
}