Compare commits

...

2 Commits

Author SHA1 Message Date
Stephan Schroevers
a698c7e6a0 WIP 2022-08-20 18:46:52 +02:00
Cernat Catalin Stefan
b8e22ffef0 Introduce NonEmptyMono check (#200) 2022-08-20 12:47:56 +02:00
3 changed files with 518 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
package tech.picnic.errorprone.bugpatterns;
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.instanceMethod;
import com.google.auto.service.AutoService;
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.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import java.util.function.BiFunction;
import reactor.core.publisher.Mono;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} which flags {@link Mono} operations that are known to be vacuous, given that
* they are invoked on a {@link Mono} that is known not to complete empty.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Avoid vacuous operations on known non-empty `Mono`s",
linkType = NONE,
severity = WARNING,
tags = SIMPLIFICATION)
// XXX: This check does not simplify `someFlux.defaultIfEmpty(T).{defaultIfEmpty(T),hasElements()}`,
// as `someFlux.defaultIfEmpty(T)` yields a `Flux` rather than a `Mono`. Consider adding support for
// these cases.
// XXX: Given more advanced analysis many more expressions could be flagged. Consider
// `Mono.just(someValue)`, `Flux.just(someNonEmptySequence)`,
// `someMono.switchIfEmpty(someProvablyNonEmptyMono)` and many other variants.
// XXX: Consider implementing a similar check for `Publisher`s that are known to complete without
// emitting a value (e.g. `Mono.empty()`, `someFlux.then()`, ...), or known not to complete normally
// (`Mono.never()`, `someFlux.repeat()`, `Mono.error(...)`, ...). The latter category could
// potentially be split out further.
public final class NonEmptyMono extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> MONO_SIZE_CHECK =
instanceMethod()
.onDescendantOf("reactor.core.publisher.Mono")
.namedAnyOf("defaultIfEmpty", "single", "switchIfEmpty");
private static final Matcher<ExpressionTree> NON_EMPTY_MONO =
anyOf(
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.namedAnyOf(
"all",
"any",
"collect",
"collectList",
"collectMap",
"collectMultimap",
"collectSortedList",
"count",
"elementAt",
"hasElement",
"hasElements",
"last",
"reduceWith",
"single"),
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.named("reduce")
.withParameters(Object.class.getName(), BiFunction.class.getName()),
instanceMethod()
.onDescendantOf("reactor.core.publisher.Mono")
.namedAnyOf("defaultIfEmpty", "hasElement", "single"));
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!MONO_SIZE_CHECK.matches(tree, state)) {
return Description.NO_MATCH;
}
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
if (!NON_EMPTY_MONO.matches(receiver, state)) {
return Description.NO_MATCH;
}
return describeMatch(
tree, SuggestedFix.replace(tree, SourceCode.treeToString(receiver, state)));
}
}

View File

