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,111 @@
package tech.picnic.errorprone.bugpatterns;

import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
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.common.primitives.Primitives;
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.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. */
// XXX: Consider detecting cases where a flagged expression is passed to a method, and where removal
// of the identify conversion would cause a different method overload to be selected. Depending on
// the target method such a modification may change the code's semantics or performance.
@AutoService(BugChecker.class)
@BugPattern(
name = "IdentityConversion",
summary = "Avoid or clarify identity conversions",
linkType = NONE,
severity = WARNING,
tags = 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(
Primitives.allWrapperTypes().stream()
.map(Class::getName)
.collect(toImmutableSet()))
.named("valueOf"),
staticMethod().onClass(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. 👀

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);
Type resultType = ASTHelpers.getType(tree);
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 || resultType == null || targetType == null) {
return Description.NO_MATCH;
}

if (!state.getTypes().isSameType(sourceType, resultType)
&& !isConvertibleWithWellDefinedEquality(sourceType, targetType.type(), state)) {
return Description.NO_MATCH;
}

return buildDescription(tree)
.setMessage(
"This method invocation appears redundant; remove it or suppress this warning and "
+ "add a comment explaining its purpose")
.addFix(SuggestedFix.replace(tree, Util.treeToString(sourceTree, state)))
.addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName()))
.build();
}

