Skip to content
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

Merged
merged 13 commits into from Apr 8, 2022

Conversation

rickie
Copy link
Member

@rickie rickie commented Jan 5, 2022

No description provided.

@rickie
Copy link
Member Author

rickie commented Jan 5, 2022

This PR replaces: PicnicSupermarket/error-prone#5

@rickie
Copy link
Member Author

rickie commented Jan 6, 2022

The IdentityConversionCheck replaces a number of Refaster templates 😄 . Really like the EPS self-check 😉 .

Copy link
Member

@werli werli left a 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"),
Copy link
Member

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? 🤔

Copy link
Member Author

@rickie rickie Jan 8, 2022

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.

Copy link
Member

@werli werli Jan 10, 2022

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. 👀

Comment on lines +64 to +67
staticMethod()
.onClass("reactor.core.publisher.Flux")
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
Copy link
Member

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.

Copy link
Member Author

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.

Comment on lines +40 to +44
"com.google.common.collect.ImmutableBiMap",
"com.google.common.collect.ImmutableList",
Copy link
Member

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?

Copy link
Member Author

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 😉

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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",
Copy link
Member

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?

Copy link
Member Author

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);
Copy link
Member

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)? 🙇

Copy link
Member Author

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.

Comment on lines +15 to +16
@Test
void identification() {
Copy link
Member

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. 👀

Copy link
Member Author

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 😄 .

Copy link
Member

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.

Copy link
Member

@Stephan202 Stephan202 left a 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.

Comment on lines +40 to +44
"com.google.common.collect.ImmutableBiMap",
"com.google.common.collect.ImmutableList",
Copy link
Member

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.

@rickie
Copy link
Member Author

rickie commented Jan 11, 2022

Working on some extra tests, but I don't know when I will finish that 😄, to be continued.

@rickie
Copy link
Member Author

rickie commented Jan 11, 2022

I wrote extra tests, tests for RxJava2Adapter, Flux and Mono. I think now the tests are sufficient. PTAL 😄.

@werli werli self-requested a review January 12, 2022 11:02
Copy link
Member

@werli werli left a 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. 👍

@rickie rickie force-pushed the rossendrijver/identity_conversion branch from b45636f to 9c9825e Compare February 18, 2022 11:34
@rickie
Copy link
Member Author

rickie commented Feb 18, 2022

Here we identified that we should look into the TypesWithUndefinedEquality. During one of our conversations we discussed that java.lang.Object was not defined in that enum and suggested that there was a chance we have to add it ourselves.

Wrote some extra tests to validate that this approach works. It appeared that to make this work, we have to add java.lang.Object to the enum. Therefore, this PR is opened in the fork: PicnicSupermarket/error-prone#29.
This PR will now fail, because it expects the java.lang.Object to be there. Let's discuss this solution and whether it is the right approach.

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.) copyOf (i.o.w. the sourceType here in the code). In hindsight this is an obvious case but the focus (at least for me) was on comparing the targetType with the sourceType. An extra check (and tests) are added to account for this case.

@rickie
Copy link
Member Author

rickie commented Mar 2, 2022

Updated the code to check for java.lang.Object.

Suggested commit message:

Introduce `IdentityConversionCheck`

Remove templates that are now covered by this check.

@rickie
Copy link
Member Author

rickie commented Mar 18, 2022

Encountered a few instances of yield in payments so the checkState was not good enough 😄.

"sourceType `%s` or targetType `%s` is null.",
sourceType,
targetType);
if (sourceType == null || targetType == null) {
Copy link
Member Author

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 😅 .

@rickie rickie added this to the 0.1.0 milestone Apr 4, 2022
Copy link
Member

@Stephan202 Stephan202 left a 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);
Copy link
Member

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.

Comment on lines 102 to 104
return types.isSubtype(sourceType, targetType)
&& !types.isSameType(sourceType, objectType)
&& !types.isSameType(targetType, objectType)
Copy link
Member

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 🙈.)

Copy link
Member Author

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.

Comment on lines 179 to 181
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)));
Copy link
Member

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.

Copy link
Member

@Stephan202 Stephan202 left a 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))
Copy link
Member

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;
Copy link
Member

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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
+ "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)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.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);",
Copy link
Member

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.

Copy link
Member

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(
Copy link
Member

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"?

Suggested change
private static boolean isSubtypeWithDefinedEquality(
private static boolean isSubtypeWithWellDefinedEquality(

Copy link
Member Author

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());",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to i1? 😄

Comment on lines 52 to 59
"",
" // 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);",
Copy link
Member

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 :)

Copy link
Member

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');",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
" char c3 = Character.valueOf((Character)'a');",
" char c3 = Character.valueOf((Character) 'a');",

Comment on lines 58 to 63
Byte.class.getName(),
Character.class.getName(),
Double.class.getName(),
Float.class.getName(),
Integer.class.getName(),
String.class.getName())
Copy link
Member

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.)

Copy link
Member Author

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.

@Stephan202
Copy link
Member

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 XXX comment and see whether this is ever an issue in practice?)

@rickie
Copy link
Member Author

rickie commented Apr 6, 2022

I like the suggestions 🚀!

Pushed a minor tweak in the @BugPattern annotation.
Besides that, did a suggestions for the XXX. WDYT?

Copy link
Member

@Stephan202 Stephan202 left a 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.

@Stephan202 Stephan202 merged commit d2bbee3 into master Apr 8, 2022
@Stephan202 Stephan202 deleted the rossendrijver/identity_conversion branch April 8, 2022 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants