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
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package tech.picnic.errorprone.bugpatterns;

import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.LinkType;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.BugPattern.StandardTags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.TypesWithUndefinedEquality;
import com.google.errorprone.fixes.SuggestedFix;
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.google.errorprone.util.ASTHelpers.TargetType;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import java.util.Arrays;
import java.util.List;

/** A {@link BugChecker} that flags redundant identity conversions. */
@AutoService(BugChecker.class)
@BugPattern(
name = "IdentityConversion",
summary = "Avoid or clarify identity conversions",
linkType = LinkType.NONE,
severity = SeverityLevel.WARNING,
tags = StandardTags.SIMPLIFICATION)
public final class IdentityConversionCheck extends BugChecker
implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
anyOf(
staticMethod()
.onClassAny(
"com.google.common.collect.ImmutableBiMap",
"com.google.common.collect.ImmutableList",
Comment on lines +49 to +50
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.ImmutableListMultimap",
"com.google.common.collect.ImmutableMap",
"com.google.common.collect.ImmutableMultimap",
"com.google.common.collect.ImmutableMultiset",
"com.google.common.collect.ImmutableRangeMap",
"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 😄 .

"com.google.common.collect.ImmutableSet",
"com.google.common.collect.ImmutableSetMultimap",
"com.google.common.collect.ImmutableTable")
.named("copyOf"),
staticMethod()
.onClassAny(
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.

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

staticMethod()
.onClass("reactor.core.publisher.Flux")
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
Comment on lines +69 to +71
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.

staticMethod().onClass("reactor.core.publisher.Mono").namedAnyOf("from", "fromDirect"));

@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments = tree.getArguments();
if (arguments.size() != 1 || !IS_CONVERSION_METHOD.matches(tree, state)) {
return Description.NO_MATCH;
}

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.

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

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.

|| isSubtypeWithDefinedEquality(sourceType, targetType.type(), state)) {
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")

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

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

}

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

Type sourceType, Type targetType, VisitorState state) {
Types types = state.getTypes();
return !types.isSameType(targetType, OBJECT_TYPE.get(state))
&& types.isSubtype(sourceType, targetType)
&& Arrays.stream(TypesWithUndefinedEquality.values())
.noneMatch(b -> b.matchesType(sourceType, state) || b.matchesType(targetType, state));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -272,17 +272,4 @@ ImmutableListMultimap<K, V2> after(
return ImmutableListMultimap.copyOf(Multimaps.transformValues(multimap, transformation));
}
}

/** Don't unnecessarily copy an {@link ImmutableListMultimap}. */
static final class ImmutableListMultimapCopyOfImmutableListMultimap<K, V> {
@BeforeTemplate
ImmutableListMultimap<K, V> before(ImmutableListMultimap<K, V> multimap) {
return ImmutableListMultimap.copyOf(multimap);
}

@AfterTemplate
ImmutableListMultimap<K, V> after(ImmutableListMultimap<K, V> multimap) {
return multimap;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,19 +242,6 @@ ImmutableMap<K, V2> after(Map<K, V1> map) {
}
}

/** Don't unnecessarily copy an {@link ImmutableMap}. */
static final class ImmutableMapCopyOfImmutableMap<K, V> {
@BeforeTemplate
ImmutableMap<K, V> before(ImmutableMap<K, V> map) {
return ImmutableMap.copyOf(map);
}

@AfterTemplate
ImmutableMap<K, V> after(ImmutableMap<K, V> map) {
return map;
}
}

// XXX: Add a template for this:
// Maps.transformValues(streamOfEntries.collect(groupBy(fun)), ImmutableMap::copyOf)
// ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,4 @@ ImmutableMultiset<T> after(Stream<T> stream) {
return stream.collect(toImmutableMultiset());
}
}

/** Don't unnecessarily copy an {@link ImmutableMultiset}. */
static final class ImmutableMultisetCopyOfImmutableMultiset<T> {
@BeforeTemplate
ImmutableMultiset<T> before(ImmutableMultiset<T> multiset) {
return ImmutableMultiset.copyOf(multiset);
}

@AfterTemplate
ImmutableMultiset<T> after(ImmutableMultiset<T> multiset) {
return multiset;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,4 @@ ImmutableSetMultimap<K, V2> after(
return ImmutableSetMultimap.copyOf(Multimaps.transformValues(multimap, transformation));
}
}

/** Don't unnecessarily copy an {@link ImmutableSetMultimap}. */
static final class ImmutableSetMultimapCopyOfImmutableSetMultimap<K, V> {
@BeforeTemplate
ImmutableSetMultimap<K, V> before(ImmutableSetMultimap<K, V> multimap) {
return ImmutableSetMultimap.copyOf(multimap);
}

@AfterTemplate
ImmutableSetMultimap<K, V> after(ImmutableSetMultimap<K, V> multimap) {
return multimap;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,6 @@ ImmutableSet<T> after(Stream<T> stream) {
}
}

/** Don't unnecessarily copy an {@link ImmutableSet}. */
static final class ImmutableSetCopyOfImmutableSet<T> {
@BeforeTemplate
ImmutableSet<T> before(ImmutableSet<T> set) {
return ImmutableSet.copyOf(set);
}

@AfterTemplate
ImmutableSet<T> after(ImmutableSet<T> set) {
return set;
}
}

/** Prefer {@link SetView#immutableCopy()} over the more verbose alternative. */
static final class ImmutableSetCopyOfSetView<T> {
@BeforeTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,19 +203,6 @@ Flux<T> after(Mono<T> mono) {
}
}

/** Don't unnecessarily invoke {@link Flux#concat(Publisher)}. */
static final class FluxIdentity<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux) {
return Flux.concat(flux);
}

@AfterTemplate
Flux<T> after(Flux<T> flux) {
return flux;
}
}

/**
* Prefer a collection using {@link MoreCollectors#toOptional()} over more contrived alternatives.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,9 @@ static final class FlowableToFlux<T> {
@BeforeTemplate
Publisher<T> before(Flowable<T> flowable) {
return Refaster.anyOf(
Flux.from(flowable),
flowable.compose(Flux::from),
flowable.to(Flux::from),
flowable.as(Flux::from),
RxJava2Adapter.flowableToFlux(flowable),
flowable.compose(RxJava2Adapter::flowableToFlux),
flowable.to(RxJava2Adapter::flowableToFlux));
}
Expand All @@ -67,7 +65,6 @@ Publisher<T> before(Flux<T> flux) {
Flowable.fromPublisher(flux),
flux.transform(Flowable::fromPublisher),
flux.as(Flowable::fromPublisher),
RxJava2Adapter.fluxToFlowable(flux),
flux.transform(RxJava2Adapter::fluxToFlowable));
}

Expand Down Expand Up @@ -140,7 +137,6 @@ Publisher<T> before(Mono<T> mono) {
Flowable.fromPublisher(mono),
mono.transform(Flowable::fromPublisher),
mono.as(Flowable::fromPublisher),
RxJava2Adapter.monoToFlowable(mono),
mono.transform(RxJava2Adapter::monoToFlowable));
}

Expand Down