-
Notifications
You must be signed in to change notification settings - Fork 39
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 IdentityConversionCheck
#27
Conversation
This PR replaces: PicnicSupermarket/error-prone#5 |
The |
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.
Some suggestions and questions 🙂
Integer.class.getName(), | ||
String.class.getName()) | ||
.named("valueOf"), | ||
staticMethod().onClass("reactor.adapter.rxjava.RxJava2Adapter"), |
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.
Probably I just had a really hard time finding it, for which method does this check apply for the RxJava2Adapter
? 🤔
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.
For the conversion methods like flowableToFlux
and singleToMono
. The idea for this check came while we were working on the migration of RxJava to Reactor. What we saw is that there were unnecessary conversions left after the migration was "complete". Since the Flux#flatMap
accepts a Publisher
, a Flowable
is valid input for the flatMap
. What we saw is RxJava2Adapter.fluxToFlowable(flux)
in the flatMap
. This check should identify these cases and remove the unnecessary conversion. In this class, you can see that we could remove some candidates because of this 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.
Ah, that makes total sense, thanks a lot!
NB: Would still make sense to test this. 👀
staticMethod() | ||
.onClass("reactor.core.publisher.Flux") | ||
.namedAnyOf("concat", "firstWithSignal", "from", "merge"), |
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.
IIUC for Flux#concat
, this check only handles the
Flux<T> concat(Publisher<? extends T>... sources)
case. What about e.g.
Flux<T> concat(Iterable<? extends Publisher<? extends T>> sources)
couldn't we statically also check whether the Iterable
only contains a single element?
Or would this be handled by a combination by refaster + this check already? 👀
Same thing should also apply for Flux#merge
and its overloads.
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.
Interesting, good one. I think we could best achieve this by creating a Matcher
that is named something like: ...ContainsSingleElement
and add some Refaster templates that use the Matcher
. However, if we decide to go for this, I would suggest a follow-up PR.
"com.google.common.collect.ImmutableBiMap", | ||
"com.google.common.collect.ImmutableList", |
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.
What about ImmutableEnumMap
and ImmutableEnumSet
?
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.
They don't have a copyOf
method 😉
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, both inherit a copyOf
method from Immutable{Map,Set}
respectively.
I frankly don't know: Would this then already be covered by the defined Immutable{Map,Set}
here?
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.
Ah good one, I totally forgot that 😄. Will check that.
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.
ImmutableEnumMap
and ImmutableEnumSet
are package-private and final. So in the source code we can only ever match (public) supertypes of these. I.o.w.: ImmutableEnumMap
and ImmutableEnumSet
are implementation details we don't need to concern ourselves with.
"com.google.common.collect.ImmutableMap", | ||
"com.google.common.collect.ImmutableMultimap", | ||
"com.google.common.collect.ImmutableMultiset", | ||
"com.google.common.collect.ImmutableRangeSet", |
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.
TIL of a RangeSet
, and of a RangeMap
. Why not add the latter as ImmutableRangeMap
?
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.
Good one, that one has a copyOf
method 😄 .
|
||
ExpressionTree sourceTree = arguments.get(0); | ||
Type sourceType = ASTHelpers.getType(sourceTree); | ||
TargetType targetType = ASTHelpers.targetType(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.
Genuine question: What's the target type here exactly? Why not ASTHelpers.getType(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.
Javadoc getType
:
Returns the Type of the given tree, or null if the type could not be determined.
Javadoc getTargetType
:
Returns the target type of the tree at the given VisitorState's path, or else null.
For example, the target type of an assignment expression is the variable's type, and the target type of a return statement is the enclosing method's type.
Nice question; the type would only give us information about the current expression itself (state), while we want to know what the "expected" type of the current expression is (i.o.w. what the parent expects/wants to receive) and thus what the target type is of this expression.
So in the Flux#flatMap
case, let's say we matched a RxJava2Adapter.fluxToFlowable(flux)
and we ask for the target type, we know that we want to have Publisher
in the end. A Flux
is already satiesfies that, so we can remove the conversion.
@Test | ||
void identification() { |
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.
Frankly, this test doesn't cover nearly all of the cases. I don't feel comfortable approving without all cases being covered in tests, knowing this will be rolled out to all Picnic services and even being open sourced. 👀
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 added extra identifications. I'm not sure whether adding more cases would be that helpful, but if you feel it would be, let me know 😄 .
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 see a reason not to test all cases, especially since the reactive ones are not yet tested and these are the actual more complex ones here.
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.
Forgot to flush this comment.
"com.google.common.collect.ImmutableBiMap", | ||
"com.google.common.collect.ImmutableList", |
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.
ImmutableEnumMap
and ImmutableEnumSet
are package-private and final. So in the source code we can only ever match (public) supertypes of these. I.o.w.: ImmutableEnumMap
and ImmutableEnumSet
are implementation details we don't need to concern ourselves with.
Working on some extra tests, but I don't know when I will finish that 😄, to be continued. |
I wrote extra tests, tests for |
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.
Okay, nothing more from my side.
Thanks a lot for adding the tests. 👍
...ne-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/IdentityConversionCheckTest.java
Outdated
Show resolved
Hide resolved
...ne-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/IdentityConversionCheckTest.java
Show resolved
Hide resolved
Also remove identity conversions if the source type is equal to the type of the current tree.
b45636f
to
9c9825e
Compare
Here we identified that we should look into the TypesWithUndefinedEquality. During one of our conversations we discussed that Wrote some extra tests to validate that this approach works. It appeared that to make this work, we have to add While working on this, I identified another case we were missing. Namely the case where the type of the conversion method is the same as the type we are passing to the (e.g.) |
Updated the code to check for Suggested commit message:
|
Encountered a few instances of |
"sourceType `%s` or targetType `%s` is null.", | ||
sourceType, | ||
targetType); | ||
if (sourceType == null || targetType == null) { |
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 changed the checkState
to an if statement, while we could also add these checks to the one below. However, I feel that this is a nice separation and (arguably) improves readability. Also fine with merging the two 😅 .
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. Did not yet review the tests :)
|
||
private static boolean isSubtypeWithDefinedEquality( | ||
Type sourceType, Type targetType, VisitorState state) { | ||
Type objectType = typeFromString("java.lang.Object").get(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.
We should not evaluate typeFromString("java.lang.Object")
each time. In this case we can reuse com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE
.
return types.isSubtype(sourceType, targetType) | ||
&& !types.isSameType(sourceType, objectType) | ||
&& !types.isSameType(targetType, objectType) |
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 first and third condition imply the second, so we can drop that one. I also suspect that the third check is faster, so would propose to test that one first.
(This also shows that we should run PITest against PRs. Maybe something for OSS support 🙈.)
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.
Good one, added it to the list.
return Refaster.anyOf( | ||
Collections.disjoint(ImmutableSet.copyOf(collection1), collection2), | ||
Collections.disjoint(new HashSet<>(collection1), collection2), | ||
Collections.disjoint(collection1, ImmutableSet.copyOf(collection2)), | ||
Collections.disjoint(collection1, new HashSet<>(collection2))); |
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.
IIUC these changes can now be reverted.
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 two more commits. I think that's it from my side. Very nice!
Tweaked suggested commit message:
Introduce `IdentityConversionCheck` (#27)
And remove any Refaster templates it subsumes.
return Description.NO_MATCH; | ||
} | ||
|
||
if (state.getTypes().isSameType(sourceType, ASTHelpers.getType(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.
I think that in practice ASTHelpers.getType(tree)
won't ever return null
here, but the method does allow it. Since isSameType
would then throw an NPE, perhaps we can make this a bit more robust.
.addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName())) | ||
.build(); | ||
} | ||
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 reduce the indentation by matching the case. Early return Description.NO_MATCH
statements also matches other code in this repo.
return buildDescription(tree) | ||
.setMessage( | ||
"This method invocation appears redundant; remove it or suppress this warning and " | ||
+ "add an comment explaining its purpose") |
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.
+ "add an comment explaining its purpose") | |
+ "add a comment explaining its purpose") |
.setMessage( | ||
"This method invocation appears redundant; remove it or suppress this warning and " | ||
+ "add an comment explaining its purpose") | ||
.addFix(SuggestedFix.replace(tree, state.getSourceForNode(sourceTree))) |
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.
.addFix(SuggestedFix.replace(tree, state.getSourceForNode(sourceTree))) | |
.addFix(SuggestedFix.replace(tree, Util.treeToString(sourceTree, state))) |
" public void foo() {", | ||
" // BUG: Diagnostic contains:", | ||
" Byte b1 = Byte.valueOf((Byte) Byte.MIN_VALUE);", | ||
" Byte b2 = Byte.valueOf(Byte.MIN_VALUE);", |
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.
Arguably we should flag this one as well (IDEA does, too). Sounds like we should more generically handle comparison between boxed and unboxed types. Looks like all we need to change is isSubtype
-> isConvertible
.
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.
When doing this some other not-so-relevant Refaster templates are flagged; will drop those.
return Description.NO_MATCH; | ||
} | ||
|
||
private static boolean isSubtypeWithDefinedEquality( |
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.
"Defined" might be a bit vague. How about "well-defined"?
private static boolean isSubtypeWithDefinedEquality( | |
private static boolean isSubtypeWithWellDefinedEquality( |
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.
Thought it wasn't "worth" it to make the method longer, but I can see how this is clearer :).
" String s2 = String.valueOf(\"1\");", | ||
"", | ||
" // BUG: Diagnostic contains:", | ||
" ImmutableBiMap<Object, Object> i2 = ImmutableBiMap.copyOf(ImmutableBiMap.of());", |
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.
What happened to i1
? 😄
"", | ||
" // BUG: Diagnostic contains:", | ||
" Integer int1 = Integer.valueOf((Integer) 1);", | ||
" Integer int2 = Integer.valueOf(1);", | ||
" // BUG: Diagnostic contains:", | ||
" int int3 = Integer.valueOf((Integer) 1);", | ||
" // BUG: Diagnostic contains:", | ||
" int int4 = Integer.valueOf(1);", |
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.
If we're trying to be complete, then we need a few more sections for other (boxed) primitives :)
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.
Additionally, in some context int -> long conversions also happen implicitly, so let's add a test for that. (Likewise for other primitives, but we need to stop somewhere.)
" Character c1 = Character.valueOf((Character) 'a');", | ||
" Character c2 = Character.valueOf('a');", | ||
" // BUG: Diagnostic contains:", | ||
" char c3 = Character.valueOf((Character)'a');", |
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.
" char c3 = Character.valueOf((Character)'a');", | |
" char c3 = Character.valueOf((Character) 'a');", |
Byte.class.getName(), | ||
Character.class.getName(), | ||
Double.class.getName(), | ||
Float.class.getName(), | ||
Integer.class.getName(), | ||
String.class.getName()) |
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.
Boolean
, Short
and Long
? :) We could use Primitives.allWrapperTypes()
here. (Void.valueOf
doesn't exist, but that's okay.)
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.
That's a nice one! Didn't know that one.
Hmm, in case the converted value is passed to a method, then the suggested fix may cause another overload to be selected. It would be odd if that overload had different semantics, but this could happen. Maybe we should detect this case? (Or just add an |
I like the suggestions 🚀! Pushed a minor tweak in the |
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.
Repeating the suggested commit message:
Introduce `IdentityConversionCheck` (#27)
And remove any Refaster templates it subsumes.
No description provided.