/* * 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.google.errorprone.bugpatterns; import static com.google.errorprone.matchers.Matchers.allOf; import static com.google.errorprone.matchers.Matchers.enclosingClass; import static com.google.errorprone.matchers.Matchers.hasAnnotation; import static com.google.errorprone.matchers.Matchers.hasAnnotationOnAnyOverriddenMethod; import static com.google.errorprone.matchers.Matchers.not; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.JUnitMatchers; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.matchers.Matchers; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.MethodTree; import com.sun.tools.javac.tree.JCTree; import java.io.Serializable; import java.util.List; import javax.annotation.Nullable; import javax.lang.model.element.Modifier; /** * Base class for JUnit4SetUp/TearDown not run. This will take care of the nitty-gritty about * replacing @After with @Before, adding @Before on unannotated methods, making them public * if necessary, fixing the imports of other @Before, etc. * * @author glorioso@google.com */ abstract class AbstractJUnit4InitMethodNotRun extends BugChecker implements MethodTreeMatcher { private static final String JUNIT_TEST = "org.junit.Test"; protected final JUnitMatchers.JUnit4TestClassMatcher isJUnit4TestClass; protected AbstractJUnit4InitMethodNotRun() { this.isJUnit4TestClass = new JUnitMatchers.JUnit4TestClassMatcher(); } /** * Returns a matcher that selects which methods this matcher applies to * (e.g. public void setUp() without @Before/@BeforeClass annotation) */ protected abstract Matcher<MethodTree> methodMatcher(); /** * Returns the fully qualified class name of the annotation this bugpattern should apply to * matched methods. * * If another annotation is on the method that has the same name, the import will be replaced * with the appropriate one (e.g.: com.example.Before becomes org.junit.Before) */ protected abstract String correctAnnotation(); /** * Returns a collection of 'before-and-after' pairs of annotations that should be replaced on * these methods. * * <p>If this method matcher finds a method annotated with * {@link AnnotationReplacements#badAnnotation}, instead of applying * {@link #correctAnnotation()}, instead replace it with * {@link AnnotationReplacements#goodAnnotation} */ protected abstract List<AnnotationReplacements> annotationReplacements(); /** * Matches if all of the following conditions are true: * 1) The method matches {@link #methodMatcher()}, (looks like setUp() or tearDown(), * and none of the overrides in the hierarchy of the method have the appropriate @Before * or @After annotations) * 2) The method is not annotated with @Test * 3) The enclosing class has an @RunWith annotation and does not extend TestCase. This marks * that the test is intended to run with JUnit 4. */ @Override public Description matchMethod(MethodTree methodTree, VisitorState state) { boolean matches = allOf( methodMatcher(), not(hasAnnotationOnAnyOverriddenMethod(JUNIT_TEST)), enclosingClass(isJUnit4TestClass)) .matches(methodTree, state); if (!matches) { return Description.NO_MATCH; } // For each annotationReplacement, replace the first annotation that matches. If any of them // matches, don't try and do the rest of the work. Description description; for (AnnotationReplacements replacement : annotationReplacements()) { description = tryToReplaceAnnotation( methodTree, state, replacement.badAnnotation, replacement.goodAnnotation); if (description != null) { return description; } } // Search for another @Before annotation on the method and replace the import // if we find one String correctAnnotation = correctAnnotation(); String unqualifiedClassName = getUnqualifiedClassName(correctAnnotation); for (AnnotationTree annotationNode : methodTree.getModifiers().getAnnotations()) { String annotationClassName = ASTHelpers.getSymbol(annotationNode).getQualifiedName().toString(); if (annotationClassName.endsWith("." + unqualifiedClassName)) { SuggestedFix.Builder suggestedFix = SuggestedFix.builder() .removeImport(annotationClassName) .addImport(correctAnnotation); if (makeProtectedPublic(methodTree, state, unqualifiedClassName, suggestedFix, false) == null) { // No source position available, don't suggest a fix return describeMatch(annotationNode); } suggestedFix.replace(annotationNode, "@" + unqualifiedClassName); return describeMatch(annotationNode, suggestedFix.build()); } } // Add correctAnnotation() to the unannotated method // (and convert protected to public if it is) SuggestedFix.Builder suggestedFix = SuggestedFix.builder().addImport(correctAnnotation); // The makeProtectedPublic will take care of adding the annotation for us Boolean annotationAdded = makeProtectedPublic(methodTree, state, unqualifiedClassName, suggestedFix, true); // if (annotationAdded == null) { // No source position available, don't suggest a fix return describeMatch(methodTree); } if (!annotationAdded) { suggestedFix.prefixWith(methodTree, "@" + unqualifiedClassName + "\n"); } return describeMatch(methodTree, suggestedFix.build()); } // returns null if the method source isn't available for some reason. Caller should check // null and not emit a fix in that case. Returns true if the method was upgraded from protected // to public @Nullable private Boolean makeProtectedPublic(MethodTree methodTree, VisitorState state, String unqualifiedClassName, SuggestedFix.Builder suggestedFix, boolean addAnnotation) { if (Matchers.<MethodTree>hasModifier(Modifier.PROTECTED).matches(methodTree, state)) { CharSequence methodSource = state.getSourceForNode((JCTree.JCMethodDecl) methodTree); if (methodSource == null) { return null; } String methodString = (addAnnotation ? "@" + unqualifiedClassName + "\n" : "") + methodSource.toString().replaceFirst("protected ", "public "); suggestedFix.replace(methodTree, methodString); return true; } return false; } private Description tryToReplaceAnnotation(MethodTree methodTree, VisitorState state, String badAnnotation, String goodAnnotation) { String finalName = getUnqualifiedClassName(goodAnnotation); if (hasAnnotation(badAnnotation).matches(methodTree, state)) { AnnotationTree annotationTree = findAnnotation(methodTree, state, badAnnotation); return describeMatch(annotationTree, SuggestedFix.builder() .addImport(goodAnnotation) .replace(annotationTree, "@" + finalName) .build()); } else { return null; } } private String getUnqualifiedClassName(String goodAnnotation) { return goodAnnotation.substring(goodAnnotation.lastIndexOf(".") + 1); } private AnnotationTree findAnnotation(MethodTree methodTree, VisitorState state, String annotationName) { AnnotationTree annotationNode = null; for (AnnotationTree annotation : methodTree.getModifiers().getAnnotations()) { if (ASTHelpers.getSymbol(annotation) .equals(state.getSymbolFromString(annotationName))) { annotationNode = annotation; } } return annotationNode; } protected static class AnnotationReplacements implements Serializable { private final String goodAnnotation; private final String badAnnotation; protected AnnotationReplacements(String badAnnotation, String goodAnnotation) { this.goodAnnotation = goodAnnotation; this.badAnnotation = badAnnotation; } } }