/* * 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.BugPattern.Category.JUNIT; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.JUnitMatchers.JUNIT_AFTER_ANNOTATION; import static com.google.errorprone.matchers.JUnitMatchers.JUNIT_BEFORE_ANNOTATION; import static com.google.errorprone.matchers.Matchers.assertStatement; import static com.google.errorprone.matchers.Matchers.assignment; import static com.google.errorprone.matchers.Matchers.booleanConstant; import static com.google.errorprone.matchers.Matchers.booleanLiteral; import static com.google.errorprone.matchers.Matchers.contains; import static com.google.errorprone.matchers.Matchers.continueStatement; import static com.google.errorprone.matchers.Matchers.ignoreParens; import static com.google.errorprone.matchers.Matchers.isInstanceField; import static com.google.errorprone.matchers.Matchers.isSameType; import static com.google.errorprone.matchers.Matchers.isSubtypeOf; import static com.google.errorprone.matchers.Matchers.isVariable; import static com.google.errorprone.matchers.Matchers.methodInvocation; import static com.google.errorprone.matchers.Matchers.nextStatement; import static com.google.errorprone.matchers.Matchers.returnStatement; import static com.google.errorprone.matchers.Matchers.throwStatement; import static com.google.errorprone.matchers.Matchers.toType; import static com.google.errorprone.matchers.method.MethodMatchers.anyMethod; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.TryTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.ChildMultiMatcher; import com.google.errorprone.matchers.ChildMultiMatcher.MatchType; 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.matchers.MultiMatcher; import com.google.errorprone.matchers.NextStatement; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.CatchTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.DoWhileLoopTree; import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ForLoopTree; 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.Tree; import com.sun.source.tree.TryTree; import com.sun.source.tree.VariableTree; import com.sun.source.tree.WhileLoopTree; import com.sun.tools.javac.code.Symbol; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import javax.lang.model.element.Name; /** @author schmitt@google.com (Peter Schmitt) */ @BugPattern( name = "MissingFail", altNames = "missing-fail", summary = "Not calling fail() when expecting an exception masks bugs", category = JUNIT, severity = WARNING ) public class MissingFail extends BugChecker implements TryTreeMatcher { // Many test writers don't seem to know about `fail()`. They instead use synonyms of varying // complexity instead. // // One category of synonyms replaces `fail()` with a single, equivalent statement // such as `assertTrue(false)`, `assertFalse(true)` or `throw new // AssertionError()`. In these cases we will simply skip any further analysis, they // work perfectly fine. // // Other, more complex synonyms tend to use boolean variables, like such: // // ```java // boolean thrown = false; // try { // throwingExpression(); // } catch (SomeException e) { // thrown = true; // } // assertTrue(thrown); // ``` private static final Matcher<ExpressionTree> ASSERT_EQUALS = Matchers.anyOf( staticMethod().onClass("org.junit.Assert").named("assertEquals"), staticMethod().onClass("junit.framework.Assert").named("assertEquals"), staticMethod().onClass("junit.framework.TestCase").named("assertEquals")); private static final Matcher<Tree> ASSERT_UNEQUAL = toType(MethodInvocationTree.class, new UnequalIntegerLiteralMatcher(ASSERT_EQUALS)); private static final Matcher<ExpressionTree> ASSERT_TRUE = Matchers.anyOf( staticMethod().onClass("org.junit.Assert").named("assertTrue"), staticMethod().onClass("junit.framework.Assert").named("assertTrue"), staticMethod().onClass("junit.framework.TestCase").named("assertTrue")); private static final Matcher<ExpressionTree> ASSERT_FALSE = Matchers.anyOf( staticMethod().onClass("org.junit.Assert").named("assertFalse"), staticMethod().onClass("junit.framework.Assert").named("assertFalse"), staticMethod().onClass("junit.framework.TestCase").named("assertFalse")); private static final Matcher<ExpressionTree> ASSERT_TRUE_FALSE = methodInvocation(ASSERT_TRUE, MatchType.AT_LEAST_ONE, Matchers.anyOf(booleanLiteral(false), booleanConstant(false))); private static final Matcher<ExpressionTree> ASSERT_FALSE_TRUE = methodInvocation(ASSERT_FALSE, MatchType.AT_LEAST_ONE, Matchers.anyOf( booleanLiteral(true), booleanConstant(true))); private static final Matcher<ExpressionTree> ASSERT_TRUE_TRUE = methodInvocation(ASSERT_TRUE, MatchType.AT_LEAST_ONE, Matchers.anyOf( booleanLiteral(true), booleanConstant(true))); private static final Matcher<ExpressionTree> ASSERT_FALSE_FALSE = methodInvocation(ASSERT_FALSE, MatchType.AT_LEAST_ONE, Matchers.anyOf( booleanLiteral(false), booleanConstant(false))); private static final Matcher<StatementTree> JAVA_ASSERT_FALSE = assertStatement(ignoreParens(Matchers.anyOf(booleanLiteral(false), booleanConstant(false)))); private static final Matcher<ExpressionTree> LOG_CALL = methodInvocation(new LogMethodMatcher()); private static final Matcher<Tree> LOG_IN_BLOCK = contains(toType(ExpressionTree.class, LOG_CALL)); private static final Pattern FAIL_PATTERN = Pattern.compile(".*(?i:fail).*"); private static final Matcher<ExpressionTree> FAIL = anyMethod().anyClass().withNameMatching(FAIL_PATTERN); private static final Matcher<ExpressionTree> ASSERT_CALL = methodInvocation(new AssertMethodMatcher()); private static final Matcher<ExpressionTree> REAL_ASSERT_CALL = Matchers.allOf( ASSERT_CALL, Matchers.not(Matchers.anyOf(ASSERT_FALSE_FALSE, ASSERT_TRUE_TRUE))); private static final Matcher<ExpressionTree> VERIFY_CALL = methodInvocation(staticMethod().onClass("org.mockito.Mockito").named("verify")); private static final MultiMatcher<TryTree, Tree> ASSERT_LAST_CALL_IN_TRY = new ChildOfTryMatcher( MatchType.LAST, contains(toType(ExpressionTree.class, Matchers.anyOf(REAL_ASSERT_CALL, VERIFY_CALL)))); private static final Matcher<Tree> ASSERT_IN_BLOCK = contains(toType(ExpressionTree.class, REAL_ASSERT_CALL)); private static final Matcher<StatementTree> THROW_STATEMENT = throwStatement(Matchers.anything()); private static final Matcher<Tree> THROW_OR_FAIL_IN_BLOCK = contains( Matchers.anyOf( toType(StatementTree.class, THROW_STATEMENT), // TODO(schmitt): Include Preconditions.checkState(false)? toType(ExpressionTree.class, ASSERT_TRUE_FALSE), toType(ExpressionTree.class, ASSERT_FALSE_TRUE), toType(ExpressionTree.class, ASSERT_UNEQUAL), toType(StatementTree.class, JAVA_ASSERT_FALSE), toType(ExpressionTree.class, FAIL))); private static final Matcher<TryTree> NON_TEST_METHOD = new IgnoredEnclosingMethodMatcher(); private static final Matcher<Tree> RETURN_IN_BLOCK = contains(toType(StatementTree.class, returnStatement(Matchers.anything()))); private static final NextStatement<StatementTree> RETURN_AFTER = nextStatement(returnStatement(Matchers.anything())); private static final Matcher<VariableTree> INAPPLICABLE_EXCEPTION = Matchers.anyOf( isSameType("java.lang.InterruptedException"), isSameType("java.lang.AssertionError"), isSameType("java.lang.Throwable"), isSameType("junit.framework.AssertionFailedError")); private static final InLoopMatcher IN_LOOP = new InLoopMatcher(); private static final Matcher<Tree> WHILE_TRUE_IN_BLOCK = contains(toType(WhileLoopTree.class, new WhileTrueLoopMatcher())); private static final Matcher<Tree> CONTINUE_IN_BLOCK = contains(toType(StatementTree.class, continueStatement())); private static final Matcher<AssignmentTree> FIELD_ASSIGNMENT = assignment(isInstanceField(), Matchers.<ExpressionTree>anything()); private static final Matcher<Tree> FIELD_ASSIGNMENT_IN_BLOCK = contains(toType(AssignmentTree.class, FIELD_ASSIGNMENT)); private static final Matcher<ExpressionTree> BOOLEAN_ASSERT_VAR = methodInvocation( Matchers.anyOf(ASSERT_FALSE, ASSERT_TRUE), MatchType.AT_LEAST_ONE, Matchers.anyOf(isInstanceField(), isVariable())); private static final Matcher<Tree> BOOLEAN_ASSERT_VAR_IN_BLOCK = contains(toType(ExpressionTree.class, BOOLEAN_ASSERT_VAR)); // Subtly different from JUnitMatchers: We want to match test base classes too. private static final Matcher<ClassTree> JUNIT3_TEST_CLASS = isSubtypeOf("junit.framework.TestCase"); private static final Matcher<ClassTree> TEST_CLASS = Matchers.anyOf(JUNIT3_TEST_CLASS, new JUnitMatchers.JUnit4TestClassMatcher()); @Override public Description matchTry(TryTree tree, VisitorState state) { if (tryTreeMatches(tree, state)) { List<? extends StatementTree> tryStatements = tree.getBlock().getStatements(); StatementTree lastTryStatement = tryStatements.get(tryStatements.size() - 1); String failCall = String.format("\nfail(\"Expected %s\");", exceptionToString(tree)); SuggestedFix.Builder fixBuilder = SuggestedFix.builder() .postfixWith(lastTryStatement, failCall); // Make sure that when the fail import is added it doesn't conflict with existing ones. fixBuilder.removeStaticImport("junit.framework.Assert.fail"); fixBuilder.removeStaticImport("junit.framework.TestCase.fail"); fixBuilder.addStaticImport("org.junit.Assert.fail"); return describeMatch(lastTryStatement, fixBuilder.build()); } else { return Description.NO_MATCH; } } /** * Returns a string describing the exception type caught by the given try tree's catch * statement(s), defaulting to {@code "Exception"} if more than one exception type is caught. */ private String exceptionToString(TryTree tree) { if (tree.getCatches().size() != 1) { return "Exception"; } String exceptionType = tree.getCatches().iterator().next().getParameter().getType().toString(); if (exceptionType.contains("|")) { return "Exception"; } return exceptionType; } private boolean tryTreeMatches(TryTree tree, VisitorState state) { if (!isInClass(tree, state, TEST_CLASS)) { return false; } if (hasToleratedException(tree)) { return false; } boolean assertInCatch = hasAssertInCatch(tree, state); if (!hasExpectedException(tree) && !assertInCatch) { return false; } if (hasThrowOrFail(tree, state) || isInInapplicableMethod(tree, state) || returnsInTryCatchOrAfter(tree, state) || isInapplicableExceptionType(tree, state) || isInLoop(state, tree) || hasWhileTrue(tree, state) || hasContinue(tree, state) || hasFinally(tree) || logsInCatch(state, tree)) { return false; } if (assertInCatch && (hasFieldAssignmentInCatch(tree, state) || hasBooleanAssertVariableInCatch(tree, state) || lastTryStatementIsAssert(tree, state))) { return false; } return true; } private boolean hasWhileTrue(TryTree tree, VisitorState state) { return WHILE_TRUE_IN_BLOCK.matches(tree, state); } private boolean isInClass(TryTree tree, VisitorState state, Matcher<ClassTree> classTree) { return Matchers.enclosingNode(toType(ClassTree.class, classTree)).matches(tree, state); } private boolean hasBooleanAssertVariableInCatch(TryTree tree, VisitorState state) { return anyCatchBlockMatches(tree, state, BOOLEAN_ASSERT_VAR_IN_BLOCK); } private boolean lastTryStatementIsAssert(TryTree tree, VisitorState state) { return ASSERT_LAST_CALL_IN_TRY.matches(tree, state); } private boolean hasFieldAssignmentInCatch(TryTree tree, VisitorState state) { return anyCatchBlockMatches(tree, state, FIELD_ASSIGNMENT_IN_BLOCK); } private boolean logsInCatch(VisitorState state, TryTree tree) { return anyCatchBlockMatches(tree, state, LOG_IN_BLOCK); } private boolean hasFinally(TryTree tree) { return tree.getFinallyBlock() != null; } private boolean hasContinue(TryTree tree, VisitorState state) { return CONTINUE_IN_BLOCK.matches(tree, state); } private boolean isInLoop(VisitorState state, TryTree tree) { return IN_LOOP.matches(tree, state); } private boolean isInapplicableExceptionType(TryTree tree, VisitorState state) { for (CatchTree catchTree : tree.getCatches()) { if (INAPPLICABLE_EXCEPTION.matches(catchTree.getParameter(), state)) { return true; } } return false; } private boolean returnsInTryCatchOrAfter(TryTree tree, VisitorState state) { return RETURN_IN_BLOCK.matches(tree, state) || RETURN_AFTER.matches(tree, state); } private boolean isInInapplicableMethod(TryTree tree, VisitorState state) { return NON_TEST_METHOD.matches(tree, state); } private boolean hasThrowOrFail(TryTree tree, VisitorState state) { return THROW_OR_FAIL_IN_BLOCK.matches(tree, state); } private boolean hasAssertInCatch(TryTree tree, VisitorState state) { return anyCatchBlockMatches(tree, state, ASSERT_IN_BLOCK); } private boolean hasToleratedException(TryTree tree) { for (CatchTree catchTree : tree.getCatches()) { if (catchTree.getParameter().getName().contentEquals("tolerated")) { return true; } } return false; } private boolean hasExpectedException(TryTree tree) { for (CatchTree catchTree : tree.getCatches()) { if (catchTree.getParameter().getName().contentEquals("expected")) { return true; } } return false; } private boolean anyCatchBlockMatches(TryTree tree, VisitorState state, Matcher<Tree> matcher) { for (CatchTree catchTree : tree.getCatches()) { if (matcher.matches(catchTree.getBlock(), state)) { return true; } } return false; } private static class AssertMethodMatcher implements Matcher<ExpressionTree> { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(expressionTree); if (sym == null) { return false; } String symSimpleName = sym.getSimpleName().toString(); return symSimpleName.startsWith("assert") || symSimpleName.startsWith("verify"); } } private static class LogMethodMatcher implements Matcher<ExpressionTree> { @Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(expressionTree); if (sym != null && sym.getSimpleName().toString().startsWith("log")) { return true; } if (sym != null && sym.isStatic()) { if (sym.owner.getQualifiedName().toString().contains("Logger")) { return true; } } else if (expressionTree instanceof MemberSelectTree) { if (((MemberSelectTree) expressionTree).getExpression().toString().startsWith("log")) { return true; } } return false; } } /** * Matches any try-tree that is enclosed in a loop. */ private static class InLoopMatcher implements Matcher<TryTree> { @Override public boolean matches(TryTree tryTree, VisitorState state) { return ASTHelpers.findEnclosingNode(state.getPath(), DoWhileLoopTree.class) != null || ASTHelpers.findEnclosingNode(state.getPath(), EnhancedForLoopTree.class) != null || ASTHelpers.findEnclosingNode(state.getPath(), WhileLoopTree.class) != null || ASTHelpers.findEnclosingNode(state.getPath(), ForLoopTree.class) != null; } } private static class WhileTrueLoopMatcher implements Matcher<WhileLoopTree> { @Override public boolean matches(WhileLoopTree tree, VisitorState state) { return ignoreParens(booleanLiteral(true)) .matches(tree.getCondition(), state); } } /** * Matches any try-tree that is in a JUNit3 {@code setUp} or {@code tearDown} method, their JUnit4 * equivalents, a JUnit {@code suite()} method or a {@code main} method. */ private static class IgnoredEnclosingMethodMatcher implements Matcher<TryTree> { @Override public boolean matches(TryTree tryTree, VisitorState state) { MethodTree enclosingMethodTree = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class); Name name = enclosingMethodTree.getName(); return JUnitMatchers.looksLikeJUnit3SetUp.matches(enclosingMethodTree, state) || JUnitMatchers.looksLikeJUnit3TearDown.matches(enclosingMethodTree, state) || name.contentEquals("main") // TODO(schmitt): Move to JUnitMatchers? || name.contentEquals("suite") || Matchers.hasAnnotation(JUNIT_BEFORE_ANNOTATION).matches(enclosingMethodTree, state) || Matchers.hasAnnotation(JUNIT_AFTER_ANNOTATION).matches(enclosingMethodTree, state); } } /** * Matches if any two of the given list of expressions are integer literals with different values. */ private static class UnequalIntegerLiteralMatcher implements Matcher<MethodInvocationTree> { private final Matcher<ExpressionTree> methodSelectMatcher; private UnequalIntegerLiteralMatcher(Matcher<ExpressionTree> methodSelectMatcher) { this.methodSelectMatcher = methodSelectMatcher; } @Override public boolean matches(MethodInvocationTree methodInvocationTree, VisitorState state) { return methodSelectMatcher.matches(methodInvocationTree, state) && matches(methodInvocationTree.getArguments()); } private boolean matches(List<? extends ExpressionTree> expressionTrees) { Set<Integer> foundValues = new HashSet<>(); for (Tree tree : expressionTrees) { if (tree instanceof LiteralTree) { Object value = ((LiteralTree) tree).getValue(); if (value instanceof Integer) { boolean duplicate = !foundValues.add((Integer) value); if (!duplicate && foundValues.size() > 1) { return true; } } } } return false; } } private static class ChildOfTryMatcher extends ChildMultiMatcher<TryTree, Tree> { public ChildOfTryMatcher(MatchType matchType, Matcher<Tree> nodeMatcher) { super(matchType, nodeMatcher); } @Override protected Iterable<? extends StatementTree> getChildNodes(TryTree tree, VisitorState state) { return tree.getBlock().getStatements(); } } }