New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce MockitoMockClassReference
check
#454
Conversation
} | ||
|
||
@Test | ||
void unimplementedCases() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like some input as to how we wish to behave in these edge-cases and what would be the ideal way to detect those cases.
" <T extends Number> T getGenericMock(Class<T> clazz) {", | ||
" // No idea how to detect type T as part of generics usage.", | ||
" return mock(clazz);", | ||
" }", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coming to think of it, if it were not such a complex case where T extends Number
, but where it was just T
, then we could likely rewrite it:
<T> T getGenericMock(Class<T> clazz) {
- return mock(clazz);
+ return mock();
}
Perhaps this case is just similar as variableTypedMock
above but instead with a generic result type. So we might be able to simplify these test cases a bit and indeed follow the ASTHelpers.isSameType
approach where it compared the erasure
of both types.
// XXX: Replace `var` usage with explicit type instead. | ||
private static final Matcher<VariableTree> INCOMPATIBLE_VARIABLE_TREE = | ||
anyOf(ASTHelpers::hasNoExplicitType, MockitoMockClassReference::hasTypeDifference); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed with @Stephan202 offline. It might be cool to also rewrite the following:
- var foo = mock(Foo.class);
+ Foo foo = mock();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't have the brain juice to go after this as it consists of multiple fixes. Feel free to have a crack at it. If someone wants to pair up, I'm open to that too. Otherwise, I'd favor leaving the XXX and revisiting this at a later time (and creating a GH issue for it).
" // This case is arguable as it is unsafe in any case.", | ||
" // This currently is not identified as the `erasure` is the same.", | ||
" List<String> genericallyTypedMock = mock(List.class);", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As pointed out, this is an unsafe cast in both before and after case. As such, at least in our own codebases, we sometimes have casts with SuppressWarnings
. If we are to rewrite these and we can circumvent these warnings, it might be nice to also remove existing suppressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I would have expected dropping List.class
to be safe here. And if we can drop a suppression in this case: great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should rewrite this case, if we would, it'd require a suppression as you mention. That is not an improvement IMO, so I wouldn't bother for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same holds for the getGenericMock
so I'm not sure if we would gain anything from that. Also, if we apply that change, then the parameter would be unused 🤔. So all in all, not sure if it would be an improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I filed mockito/mockito#2866; with those changes dropping List.class
would be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice @Badbond! I'll try to find some time to play with the code for proper advice; not sure I'll have time for that this weekend, though. But let's see 🤞
" // This case is arguable as it is unsafe in any case.", | ||
" // This currently is not identified as the `erasure` is the same.", | ||
" List<String> genericallyTypedMock = mock(List.class);", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I would have expected dropping List.class
to be safe here. And if we can drop a suppression in this case: great!
MockitoMockClassReference
check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a commit with some suggestions. Also some opinions on open stuff :).
argument(0, isSameType(Class.class.getName())), | ||
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy")); | ||
// XXX: Replace `var` usage with explicit type instead. | ||
private static final Matcher<VariableTree> INCOMPATIBLE_VARIABLE_TREE = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using the tree type in the name, we generally give it a name that describes what it flags :). Pushing a proposal.
I think dropping TREE
already makes it clear here :).
" // This case is arguable as it is unsafe in any case.", | ||
" // This currently is not identified as the `erasure` is the same.", | ||
" List<String> genericallyTypedMock = mock(List.class);", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should rewrite this case, if we would, it'd require a suppression as you mention. That is not an improvement IMO, so I wouldn't bother for now.
" // This case is arguable as it is unsafe in any case.", | ||
" // This currently is not identified as the `erasure` is the same.", | ||
" List<String> genericallyTypedMock = mock(List.class);", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same holds for the getGenericMock
so I'm not sure if we would gain anything from that. Also, if we apply that change, then the parameter would be unused 🤔. So all in all, not sure if it would be an improvement.
040d1fd
to
263bc5e
Compare
Looks good. All 22 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am happy with the current state of the PR to continue its review 👍. Thanks for filing the Mockito PR upstream @Stephan202.
@@ -45,11 +51,13 @@ public final class MockitoMockClassReference extends BugChecker | |||
private static final Matcher<MethodInvocationTree> MOCKITO_MOCK_OR_SPY = | |||
allOf( | |||
argumentCount(1), | |||
argument(0, isSameType(Class.class.getName())), | |||
argument(0, allOf(isSameType(Class.class.getName()), not(isVariable()))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There can be caveats when the passed argument is a variable that can lead into non-compilable/working-with-Mockito code, e.g. the following is odd and won't work:
-<T> T getMock(class<T> clazz) {
- return mock(clazz);
+<T> T getMock() {
+ return mock();
}
@Test
void mockType() {
- assertThat(getMock(BigInteger.class)).isInstanceOf(BigInteger.class);
+ assertThat((BigInteger) getMock()).isInstanceOf(BigInteger.class);
}
There might be cases where we could still replace the variable, but I couldn't find an elegant way for it without going into the caveats. For example the trivial:
-Class<BigInteger> clazz = BigInteger.class;
-BigInteger mock = mock(clazz);
+BigInteger mock = mock();
}
Feel free to play around and see if you find an elegant way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it's fair to leave this out of scope.
} | ||
|
||
private static boolean hasTypeDifference(ReturnTree tree, VisitorState state) { | ||
Tree returnTypeTree = requireNonNull(state.findEnclosing(MethodTree.class)).getReturnType(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This requireNonNull
is safe because passed is a ReturnTree
. Alternatively we could have passed the matching MethodInvocationTree
(to prevent having to do tree.getExpression
again), but then finding the enclosing MethodTree
is less 'safe' in case this method gets reused. I found this to be cleaner and clearer given the similar method signatures.
Furthermore, inlining as a lambda expression instead was flagged by ErrorProne, and indeed I do find this to read better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will not select the correct type in case of a return
statement inside a lambda expression, such as this case:
Stream.of("foo").map(x -> { return mock(Integer.class ); }).toArray();
(And if this code is part of an initializer block, then state.findEnclosing(MethodTree.class)
will return null
🙃.)
// XXX: Replace `var` usage with explicit type instead. | ||
private static final Matcher<VariableTree> INCOMPATIBLE_VARIABLE_TREE = | ||
anyOf(ASTHelpers::hasNoExplicitType, MockitoMockClassReference::hasTypeDifference); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't have the brain juice to go after this as it consists of multiple fixes. Feel free to have a crack at it. If someone wants to pair up, I'm open to that too. Otherwise, I'd favor leaving the XXX and revisiting this at a later time (and creating a GH issue for it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a quick look; I don't mind proposing a fix, but @Badbond if you wan to have a crack at it yourself, lemme know :)
Tree parent = state.getPath().getParentPath().getLeaf(); | ||
if (parent.getKind() == VARIABLE | ||
&& INCOMPATIBLE_VARIABLE.matches((VariableTree) parent, state)) { | ||
return Description.NO_MATCH; | ||
} else if (parent.getKind() == RETURN | ||
&& INCOMPATIBLE_RETURN.matches((ReturnTree) parent, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
return describeMatch(tree, SuggestedFix.delete(Iterables.getOnlyElement(tree.getArguments()))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code now assumes a match in case the parent is not a VARIABLE
or RETURN
statement. So the following statement is also (incorrectly) flagged:
Objects.hash(mock(Integer.class));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found another edge case, but I'm not sure if it is a problem or not... I was just playing around and did this random thing:
String s = new String(mock(String.class));
However, if we drop String.class
the new String(
can't detect which overload to use. Is that something we should fix? I'm not sure how realistic / niche this is 😋.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some rewriting and trying stuff out I found out we are strictly speaking too many cases. So the example in my previous comment is the Kind.NEW_CLASS
edge case. We should only rewrite things if we are sure it will be correct, so swapping the default here.
While doing so, I changed the setup to not use Matcher
s and have multiple hasTypeDifference
methods. PTAL and let me know what you think.
Feel free to! |
On the list it goes 😄 |
263bc5e
to
d8bffbb
Compare
Looks good. All 22 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a rewrite based on some (open) comments and edge cases that I spotted 😄. Let me know what you think.
Added a commit and rebased.
Tree parent = state.getPath().getParentPath().getLeaf(); | ||
if (parent.getKind() == VARIABLE | ||
&& INCOMPATIBLE_VARIABLE.matches((VariableTree) parent, state)) { | ||
return Description.NO_MATCH; | ||
} else if (parent.getKind() == RETURN | ||
&& INCOMPATIBLE_RETURN.matches((ReturnTree) parent, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
return describeMatch(tree, SuggestedFix.delete(Iterables.getOnlyElement(tree.getArguments()))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found another edge case, but I'm not sure if it is a problem or not... I was just playing around and did this random thing:
String s = new String(mock(String.class));
However, if we drop String.class
the new String(
can't detect which overload to use. Is that something we should fix? I'm not sure how realistic / niche this is 😋.
return describeMatch(tree, SuggestedFix.delete(Iterables.getOnlyElement(tree.getArguments()))); | ||
} | ||
|
||
private static boolean hasTypeDifference(VariableTree tree, VisitorState state) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After adding some random test cases I found some more examples / cases that should be covered. I'll push a change where this is addressed.
Tree parent = state.getPath().getParentPath().getLeaf(); | ||
if (parent.getKind() == VARIABLE | ||
&& INCOMPATIBLE_VARIABLE.matches((VariableTree) parent, state)) { | ||
return Description.NO_MATCH; | ||
} else if (parent.getKind() == RETURN | ||
&& INCOMPATIBLE_RETURN.matches((ReturnTree) parent, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
return describeMatch(tree, SuggestedFix.delete(Iterables.getOnlyElement(tree.getArguments()))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some rewriting and trying stuff out I found out we are strictly speaking too many cases. So the example in my previous comment is the Kind.NEW_CLASS
edge case. We should only rewrite things if we are sure it will be correct, so swapping the default here.
While doing so, I changed the setup to not use Matcher
s and have multiple hasTypeDifference
methods. PTAL and let me know what you think.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
ef8f41d
to
f2837e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased and added a commit. Didn't finish reviewing yet :)
return hasTypeDifference(returnTypeTree, ((ReturnTree) parent).getExpression(), state); | ||
} | ||
|
||
private static boolean hasTypeDifference(Tree treeA, Tree treeB, VisitorState state) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and above: it's generally preferred to use "positive" names (like hasSameType
).
if (parentKind != VARIABLE && parentKind != ASSIGNMENT && parentKind != RETURN) { | ||
return Description.NO_MATCH; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can combine this check with the if
/else
logic below using a switch
.
} | ||
|
||
private static boolean hasTypeDifference(ReturnTree tree, VisitorState state) { | ||
Tree returnTypeTree = requireNonNull(state.findEnclosing(MethodTree.class)).getReturnType(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will not select the correct type in case of a return
statement inside a lambda expression, such as this case:
Stream.of("foo").map(x -> { return mock(Integer.class ); }).toArray();
(And if this code is part of an initializer block, then state.findEnclosing(MethodTree.class)
will return null
🙃.)
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added one more commit. Also tweaked the suggested commit messages, as we now cover more methods. Eager to see this check in use 😄
" // BUG: Diagnostic contains:", | ||
" String memberMock = mock(String.class);", | ||
" // BUG: Diagnostic contains:", | ||
" String memberSpy = mock(String.class);", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
" String memberSpy = mock(String.class);", | |
" String memberSpy = spy(String.class);", |
summary = | ||
"Don't unnecessarily pass the type reference to Mockito's `mock(Class)` and spy(Class)`", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
summary = | |
"Don't unnecessarily pass the type reference to Mockito's `mock(Class)` and spy(Class)`", | |
summary = "Don't unnecessarily pass a type to Mockito's `mock(Class)` and `spy(Class)` methods", |
@@ -45,11 +51,13 @@ public final class MockitoMockClassReference extends BugChecker | |||
private static final Matcher<MethodInvocationTree> MOCKITO_MOCK_OR_SPY = | |||
allOf( | |||
argumentCount(1), | |||
argument(0, isSameType(Class.class.getName())), | |||
argument(0, allOf(isSameType(Class.class.getName()), not(isVariable()))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it's fair to leave this out of scope.
" // BUG: Diagnostic contains:", | ||
" variableMock = mock(Integer.class);", | ||
" // BUG: Diagnostic contains:", | ||
" List nonGenericallyTypedMock = mock(List.class);", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
" List nonGenericallyTypedMock = mock(List.class);", | |
" List rawListTypeMock = mock(List.class);", |
" var nonExplicitlyTypedMock = mock(Integer.class);", | ||
" Class<? extends Number> variableType = Integer.class;", | ||
" Number variableTypedMock = mock(variableType);", | ||
" Integer namedMock = mock(Integer.class, \"name\");", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can actually also simplify this one and the other two-arg overloads; it's barely any extra code. Will propose something :)
} | ||
|
||
@Test | ||
void replacement() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can simplify this test, since this fix isn't (so) context dependent.
Looks good. All 20 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
Looks good. All 20 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -13,17 +13,13 @@ void identification() { | |||
"A.java", | |||
"import static org.mockito.Mockito.mock;", | |||
"import static org.mockito.Mockito.spy;", | |||
"import static org.mockito.Mockito.withSettings;", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: always check for more edge cases that we can test 😛.
Good one. For this PR we should at least (manually?) clean up the impacted files. I see three options:
Tnx ;) |
Didn't notice that the diff referenced another repo 🙃. Anyway, options remain the same. |
Looks good. All 20 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
Added a commit with the XXX. I'd like to propose the following. Create a GH issue for (1) and keep (2) for later. Although that is the best way to solve it, it sounds like quite an involved check that not everyone is able to pick up. |
1 similar comment
Added a commit with the XXX. I'd like to propose the following. Create a GH issue for (1) and keep (2) for later. Although that is the best way to solve it, it sounds like quite an involved check that not everyone is able to pick up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having slept on it, I don't think (1) makes sense: an initial version of (2) could be one that only works for mock()
calls.
// as to disambiguate between method overloads. | ||
// XXX: This check currently does not flag (implicit or explicit) lambda return expressions. | ||
// XXX: This check currently does not drop suppressions that become obsolete after the | ||
// suggested fix is applied, consider adding support for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// suggested fix is applied, consider adding support for this. | |
// suggested fix is applied; consider adding support for this. |
Looks good. All 20 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
Let's discuss offline what the best approach would be. For now I'd propose to merge once 💚 ! 🚀 |
Thanks for the tweaks @Stephan202! PR LGTM for merge. Very nice that you flag additional cases of multi-arg mocking where we can still omit the class as well as the usage within lambda expressions. Amazing polish of tests, comments and casting too 😍.
I would also think (2) is the best option to also gain the most value. Complexity indeed to-be-seen (likely as part of implementation), but it sounds to me that this can be a generic check flagging safe casts (to subtype/to same type/raw-to-raw) annotated with the |
Opening PR as draft already to get some feedback on the best approach for the unimplemented test cases regarding type arguments and generics (see corresponding test).Suggested commit message:
Description
Adds a bug check that flags usages of
Mockito#mock(class)
andMockito#spy(Class)
which can be replaced with the no-arg overload, as introduced in Mockito 4.10.0 (mockito/mockito#2779). This is possible when the type this mock is assigned to is known and equal to the class passed to this method.Some simple cases:
Edge-cases
Subtypes
We don't want to replace cases where the class passed is a subtype of the class that the mock is assigned to. For example (where
Foo extends|implements Bar
):Changing this would otherwise make the assertion fail.
Classes with generic type arguments
It is safe to mock classes with generic type parameters (e.g.
List<E>
) as the mock should be programmed either way based on the type of the type parameter. Therefore these are currently rewritten. For example:Variably typed mocks
When the type of the mock is variable, especially when encapsulated in a method parameter, there can be problems when removing the argument. Therefore, currently, these cases are not supported. A simple case that could be rewritten is:
However, when the variable is dynamic, it could lead to problems. For example:
Non-explicitly typed mocks
When the mock is assigned to a non-explicitly typed variable (i.e. when using
var
), we can not simply drop the mock argument as otherwise Mockito could not derive its type. Thus we will not rewrite: