/*
* Copyright 2015 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.MOCKITO;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.Matchers.anyOf;
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.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import java.util.List;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "MockitoUsage",
summary = "Missing method call for verify(mock) here",
category = MOCKITO,
severity = ERROR
)
public class MockitoUsage extends BugChecker implements MethodInvocationTreeMatcher {
private static final String MESSAGE_FORMAT = "Missing method call for %s here";
private static final Matcher<ExpressionTree> MOCK_METHOD =
anyOf(
staticMethod().onClass("org.mockito.Mockito").withSignature("<T>when(T)"),
staticMethod().onClass("org.mockito.Mockito").withSignature("<T>verify(T)"),
staticMethod()
.onClass("org.mockito.Mockito")
.withSignature("<T>verify(T,org.mockito.verification.VerificationMode)"));
private static final Matcher<ExpressionTree> NEVER_METHOD =
staticMethod().onClass("org.mockito.Mockito").withSignature("never()");
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!MOCK_METHOD.matches(tree, state)) {
return Description.NO_MATCH;
}
if (state.getPath().getParentPath().getLeaf().getKind() != Tree.Kind.EXPRESSION_STATEMENT) {
return Description.NO_MATCH;
}
String message = String.format(MESSAGE_FORMAT, state.getSourceForNode(tree));
Description.Builder builder = buildDescription(tree).setMessage(message);
buildFix(builder, tree, state);
return builder.build();
}
/**
* Create fixes for invalid assertions.
*
* <ul>
* <li>Rewrite `verify(mock.bar())` to `verify(mock).bar()`
* <li>Rewrite `verify(mock.bar(), times(N))` to `verify(mock, times(N)).bar()`
* <li>Rewrite `verify(mock, never())` to `verifyZeroInteractions(mock)`
* <li>Finally, offer to delete the mock statement.
* </ul>
*/
private void buildFix(
Description.Builder builder, MethodInvocationTree tree, VisitorState state) {
MethodInvocationTree mockitoCall = tree;
List<? extends ExpressionTree> args = mockitoCall.getArguments();
Tree mock = mockitoCall.getArguments().get(0);
boolean isVerify = ASTHelpers.getSymbol(tree).getSimpleName().contentEquals("verify");
if (isVerify && mock.getKind() == Kind.METHOD_INVOCATION) {
MethodInvocationTree invocation = (MethodInvocationTree) mock;
String verify = state.getSourceForNode(mockitoCall.getMethodSelect());
String receiver = state.getSourceForNode(ASTHelpers.getReceiver(invocation));
String mode = args.size() > 1 ? ", " + state.getSourceForNode(args.get(1)) : "";
String call = state.getSourceForNode(invocation).substring(receiver.length());
builder.addFix(
SuggestedFix.replace(tree, String.format("%s(%s%s)%s", verify, receiver, mode, call)));
}
if (isVerify && args.size() > 1 && NEVER_METHOD.matches(args.get(1), state)) {
// TODO(cushon): handle times(0) the same as never()
builder.addFix(
SuggestedFix.builder()
.addStaticImport("org.mockito.Mockito.verifyZeroInteractions")
.replace(tree, String.format("verifyZeroInteractions(%s)", mock))
.build());
}
// Always suggest the naive semantics-preserving option, which is just to
// delete the assertion:
Tree parent = state.getPath().getParentPath().getLeaf();
if (parent.getKind() == Kind.EXPRESSION_STATEMENT) {
// delete entire expression statement
builder.addFix(SuggestedFix.delete(parent));
} else {
builder.addFix(SuggestedFix.delete(tree));
}
}
}