-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
102 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
.../src/main/java/tech/picnic/errorprone/workshop/bugpatterns/MockitoMockClassReference.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package tech.picnic.errorprone.workshop.bugpatterns; | ||
|
||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; | ||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION; | ||
import static com.google.errorprone.matchers.Matchers.allOf; | ||
import static com.google.errorprone.matchers.Matchers.argument; | ||
import static com.google.errorprone.matchers.Matchers.isSameType; | ||
import static com.google.errorprone.matchers.Matchers.isVariable; | ||
import static com.google.errorprone.matchers.Matchers.not; | ||
import static com.google.errorprone.matchers.Matchers.staticMethod; | ||
|
||
import com.google.auto.service.AutoService; | ||
import com.google.errorprone.BugPattern; | ||
import com.google.errorprone.VisitorState; | ||
import com.google.errorprone.bugpatterns.BugChecker; | ||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; | ||
import com.google.errorprone.fixes.SuggestedFixes; | ||
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.LambdaExpressionTree; | ||
import com.sun.source.tree.MethodInvocationTree; | ||
import com.sun.source.tree.MethodTree; | ||
import com.sun.source.tree.Tree; | ||
import com.sun.source.tree.VariableTree; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
/** | ||
* A {@link BugChecker} that flags the use of {@link org.mockito.Mockito#mock(Class)} and {@link | ||
* org.mockito.Mockito#spy(Class)} where instead the type to be mocked or spied can be derived from | ||
* context. | ||
*/ | ||
@AutoService(BugChecker.class) | ||
@BugPattern( | ||
summary = "Don't unnecessarily pass a type to Mockito's `mock(Class)` and `spy(Class)` methods", | ||
severity = SUGGESTION, | ||
tags = SIMPLIFICATION) | ||
public final class MockitoMockClassReference extends BugChecker | ||
implements MethodInvocationTreeMatcher { | ||
private static final long serialVersionUID = 1L; | ||
private static final Matcher<MethodInvocationTree> MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE = | ||
allOf( | ||
argument(0, allOf(isSameType(Class.class.getName()), not(isVariable()))), | ||
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy")); | ||
|
||
/** Instantiates a new {@link MockitoMockClassReference} instance. */ | ||
public MockitoMockClassReference() {} | ||
|
||
@Override | ||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { | ||
if (!MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE.matches(tree, state) | ||
|| !isTypeDerivableFromContext(tree, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
List<? extends ExpressionTree> arguments = tree.getArguments(); | ||
return describeMatch(tree, SuggestedFixes.removeElement(arguments.get(0), arguments, state)); | ||
} | ||
|
||
private static boolean isTypeDerivableFromContext(MethodInvocationTree tree, VisitorState state) { | ||
Tree parent = state.getPath().getParentPath().getLeaf(); | ||
switch (parent.getKind()) { | ||
case VARIABLE: | ||
return !ASTHelpers.hasImplicitType((VariableTree) parent, state) | ||
&& areSameType(tree, parent, state); | ||
case ASSIGNMENT: | ||
return areSameType(tree, parent, state); | ||
case RETURN: | ||
return findMethodExitedOnReturn(state) | ||
.filter(m -> areSameType(tree, m.getReturnType(), state)) | ||
.isPresent(); | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Tells whether the given trees are of the same type, after type erasure. | ||
* | ||
* @param treeA The first tree of interest. | ||
* @param treeB The second tree of interest. | ||
* @param state The {@link VisitorState} describing the context in which the given trees were | ||
* found. | ||
* @return Whether the specified trees have the same erased types. | ||
*/ | ||
private static boolean areSameType(Tree treeA, Tree treeB, VisitorState state) { | ||
return ASTHelpers.isSameType(ASTHelpers.getType(treeA), ASTHelpers.getType(treeB), state); | ||
} | ||
|
||
private static Optional<MethodTree> findMethodExitedOnReturn(VisitorState state) { | ||
return Optional.ofNullable(state.findEnclosing(LambdaExpressionTree.class, MethodTree.class)) | ||
.filter(MethodTree.class::isInstance) | ||
.map(MethodTree.class::cast); | ||
} | ||
} |