@@ -0,0 +1,272 @@
package tech.picnic.errorprone.refastertemplates;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import org.assertj.core.util.Streams;
import org.reactivestreams.Publisher;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/** Refaster templates which replace RxJava expressions with equivalent Reactor assertions. */
// XXX: Document the approach and any limitations; see the `TestNGToAssertJTemplates` documentation.
// XXX: Document that the templates use `Completable` rather than `CompletableSource`, etc.
// XXX: Have separate files for Completable, Maybe, Single, Flowable?
final class RxJavaToReactorTemplates {
private RxJavaToReactorTemplates() {}
// XXX: Handle array and varargs cases.
// XXX: Simplify rule with `Completable.amb(Arrays.asList(sources))`?
static final class CompletableAmbArray {
@BeforeTemplate
Completable before(Completable... sources) {
return Completable.ambArray(sources);
}
@AfterTemplate
Completable after(Completable... sources) {
return Mono.firstWithSignal(
Arrays.stream(sources)
.map(RxJava2Adapter::completableToMono)
.collect(toImmutableList()))
.as(RxJava2Adapter::monoToCompletable);
}
}
static final class CompletableAmb {
@BeforeTemplate
Completable before(Iterable<? extends Completable> sources) {
return Completable.amb(sources);
}
@AfterTemplate
Completable after(Iterable<? extends Completable> sources) {
return Mono.firstWithSignal(
Streams.stream(sources)
.map(RxJava2Adapter::completableToMono)
.collect(toImmutableList()))
.as(RxJava2Adapter::monoToCompletable);
}
}
static final class CompletableComplete {
@BeforeTemplate
Completable before() {
return Completable.complete();
}
@AfterTemplate
Completable after() {
return Mono.empty().as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Handle array and varargs cases.
// XXX: Simplify rule with `Completable.amb(Arrays.asList(sources))`?
static final class CompletableConcatArray {
@BeforeTemplate
Completable before(Completable... sources) {
return Completable.concatArray(sources);
}
@AfterTemplate
Completable after(Completable... sources) {
return Flux.concat(
Arrays.stream(sources)
.map(RxJava2Adapter::completableToMono)
.collect(toImmutableList()))
.then()
.as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Simplify rule with `Completable.concat(Flux.fromIterable(sources))`?
static final class CompletableConcatIterable {
@BeforeTemplate
Completable before(Iterable<? extends Completable> sources) {
return Completable.concat(sources);
}
@AfterTemplate
Completable after(Iterable<? extends Completable> sources) {
return Flux.concat(Flux.fromIterable(sources).map(RxJava2Adapter::completableToMono))
.then()
.as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Simplify rule with `Completable.concat(sources, 2)`?
// XXX: Arguably we should, since the Reactor prefetch is `Queues.XS_BUFFER_SIZE`.
static final class CompletableConcatPublisher {
@BeforeTemplate
Completable before(Publisher<? extends Completable> sources) {
return Completable.concat(sources);
}
@AfterTemplate
Completable after(Publisher<? extends Completable> sources) {
return Flux.concat(Flux.from(sources).map(RxJava2Adapter::completableToMono))
.then()
.as(RxJava2Adapter::monoToCompletable);
}
}
static final class CompletableConcatPublisherPrefetch {
@BeforeTemplate
Completable before(Publisher<? extends Completable> sources, int prefetch) {
return Completable.concat(sources, prefetch);
}
@AfterTemplate
Completable after(Publisher<? extends Completable> sources, int prefetch) {
return Flux.concat(Flux.from(sources).map(RxJava2Adapter::completableToMono), prefetch)
.then()
.as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Migrate `Completable#create(CompletableOnSubscribe)`
// XXX: Migrate `Completable#unsafeCreate(CompletableSource)`
static final class CompletableDefer {
@BeforeTemplate
Completable before(Callable<? extends Completable> completableSupplier) {
return Completable.defer(completableSupplier);
}
@AfterTemplate
Completable after(Callable<? extends Completable> completableSupplier) {
return Mono.defer(
() ->
RxJava2ReactorMigrationUtil.getUnchecked(completableSupplier)
.as(RxJava2Adapter::completableToMono))
.as(RxJava2Adapter::monoToCompletable);
}
}
static final class CompletableErrorDeferred {
@BeforeTemplate
Completable before(Callable<? extends Throwable> errorSupplier) {
return Completable.error(errorSupplier);
}
@AfterTemplate
Completable after(Callable<? extends Throwable> errorSupplier) {
return Mono.error(RxJava2ReactorMigrationUtil.callableAsSupplier(errorSupplier))
.as(RxJava2Adapter::monoToCompletable);
}
}
static final class CompletableError {
@BeforeTemplate
Completable before(Throwable error) {
return Completable.error(error);
}
@AfterTemplate
Completable after(Throwable error) {
return Mono.error(error).as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Migrate `Completable#fromAction(Action)`
static final class CompletableFromCallable<T> {
@BeforeTemplate
Completable before(Callable<T> callable) {
return Completable.fromCallable(callable);
}
@AfterTemplate
Completable after(Callable<T> callable) {
return Mono.fromSupplier(RxJava2ReactorMigrationUtil.callableAsSupplier(callable))
.as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Also handle `Future`s that don't extend `CompletableFuture`.
static final class CompletableFromFuture<T> {
@BeforeTemplate
Completable before(CompletableFuture<T> future) {
return Completable.fromFuture(future);
}
@AfterTemplate
Completable after(CompletableFuture<T> future) {
return Mono.fromFuture(future).as(RxJava2Adapter::monoToCompletable);
}
}
// XXX: Next up: migrate `Completable#fromMaybe(Maybe)`
// XXX: Move to a separate Maven module.
static final class RxJava2ReactorMigrationUtil {
private RxJava2ReactorMigrationUtil() {}
// XXX: Rename.
// XXX: Introduce Refaster rules to drop this wrapper when possible.
static <T> T getUnchecked(Callable<T> callable) {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException("Callable threw checked exception", e);
}
}
// XXX: Rename.
// XXX: Introduce Refaster rules to drop this wrapper when possible.
static <T> Supplier<T> callableAsSupplier(Callable<T> callable) {
return () -> {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException("Callable threw checked exception", e);
}
};
}
static <T, U, R> BiFunction<? super T, ? super U, ? extends R> toJdkBiFunction(
io.reactivex.functions.BiFunction<? super T, ? super U, ? extends R> biFunction) {
return (t, u) -> {
try {
return biFunction.apply(t, u);
} catch (Exception e) {
throw new RuntimeException("BiFunction threw checked exception", e);
}
};
}
}
///////////////////////////////// FLOWABLE - MOVE!
static final class FlowableCombineLatest<T1, T2, R> {
@BeforeTemplate
Flowable<R> before(
Publisher<? extends T1> publisher1,
Publisher<? extends T2> publisher2,
io.reactivex.functions.BiFunction<? super T1, ? super T2, ? extends R> combiner) {
return Flowable.combineLatest(publisher1, publisher2, combiner);
}
@AfterTemplate
Flowable<R> after(
Publisher<? extends T1> publisher1,
Publisher<? extends T2> publisher2,
io.reactivex.functions.BiFunction<? super T1, ? super T2, ? extends R> combiner) {
// XXX: Generic type parameters are specified to appease IDEA; review.
return RxJava2Adapter.fluxToFlowable(
Flux.<T1, T2, R>combineLatest(
publisher1, publisher2, RxJava2ReactorMigrationUtil.toJdkBiFunction(combiner)));
}
}
}

View File

@@ -0,0 +1,155 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class NonEmptyMonoTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(NonEmptyMono.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(NonEmptyMono.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import static com.google.common.collect.ImmutableList.toImmutableList;",
"import static java.util.function.Function.identity;",
"",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableMap;",
"import java.util.ArrayList;",
"import java.util.HashMap;",
"import java.util.List;",
"import reactor.core.publisher.Flux;",
"import reactor.core.publisher.Mono;",
"",
"class A {",
" void m() {",
" Mono.just(1).defaultIfEmpty(2);",
" Mono.just(1).single();",
" Mono.just(1).switchIfEmpty(Mono.just(2));",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).all(x -> true).defaultIfEmpty(true);",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).any(x -> true).single();",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).collect(toImmutableList()).switchIfEmpty(Mono.just(ImmutableList.of()));",
" // BUG: Diagnostic contains:",
" Flux.just(1).collect(ArrayList::new, List::add).defaultIfEmpty(new ArrayList<>());",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectList().single();",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectMap(identity()).switchIfEmpty(Mono.just(ImmutableMap.of()));",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectMap(identity(), identity()).defaultIfEmpty(ImmutableMap.of());",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectMap(identity(), identity(), HashMap::new).single();",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectMultimap(identity()).switchIfEmpty(Mono.just(ImmutableMap.of()));",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectMultimap(identity(), identity()).defaultIfEmpty(ImmutableMap.of());",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectMultimap(identity(), identity(), HashMap::new).single();",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectSortedList().defaultIfEmpty(ImmutableList.of());",
" // BUG: Diagnostic contains:",
" Flux.just(1).collectSortedList((o1, o2) -> 0).single();",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).count().switchIfEmpty(Mono.just(2L));",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).elementAt(0).defaultIfEmpty(1);",
" // BUG: Diagnostic contains:",
" Flux.just(1).elementAt(0, 2).single();",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).hasElement(2).switchIfEmpty(Mono.just(true));",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).hasElements().defaultIfEmpty(true);",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).last().single();",
" // BUG: Diagnostic contains:",
" Flux.just(1).last(2).switchIfEmpty(Mono.just(3));",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).reduceWith(() -> 0, Integer::sum).defaultIfEmpty(2);",
"",
" // BUG: Diagnostic contains:",
" Flux.just(1).single().single();",
" // BUG: Diagnostic contains:",
" Flux.just(1).single(2).switchIfEmpty(Mono.just(3));",
"",
" Flux.just(1).reduce(Integer::sum).defaultIfEmpty(2);",
" // BUG: Diagnostic contains:",
" Flux.just(1).reduce(2, Integer::sum).single();",
"",
" // BUG: Diagnostic contains:",
" Mono.just(1).defaultIfEmpty(1).switchIfEmpty(Mono.just(2));",
" // BUG: Diagnostic contains:",
" Mono.just(1).hasElement().defaultIfEmpty(true);",
" // BUG: Diagnostic contains:",
" Mono.just(1).single().single();",
" }",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import static com.google.common.collect.ImmutableList.toImmutableList;",
"",
"import com.google.common.collect.ImmutableList;",
"import reactor.core.publisher.Flux;",
"import reactor.core.publisher.Mono;",
"",
"class A {",
" void m() {",
" Flux.just(1).collect(toImmutableList()).single();",
" Flux.just(1).collect(toImmutableList()).defaultIfEmpty(ImmutableList.of());",
" Flux.just(1).collect(toImmutableList()).switchIfEmpty(Mono.just(ImmutableList.of()));",
"",
" Mono.just(2).hasElement().single();",
" Mono.just(2).hasElement().defaultIfEmpty(true);",
" Mono.just(2).hasElement().switchIfEmpty(Mono.just(true));",
" }",
"}")
.addOutputLines(
"A.java",
"import static com.google.common.collect.ImmutableList.toImmutableList;",
"",
"import com.google.common.collect.ImmutableList;",
"import reactor.core.publisher.Flux;",
"import reactor.core.publisher.Mono;",
"",
"class A {",
" void m() {",
" Flux.just(1).collect(toImmutableList());",
" Flux.just(1).collect(toImmutableList());",
" Flux.just(1).collect(toImmutableList());",
"",
" Mono.just(2).hasElement();",
" Mono.just(2).hasElement();",
" Mono.just(2).hasElement();",
" }",
"}")
.doTest(TEXT_MATCH);
}
}