/*
* Copyright 2012 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.anyOf;
import static com.google.errorprone.matchers.Matchers.contains;
import static com.google.errorprone.matchers.Matchers.enclosingMethod;
import static com.google.errorprone.matchers.Matchers.enclosingNode;
import static com.google.errorprone.matchers.Matchers.expressionStatement;
import static com.google.errorprone.matchers.Matchers.isLastStatementInBlock;
import static com.google.errorprone.matchers.Matchers.kindIs;
import static com.google.errorprone.matchers.Matchers.methodInvocation;
import static com.google.errorprone.matchers.Matchers.methodSelect;
import static com.google.errorprone.matchers.Matchers.nextStatement;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.parentNode;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import java.util.regex.Pattern;
/**
* An abstract base class to match method invocations in which the return value is not used.
*
* @author eaftan@google.com (Eddie Aftandilian)
*/
public abstract class AbstractReturnValueIgnored extends BugChecker
implements MethodInvocationTreeMatcher {
@Override
public Description matchMethodInvocation(
MethodInvocationTree methodInvocationTree, VisitorState state) {
if (allOf(
parentNode(Matchers.<MethodInvocationTree>kindIs(Kind.EXPRESSION_STATEMENT)),
not(
methodSelect(
Matchers.<ExpressionTree>allOf(
kindIs(Kind.IDENTIFIER), identifierHasName("super")))),
not((t, s) -> ASTHelpers.isVoidType(ASTHelpers.getType(t), s)),
not(AbstractReturnValueIgnored::expectedExceptionTest),
specializedMatcher())
.matches(methodInvocationTree, state)) {
return describe(methodInvocationTree, state);
}
return Description.NO_MATCH;
}
/**
* Match whatever additional conditions concrete subclasses want to match (a list of known
* side-effect-free methods, has a @CheckReturnValue annotation, etc.).
*/
public abstract Matcher<? super MethodInvocationTree> specializedMatcher();
private static Matcher<ExpressionTree> identifierHasName(final String name) {
return new Matcher<ExpressionTree>() {
@Override
public boolean matches(ExpressionTree item, VisitorState state) {
return ((IdentifierTree) item).getName().contentEquals(name);
}
};
}
/**
* Fixes the error by assigning the result of the call to the receiver reference, or deleting the
* method call.
*/
public Description describe(MethodInvocationTree methodInvocationTree, VisitorState state) {
// Find the root of the field access chain, i.e. a.intern().trim() ==> a.
ExpressionTree identifierExpr = ASTHelpers.getRootAssignable(methodInvocationTree);
String identifierStr = null;
Type identifierType = null;
if (identifierExpr != null) {
identifierStr = identifierExpr.toString();
if (identifierExpr instanceof JCIdent) {
identifierType = ((JCIdent) identifierExpr).sym.type;
} else if (identifierExpr instanceof JCFieldAccess) {
identifierType = ((JCFieldAccess) identifierExpr).sym.type;
} else {
throw new IllegalStateException("Expected a JCIdent or a JCFieldAccess");
}
}
Type returnType =
ASTHelpers.getReturnType(((JCMethodInvocation) methodInvocationTree).getMethodSelect());
Fix fix;
if (identifierStr != null
&& !"this".equals(identifierStr)
&& returnType != null
&& state.getTypes().isAssignable(returnType, identifierType)) {
// Fix by assigning the assigning the result of the call to the root receiver reference.
fix = SuggestedFix.prefixWith(methodInvocationTree, identifierStr + " = ");
} else {
// Unclear what the programmer intended. Delete since we don't know what else to do.
Tree parent = state.getPath().getParentPath().getLeaf();
fix = SuggestedFix.delete(parent);
}
return describeMatch(methodInvocationTree, fix);
}
/** Allow return values to be ignored in tests that expect an exception to be thrown. */
static boolean expectedExceptionTest(Tree tree, VisitorState state) {
if (mockitoInvocation(tree, state)) {
return true;
}
// Allow unused return values in tests that check for thrown exceptions, e.g.:
//
// try {
// Foo.newFoo(-1);
// fail();
// } catch (IllegalArgumentException expected) {
// }
//
StatementTree statement = ASTHelpers.findEnclosingNode(state.getPath(), StatementTree.class);
if (statement != null && EXPECTED_EXCEPTION_MATCHER.matches(statement, state)) {
return true;
}
return false;
}
private static final Matcher<ExpressionTree> FAIL_METHOD =
anyOf(
instanceMethod().onDescendantOf("com.google.common.truth.AbstractVerb").named("fail"),
staticMethod().onClass("org.junit.Assert").named("fail"),
staticMethod().onClass("junit.framework.Assert").named("fail"),
staticMethod().onClass("junit.framework.TestCase").named("fail"));
private static final Matcher<ExpressionTree> EXPECT_THROWS =
anyOf(
// JUnit 4
staticMethod().onClass("org.junit.Assert").named("assertThrows"),
staticMethod().onClass("org.junit.Assert").named("expectThrows"),
// JUnit 5
staticMethod().onClass("org.junit.jupiter.api.Assertions").named("assertThrows"),
staticMethod().onClass("org.junit.jupiter.api.Assertions").named("expectThrows"));
private static final Matcher<StatementTree> EXPECTED_EXCEPTION_MATCHER =
anyOf(
// expectedException.expect(Foo.class); me();
allOf(
isLastStatementInBlock(),
enclosingMethod(
contains(
ExpressionTree.class,
methodInvocation(
instanceMethod()
.onDescendantOf("org.junit.rules.TestRule")
.withNameMatching(Pattern.compile("expect(Message|Cause)?")))))),
// try { me(); fail(); } catch (Throwable t) {}
allOf(enclosingNode(kindIs(Kind.TRY)), nextStatement(expressionStatement(FAIL_METHOD))),
// assertThrows(Throwable.class, () => { me(); })
allOf(
isLastStatementInBlock(),
enclosingNode(
// Extra kindIs is needed as methodInvocation will cast each parent node to
// ExpressionTree.
allOf(kindIs(Kind.METHOD_INVOCATION), methodInvocation(EXPECT_THROWS)))));
private static final Matcher<ExpressionTree> MOCKITO_MATCHER =
anyOf(
staticMethod().onClass("org.mockito.Mockito").named("verify"),
instanceMethod().onDescendantOf("org.mockito.stubbing.Stubber").named("when"),
instanceMethod().onDescendantOf("org.mockito.InOrder").named("verify"));
/**
* Don't match the method that is invoked through {@code Mockito.verify(t)} or {@code
* doReturn(val).when(t)}.
*/
private static boolean mockitoInvocation(Tree tree, VisitorState state) {
if (!(tree instanceof JCMethodInvocation)) {
return false;
}
JCMethodInvocation invocation = (JCMethodInvocation) tree;
if (!(invocation.getMethodSelect() instanceof JCFieldAccess)) {
return false;
}
ExpressionTree receiver = ASTHelpers.getReceiver(invocation);
return MOCKITO_MATCHER.matches(receiver, state);
}
}