private static boolean isConvertibleWithWellDefinedEquality(
Type sourceType, Type targetType, VisitorState state) {
Types types = state.getTypes();
return !types.isSameType(targetType, OBJECT_TYPE.get(state))
&& types.isConvertible(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 @@ -15,11 +15,7 @@ static final class AbstractByteAssertIsEqualTo {
@BeforeTemplate
AbstractByteAssert<?> before(AbstractByteAssert<?> byteAssert, byte n) {
return Refaster.anyOf(
byteAssert.isCloseTo(n, offset((byte) 0)),
byteAssert.isCloseTo(Byte.valueOf(n), offset((byte) 0)),
byteAssert.isCloseTo(n, withPercentage(0)),
byteAssert.isCloseTo(Byte.valueOf(n), withPercentage(0)),
byteAssert.isEqualTo(Byte.valueOf(n)));
byteAssert.isCloseTo(n, offset((byte) 0)), byteAssert.isCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand All @@ -33,10 +29,7 @@ static final class AbstractByteAssertIsNotEqualTo {
AbstractByteAssert<?> before(AbstractByteAssert<?> byteAssert, byte n) {
return Refaster.anyOf(
byteAssert.isNotCloseTo(n, offset((byte) 0)),
byteAssert.isNotCloseTo(Byte.valueOf(n), offset((byte) 0)),
byteAssert.isNotCloseTo(n, withPercentage(0)),
byteAssert.isNotCloseTo(Byte.valueOf(n), withPercentage(0)),
byteAssert.isNotEqualTo(Byte.valueOf(n)));
byteAssert.isNotCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ static final class AbstractDoubleAssertIsEqualTo {
@BeforeTemplate
AbstractDoubleAssert<?> before(AbstractDoubleAssert<?> doubleAssert, double n) {
return Refaster.anyOf(
doubleAssert.isCloseTo(n, offset(0.0)),
doubleAssert.isCloseTo(Double.valueOf(n), offset(0.0)),
doubleAssert.isCloseTo(n, withPercentage(0.0)),
doubleAssert.isCloseTo(Double.valueOf(n), withPercentage(0.0)),
doubleAssert.isEqualTo(Double.valueOf(n)));
doubleAssert.isCloseTo(n, offset(0.0)), doubleAssert.isCloseTo(n, withPercentage(0.0)));
}

@AfterTemplate
Expand All @@ -54,10 +50,7 @@ static final class AbstractDoubleAssertIsNotEqualTo {
AbstractDoubleAssert<?> before(AbstractDoubleAssert<?> doubleAssert, double n) {
return Refaster.anyOf(
doubleAssert.isNotCloseTo(n, offset(0.0)),
doubleAssert.isNotCloseTo(Double.valueOf(n), offset(0.0)),
doubleAssert.isNotCloseTo(n, withPercentage(0.0)),
doubleAssert.isNotCloseTo(Double.valueOf(n), withPercentage(0.0)),
doubleAssert.isNotEqualTo(Double.valueOf(n)));
doubleAssert.isNotCloseTo(n, withPercentage(0.0)));
}

@AfterTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ static final class AbstractFloatAssertIsEqualTo {
@BeforeTemplate
AbstractFloatAssert<?> before(AbstractFloatAssert<?> floatAssert, float n) {
return Refaster.anyOf(
floatAssert.isCloseTo(n, offset(0F)),
floatAssert.isCloseTo(Float.valueOf(n), offset(0F)),
floatAssert.isCloseTo(n, withPercentage(0)),
floatAssert.isCloseTo(Float.valueOf(n), withPercentage(0)),
floatAssert.isEqualTo(Float.valueOf(n)));
floatAssert.isCloseTo(n, offset(0F)), floatAssert.isCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand All @@ -53,11 +49,7 @@ static final class AbstractFloatAssertIsNotEqualTo {
@BeforeTemplate
AbstractFloatAssert<?> before(AbstractFloatAssert<?> floatAssert, float n) {
return Refaster.anyOf(
floatAssert.isNotCloseTo(n, offset(0F)),
floatAssert.isNotCloseTo(Float.valueOf(n), offset(0F)),
floatAssert.isNotCloseTo(n, withPercentage(0)),
floatAssert.isNotCloseTo(Float.valueOf(n), withPercentage(0)),
floatAssert.isNotEqualTo(Float.valueOf(n)));
floatAssert.isNotCloseTo(n, offset(0F)), floatAssert.isNotCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ static final class AbstractIntegerAssertIsEqualTo {
@BeforeTemplate
AbstractIntegerAssert<?> before(AbstractIntegerAssert<?> intAssert, int n) {
return Refaster.anyOf(
intAssert.isCloseTo(n, offset(0)),
intAssert.isCloseTo(Integer.valueOf(n), offset(0)),
intAssert.isCloseTo(n, withPercentage(0)),
intAssert.isCloseTo(Integer.valueOf(n), withPercentage(0)),
intAssert.isEqualTo(Integer.valueOf(n)));
intAssert.isCloseTo(n, offset(0)), intAssert.isCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand All @@ -32,11 +28,7 @@ static final class AbstractIntegerAssertIsNotEqualTo {
@BeforeTemplate
AbstractIntegerAssert<?> before(AbstractIntegerAssert<?> intAssert, int n) {
return Refaster.anyOf(
intAssert.isNotCloseTo(n, offset(0)),
intAssert.isNotCloseTo(Integer.valueOf(n), offset(0)),
intAssert.isNotCloseTo(n, withPercentage(0)),
intAssert.isNotCloseTo(Integer.valueOf(n), withPercentage(0)),
intAssert.isNotEqualTo(Integer.valueOf(n)));
intAssert.isNotCloseTo(n, offset(0)), intAssert.isNotCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ static final class AbstractLongAssertIsEqualTo {
@BeforeTemplate
AbstractLongAssert<?> before(AbstractLongAssert<?> longAssert, long n) {
return Refaster.anyOf(
longAssert.isCloseTo(n, offset(0L)),
longAssert.isCloseTo(Long.valueOf(n), offset(0L)),
longAssert.isCloseTo(n, withPercentage(0)),
longAssert.isCloseTo(Long.valueOf(n), withPercentage(0)),
longAssert.isEqualTo(Long.valueOf(n)));
longAssert.isCloseTo(n, offset(0L)), longAssert.isCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand All @@ -32,11 +28,7 @@ static final class AbstractLongAssertIsNotEqualTo {
@BeforeTemplate
AbstractLongAssert<?> before(AbstractLongAssert<?> longAssert, long n) {
return Refaster.anyOf(
longAssert.isNotCloseTo(n, offset(0L)),
longAssert.isNotCloseTo(Long.valueOf(n), offset(0L)),
longAssert.isNotCloseTo(n, withPercentage(0)),
longAssert.isNotCloseTo(Long.valueOf(n), withPercentage(0)),
longAssert.isNotEqualTo(Long.valueOf(n)));
longAssert.isNotCloseTo(n, offset(0L)), longAssert.isNotCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ static final class AbstractShortAssertIsEqualTo {
@BeforeTemplate
AbstractShortAssert<?> before(AbstractShortAssert<?> shortAssert, short n) {
return Refaster.anyOf(
shortAssert.isCloseTo(n, offset((short) 0)),
shortAssert.isCloseTo(Short.valueOf(n), offset((short) 0)),
shortAssert.isCloseTo(n, withPercentage(0)),
shortAssert.isCloseTo(Short.valueOf(n), withPercentage(0)),
shortAssert.isEqualTo(Short.valueOf(n)));
shortAssert.isCloseTo(n, offset((short) 0)), shortAssert.isCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand All @@ -33,10 +29,7 @@ static final class AbstractShortAssertIsNotEqualTo {
AbstractShortAssert<?> before(AbstractShortAssert<?> shortAssert, short n) {
return Refaster.anyOf(
shortAssert.isNotCloseTo(n, offset((short) 0)),
shortAssert.isNotCloseTo(Short.valueOf(n), offset((short) 0)),
shortAssert.isNotCloseTo(n, withPercentage(0)),
shortAssert.isNotCloseTo(Short.valueOf(n), withPercentage(0)),
shortAssert.isNotEqualTo(Short.valueOf(n)));
shortAssert.isNotCloseTo(n, withPercentage(0)));
}

@AfterTemplate
Expand Down
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