mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 08:11:25 +00:00
Compare commits
9 Commits
v0.22.0
...
sschroever
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5f24c966b | ||
|
|
001243f31a | ||
|
|
8909323e37 | ||
|
|
4803b7c051 | ||
|
|
9882a5047d | ||
|
|
1c78a0d0c2 | ||
|
|
789a2be195 | ||
|
|
70aa735a77 | ||
|
|
2c66daa6f3 |
@@ -0,0 +1,247 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
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 static com.google.errorprone.matchers.Matchers.nullLiteral;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.typePredicateMatcher;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.unbound;
|
||||
|
||||
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.predicates.TypePredicate;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.function.Consumer;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Publisher} operations that are known to be vacuous, given
|
||||
* that they are applied to a {@link Mono} or {@link Flux} that does not emit {@code onNext}
|
||||
* signals.
|
||||
*/
|
||||
// XXX: Also match (effectively) final variables that reference provably-empty publishers.
|
||||
// XXX: Also handle `#subscribe` invocations with a non-null value consumer.
|
||||
// XXX: Suggest a fix, or document why we don't (e.g. because this this requires inference of the
|
||||
// type of the `Mono` or `Flux`).
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid vacuous operations on empty `Publisher`s",
|
||||
link = BUG_PATTERNS_BASE_URL + "EmptyPublisher",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class EmptyPublisher extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> VOID = type(Void.class.getCanonicalName());
|
||||
private static final Supplier<Type> MONO =
|
||||
Suppliers.typeFromString("reactor.core.publisher.Mono");
|
||||
private static final Supplier<Type> FLUX =
|
||||
Suppliers.typeFromString("reactor.core.publisher.Flux");
|
||||
private static final TypePredicate CONSUMER =
|
||||
isSubTypeOf(generic(type(Consumer.class.getCanonicalName()), unbound()));
|
||||
// XXX: There are many operators that do not change the number of `onNext` signals, such as
|
||||
// `onErrorComplete()` or `publishOn(Scheduler)`. Cover those cases as well.
|
||||
private static final Matcher<ExpressionTree> EMPTY_FLUX =
|
||||
anyOf(
|
||||
staticMethod().onDescendantOf(FLUX).namedAnyOf("empty", "never"),
|
||||
typePredicateMatcher(isSubTypeOf(generic(FLUX, VOID))));
|
||||
private static final Matcher<ExpressionTree> EMPTY_MONO =
|
||||
anyOf(
|
||||
staticMethod().onDescendantOf(MONO).namedAnyOf("empty", "never"),
|
||||
typePredicateMatcher(isSubTypeOf(generic(MONO, VOID))));
|
||||
|
||||
// XXX: Include `reduce` and `scan` in this list. Only the 1-arg variants are a no-op.
|
||||
// XXX: Include the non-duration `take` variants in this list.
|
||||
// XXX: Not a no-op, and may be dropped if types match:
|
||||
// - all
|
||||
// - any
|
||||
// - count
|
||||
// - reduce with two arguments (can be `startWith(x)`?)
|
||||
// - reduceWith
|
||||
// - scan with two arguments (can be `startWith(x)`?)
|
||||
// - scanWith
|
||||
// - last, single (+ others; see Refaster rules).
|
||||
// XXX: The `collect*` (and possibly some of the `buffer*` and `window*`) operations below do
|
||||
// change what's
|
||||
// emitted. Handle differently.
|
||||
// XXX: Should this list include `groupJoin`, `join`, `windowWhen`, `withLatestFrom`,
|
||||
// `zipWith`...?
|
||||
// XXX: The `onBackpressure*` and some `take` operators do influence how much is requested
|
||||
// upstream, and may thus not be true no-ops; evaluate whether they should really be part of this
|
||||
// list.
|
||||
// XXX: What about `publish`?
|
||||
// XXX: What about `toIterable()` and `toStream()`?
|
||||
private static final Matcher<ExpressionTree> VACUOUS_EMPTY_FLUX_OPERATORS =
|
||||
instanceMethod()
|
||||
.onDescendantOf(FLUX)
|
||||
.namedAnyOf(
|
||||
"buffer",
|
||||
"bufferTimeout",
|
||||
"bufferUntil",
|
||||
"bufferUntilChanged",
|
||||
"bufferWhile",
|
||||
"collect",
|
||||
"collectList",
|
||||
"collectMap",
|
||||
"collectMultimap",
|
||||
"collectSortedList",
|
||||
"concatMap",
|
||||
"concatMapDelayError",
|
||||
"concatMapIterable",
|
||||
"delayElements",
|
||||
"delaySequence",
|
||||
"delayUntil",
|
||||
"distinct",
|
||||
"distinctUntilChanged",
|
||||
"doOnNext",
|
||||
"elapsed",
|
||||
"expand",
|
||||
"expandDeep",
|
||||
"filter",
|
||||
"filterWhen",
|
||||
"flatMap",
|
||||
"flatMapDelayError",
|
||||
"flatMapIterable",
|
||||
"flatMapSequential",
|
||||
"flatMapSequentialDelayError",
|
||||
"groupBy",
|
||||
"handle",
|
||||
"ignoreElements",
|
||||
"index",
|
||||
"limitRate",
|
||||
"map",
|
||||
"mapNotNull",
|
||||
"next",
|
||||
"ofType",
|
||||
"onBackpressureBuffer",
|
||||
"onBackpressureDrop",
|
||||
"onBackpressureError",
|
||||
"onBackpressureLatest",
|
||||
"parallel",
|
||||
"sample",
|
||||
"sampleFirst",
|
||||
"sampleTimeout",
|
||||
"singleOrEmpty",
|
||||
"skip",
|
||||
"skipLast",
|
||||
"skipUntil",
|
||||
"skipWhile",
|
||||
"sort",
|
||||
"switchMap",
|
||||
"take",
|
||||
"takeLast",
|
||||
"takeUntil",
|
||||
"takeWhile",
|
||||
"timed",
|
||||
"timestamp",
|
||||
"window",
|
||||
"windowTimeout",
|
||||
"windowUntil",
|
||||
"windowUntilChanged",
|
||||
"windowWhile",
|
||||
"zipWithIterable");
|
||||
private static final Matcher<ExpressionTree> VACUOUS_EMPTY_MONO_OPERATORS =
|
||||
instanceMethod()
|
||||
.onDescendantOf(MONO)
|
||||
.namedAnyOf(
|
||||
"delayElement",
|
||||
"delayUntil",
|
||||
"doOnNext",
|
||||
"expand",
|
||||
"expandDeep",
|
||||
"filter",
|
||||
"filterWhen",
|
||||
"flatMap",
|
||||
"flatMapIterable",
|
||||
"flatMapMany",
|
||||
"handle",
|
||||
"ignoreElement",
|
||||
"map",
|
||||
"mapNotNull",
|
||||
"ofType",
|
||||
"timestamp",
|
||||
"zipWhen",
|
||||
"zipWith");
|
||||
|
||||
private static final Matcher<ExpressionTree> SUBSCRIBE =
|
||||
instanceMethod()
|
||||
.onDescendantOf(Suppliers.typeFromString("org.reactivestreams.Publisher"))
|
||||
.named("subscribe");
|
||||
|
||||
/** Instantiates a new {@link EmptyPublisher} instance. */
|
||||
public EmptyPublisher() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
|
||||
// XXX: Factor our the matcher result for reuse below.
|
||||
if (receiver == null
|
||||
|| (!EMPTY_FLUX.matches(receiver, state) && !EMPTY_MONO.matches(receiver, state))) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (!tree.getArguments().isEmpty() && SUBSCRIBE.matches(tree, state)) {
|
||||
/*
|
||||
* The first argument to `#subscribe` overloads is either an `onNext` signal consumer or a
|
||||
* `Subscriber`. In the former case, for known-empty publishers, this argument should be
|
||||
* `null`.
|
||||
*/
|
||||
ExpressionTree firstArgument = tree.getArguments().get(0);
|
||||
if (!nullLiteral().matches(firstArgument, state)
|
||||
&& CONSUMER.apply(ASTHelpers.getSymbol(tree).getParameters().get(0).asType(), state)) {
|
||||
return buildDescription(firstArgument)
|
||||
.setMessage("Passing an on next signal `Consumer` on empty `Publisher`s is a no-op")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
if (EMPTY_FLUX.matches(receiver, state) && VACUOUS_EMPTY_FLUX_OPERATORS.matches(tree, state)) {
|
||||
// XXX: Do the same as for `Mono` below, and update the tests.
|
||||
// XXX: Here and below: don't call it a no-op if the return type is changed. Test error
|
||||
// messages!
|
||||
return buildDescription(tree)
|
||||
.setMessage(
|
||||
String.format(
|
||||
"Operator `%s` on an empty `Flux`s is a no-op",
|
||||
ASTHelpers.getSymbol(tree).getSimpleName()))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (EMPTY_MONO.matches(receiver, state) && VACUOUS_EMPTY_MONO_OPERATORS.matches(tree, state)) {
|
||||
Description.Builder description = buildDescription(tree);
|
||||
if (state.getTypes().isSubtype(ASTHelpers.getType(receiver), ASTHelpers.getType(tree))) {
|
||||
description.addFix(SuggestedFix.replace(tree, SourceCode.treeToString(receiver, state)));
|
||||
}
|
||||
|
||||
return description
|
||||
.setMessage(
|
||||
String.format(
|
||||
"Operator `%s` on an empty `Mono`s is a no-op",
|
||||
ASTHelpers.getSymbol(tree).getSimpleName()))
|
||||
.build();
|
||||
}
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -59,6 +60,8 @@ import tech.picnic.errorprone.refaster.matchers.IsIdentityOperation;
|
||||
import tech.picnic.errorprone.refaster.matchers.ThrowsCheckedException;
|
||||
|
||||
/** Refaster rules related to Reactor expressions and statements. */
|
||||
// For `@Matches(IsEmpty) {mono,flux}`, can `#takeUntilOther` and `#skipUntilOther` be replaced with
|
||||
// `#or` or some other merge operator?
|
||||
@OnlineDocumentation
|
||||
final class ReactorRules {
|
||||
private ReactorRules() {}
|
||||
@@ -374,12 +377,228 @@ final class ReactorRules {
|
||||
mono.then(Mono.just(object)));
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
Mono<T> before2(@Matches(IsEmpty.class) Mono<T> mono, T object) {
|
||||
return mono.defaultIfEmpty(object);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<S> after(Mono<T> mono, S object) {
|
||||
return mono.thenReturn(object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Mono#hasElement()} when the receiver is known to be empty;
|
||||
* explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
// XXX: If we introduce an `IsNotEmpty` matcher, we can introduce a similar `MonoThenReturnTrue`
|
||||
// rule.
|
||||
static final class MonoThenReturnFalse<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Boolean> before(@Matches(IsEmpty.class) Mono<T> mono) {
|
||||
return mono.hasElement();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<Boolean> after(Mono<T> mono) {
|
||||
return mono.thenReturn(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid invocations of {@link Flux#elementAt(int, Object)}, {@link Flux#last(Object)} and {@link
|
||||
* Flux#single(Object)} when the receiver is known to be empty; explicitly communicate the element
|
||||
* emitted downstream instead.
|
||||
*/
|
||||
static final class FluxThenMonoJust<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(@Matches(IsEmpty.class) Flux<T> flux, T value, int index) {
|
||||
return Refaster.anyOf(flux.elementAt(index, value), flux.last(value), flux.single(value));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Flux<T> flux, T value) {
|
||||
return flux.then(Mono.just(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid invocations of {@link Flux#defaultIfEmpty(Object)} when the receiver is known to be
|
||||
* empty; explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
static final class FluxThenManyMonoJust<T> {
|
||||
@BeforeTemplate
|
||||
Flux<T> before(@Matches(IsEmpty.class) Flux<T> flux, T value) {
|
||||
return flux.defaultIfEmpty(value);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<T> after(Flux<T> flux, T value) {
|
||||
// XXX: One could argue it'd even be clearer here to do `.then(...).flux()`, to indicate that
|
||||
// this is really a `Mono` in disguise.
|
||||
return flux.thenMany(Mono.just(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#any(Predicate)}, {@code Flux#hasElements} and {@link
|
||||
* Flux#hasElement(Object)} when the receiver is known to be empty; explicitly communicate the
|
||||
* element emitted downstream instead.
|
||||
*/
|
||||
static final class FluxThenMonoJustFalse<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Boolean> before(
|
||||
@Matches(IsEmpty.class) Flux<T> flux, Predicate<? super T> predicate, T element) {
|
||||
return Refaster.anyOf(flux.any(predicate), flux.hasElements(), flux.hasElement(element));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<Boolean> after(Flux<T> flux) {
|
||||
return flux.then(Mono.just(false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#all(Predicate)} when the receiver is known to be
|
||||
* empty; explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
static final class FluxThenMonoJustTrue<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Boolean> before(@Matches(IsEmpty.class) Flux<T> flux, Predicate<? super T> predicate) {
|
||||
return flux.all(predicate);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<Boolean> after(Flux<T> flux) {
|
||||
return flux.then(Mono.just(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#count()} when the receiver is known to be empty;
|
||||
* explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
static final class FluxThenMonoJust0<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Long> before(@Matches(IsEmpty.class) Flux<T> flux) {
|
||||
return flux.count();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<Integer> after(Flux<T> flux) {
|
||||
return flux.then(Mono.just(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#buffer} when the receiver is known to be empty;
|
||||
* explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
// XXX: The replacement expression introduces an immutable list, where the original expression
|
||||
// emits a mutable `ArrayList`. This fact isn't documented, though.
|
||||
// XXX: Likely this rule will be replaced. Similarly handle `collectMap`, `collectMultimap`,
|
||||
// `collectSortedList`
|
||||
static final class FluxThenMonoJustImmutableListOf<T> {
|
||||
@BeforeTemplate
|
||||
Mono<List<T>> before(@Matches(IsEmpty.class) Flux<T> flux) {
|
||||
return flux.collectList();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<List<T>> after(Flux<T> flux) {
|
||||
return flux.then(Mono.just(ImmutableList.of()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#buffer} when the receiver is known to be empty;
|
||||
* explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
// XXX: The replacement expression introduces an immutable list, where the original expressions
|
||||
// emit a mutable `ArrayList`. This fact isn't documented, though.
|
||||
static final class FluxThenMonoJustImmutableListOfFlux<T> {
|
||||
@BeforeTemplate
|
||||
Flux<List<T>> before(@Matches(IsEmpty.class) Flux<T> flux, int maxSize, int skip) {
|
||||
return Refaster.anyOf(flux.buffer(), flux.buffer(maxSize), flux.buffer(maxSize, skip));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<List<T>> after(Flux<T> flux) {
|
||||
return flux.then(Mono.<List<T>>just(ImmutableList.of())).flux();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#elementAt(int)} when the receiver is known to be
|
||||
* empty; explicitly communicate that an {@link IndexOutOfBoundsException} will be emitted
|
||||
* instead.
|
||||
*/
|
||||
static final class FluxThenMonoErrorNewIndexOutOfBoundsException<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(@Matches(IsEmpty.class) Flux<T> flux, int index) {
|
||||
return flux.elementAt(index);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Flux<T> flux, int index) {
|
||||
return flux.then(Mono.error(() -> new IndexOutOfBoundsException(index)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#last()} and {@link Flux#single()} when the receiver is
|
||||
* known to be empty; explicitly communicate that an {@link NoSuchElementException} will be
|
||||
* emitted instead.
|
||||
*/
|
||||
static final class FluxThenMonoErrorNoSuchElementExceptionNew<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(@Matches(IsEmpty.class) Flux<T> flux) {
|
||||
return Refaster.anyOf(flux.last(), flux.single());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Flux<T> flux) {
|
||||
return flux.then(Mono.error(NoSuchElementException::new));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Flux#buffer} when the receiver is known to be empty;
|
||||
* explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
static final class FluxThenMonoFromSupplierFlux<T, C extends Collection<? super T>> {
|
||||
@BeforeTemplate
|
||||
Flux<C> before(
|
||||
@Matches(IsEmpty.class) Flux<T> flux, Supplier<C> bufferSupplier, int maxSize, int skip) {
|
||||
return Refaster.anyOf(
|
||||
flux.buffer(maxSize, bufferSupplier), flux.buffer(maxSize, skip, bufferSupplier));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<C> after(Flux<T> flux, Supplier<C> bufferSupplier) {
|
||||
return flux.then(Mono.fromSupplier(bufferSupplier)).flux();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid vacuous invocations of {@link Mono#singleOptional()} when the receiver is known to be
|
||||
* empty; explicitly communicate the element emitted downstream instead.
|
||||
*/
|
||||
// XXX: If we introduce an `IsNotEmpty` matcher, we can introduce a similar
|
||||
// `MonoThenReturnOptionalOf` rule.
|
||||
static final class MonoThenReturnOptionalEmpty<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Optional<T>> before(@Matches(IsEmpty.class) Mono<T> mono) {
|
||||
return mono.singleOptional();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<Optional<T>> after(Mono<T> mono) {
|
||||
return mono.thenReturn(Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Flux#take(long)} over {@link Flux#take(long, boolean)} where relevant.
|
||||
*
|
||||
@@ -510,7 +729,7 @@ final class ReactorRules {
|
||||
|
||||
@BeforeTemplate
|
||||
Mono<@Nullable Void> before2(Mono<@Nullable Void> mono) {
|
||||
return Refaster.anyOf(mono.ignoreElement(), mono.then());
|
||||
return mono.then();
|
||||
}
|
||||
|
||||
// XXX: Replace this rule with an extension of the `IdentityConversion` rule, supporting
|
||||
@@ -949,6 +1168,7 @@ final class ReactorRules {
|
||||
}
|
||||
|
||||
/** Avoid vacuous invocations of {@link Flux#ignoreElements()}. */
|
||||
// XXX: Check whether there's a nice non-Void alternative to `emptyFlux#next()`.
|
||||
static final class FluxThen<T> {
|
||||
@BeforeTemplate
|
||||
Mono<@Nullable Void> before(Flux<T> flux) {
|
||||
@@ -1029,6 +1249,11 @@ final class ReactorRules {
|
||||
return flux.ignoreElements().thenMany(publisher);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
Flux<T> before2(@Matches(IsEmpty.class) Flux<T> flux, Publisher<? extends T> publisher) {
|
||||
return Refaster.anyOf(flux.concatWith(publisher), flux.switchIfEmpty(publisher));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<S> after(Flux<T> flux, Publisher<S> publisher) {
|
||||
return flux.thenMany(publisher);
|
||||
@@ -1043,7 +1268,12 @@ final class ReactorRules {
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
Mono<@Nullable Void> before2(Mono<T> mono1, Mono<@Nullable Void> mono2) {
|
||||
Mono<T> before2(@Matches(IsEmpty.class) Mono<T> mono1, Mono<? extends T> mono2) {
|
||||
return mono1.switchIfEmpty(mono2);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
Mono<@Nullable Void> before3(Mono<T> mono1, Mono<@Nullable Void> mono2) {
|
||||
return mono1.thenEmpty(mono2);
|
||||
}
|
||||
|
||||
@@ -1092,26 +1322,40 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Mono#cast(Class)} over {@link Mono#map(Function)} with a cast. */
|
||||
/** Prefer {@link Mono#cast(Class)} over more contrived alternatives. */
|
||||
static final class MonoCast<T, S> {
|
||||
@BeforeTemplate
|
||||
Mono<S> before(Mono<T> mono) {
|
||||
return mono.map(Refaster.<S>clazz()::cast);
|
||||
}
|
||||
|
||||
// XXX: In cases where this template matches, often the `cast` operation can be avoided by
|
||||
// passing an explicit type argument to the preceding reactive operator.
|
||||
@BeforeTemplate
|
||||
Mono<S> before2(@Matches(IsEmpty.class) Mono<T> mono) {
|
||||
return mono.ofType(Refaster.<S>clazz());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<S> after(Mono<T> mono) {
|
||||
return mono.cast(Refaster.<S>clazz());
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Flux#cast(Class)} over {@link Flux#map(Function)} with a cast. */
|
||||
/** Prefer {@link Flux#cast(Class)} over more contrived alternatives. */
|
||||
static final class FluxCast<T, S> {
|
||||
@BeforeTemplate
|
||||
Flux<S> before(Flux<T> flux) {
|
||||
return flux.map(Refaster.<S>clazz()::cast);
|
||||
}
|
||||
|
||||
// XXX: In cases where this template matches, often the `cast` operation can be avoided by
|
||||
// passing an explicit type argument to the preceding reactive operator.
|
||||
@BeforeTemplate
|
||||
Flux<S> before2(@Matches(IsEmpty.class) Flux<T> flux) {
|
||||
return flux.ofType(Refaster.<S>clazz());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<S> after(Flux<T> flux) {
|
||||
return flux.cast(Refaster.<S>clazz());
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper;
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
|
||||
import com.google.errorprone.CompilationTestHelper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class EmptyPublisherTest {
|
||||
// XXX: Reorder test cases.
|
||||
// XXX: Update numbering.
|
||||
@Test
|
||||
void identification() {
|
||||
CompilationTestHelper.newInstance(EmptyPublisher.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableSet;",
|
||||
"import org.reactivestreams.Subscriber;",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"import reactor.core.publisher.Mono;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Mono.just(0).subscribe(i -> {});",
|
||||
" Mono.just(1).doOnNext(i -> {});",
|
||||
" Mono.just(2).filter(i -> true);",
|
||||
" Mono.just(3).flatMap(Mono::just);",
|
||||
" Mono.just(4).flatMapMany(Flux::just);",
|
||||
" Mono.just(5).flatMapIterable(ImmutableSet::of);",
|
||||
" Mono.just(6).handle((i, j) -> {});",
|
||||
" Mono.just(7).map(i -> i);",
|
||||
"",
|
||||
" // BUG: Diagnostic contains: doOnNext",
|
||||
" Mono.empty().doOnNext(i -> {});",
|
||||
" // BUG: Diagnostic contains: filter",
|
||||
" Mono.empty().filter(i -> true);",
|
||||
" // BUG: Diagnostic contains: flatMap",
|
||||
" Mono.empty().flatMap(Mono::just);",
|
||||
" // BUG: Diagnostic contains: flatMapMany",
|
||||
" Mono.empty().flatMapMany(Flux::just);",
|
||||
" // BUG: Diagnostic contains: flatMapIterable",
|
||||
" Mono.empty().flatMapIterable(ImmutableSet::of);",
|
||||
" // BUG: Diagnostic contains: handle",
|
||||
" Mono.empty().handle((i, j) -> {});",
|
||||
" // BUG: Diagnostic contains: map",
|
||||
" Mono.empty().map(i -> i);",
|
||||
"",
|
||||
" // BUG: Diagnostic contains: doOnNext",
|
||||
" Flux.empty().doOnNext(i -> {});",
|
||||
" // BUG: Diagnostic contains: filter",
|
||||
" Flux.empty().filter(i -> true);",
|
||||
" // BUG: Diagnostic contains: concatMap",
|
||||
" Flux.empty().concatMap(Mono::just);",
|
||||
" // BUG: Diagnostic contains: flatMap",
|
||||
" Flux.empty().flatMap(Mono::just);",
|
||||
" // BUG: Diagnostic contains: flatMapSequential",
|
||||
" Flux.empty().flatMapSequential(Flux::just);",
|
||||
" // BUG: Diagnostic contains: flatMapIterable",
|
||||
" Flux.empty().flatMapIterable(ImmutableSet::of);",
|
||||
" // BUG: Diagnostic contains: handle",
|
||||
" Flux.empty().handle((i, j) -> {});",
|
||||
" // BUG: Diagnostic contains: map",
|
||||
" Flux.empty().map(i -> i);",
|
||||
"",
|
||||
" // BUG: Diagnostic contains: doOnNext",
|
||||
" Mono.just(8).then().doOnNext(i -> {});",
|
||||
" // BUG: Diagnostic contains: filter",
|
||||
" Mono.just(9).then().filter(i -> true);",
|
||||
" // BUG: Diagnostic contains: flatMap",
|
||||
" Mono.just(10).then().flatMap(Mono::just);",
|
||||
" // BUG: Diagnostic contains: flatMapMany",
|
||||
" Mono.just(11).then().flatMapMany(Flux::just);",
|
||||
" // BUG: Diagnostic contains: flatMapIterable",
|
||||
" Mono.just(12).then().flatMapIterable(ImmutableSet::of);",
|
||||
" // BUG: Diagnostic contains: handle",
|
||||
" Mono.just(13).then().handle((i, j) -> {});",
|
||||
" // BUG: Diagnostic contains: map",
|
||||
" Mono.just(14).then().map(i -> i);",
|
||||
"",
|
||||
" Mono.just(15).then().subscribe();",
|
||||
" Mono.just(16).then().subscribe((Subscriber<Object>) null);",
|
||||
" Mono.just(17).then().subscribe(null, t -> {});",
|
||||
"",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Mono.just(17).then().subscribe(i -> {});",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Mono.just(18).then().subscribe(i -> {}, t -> {});",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacement() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(EmptyPublisher.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Mono;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Mono.empty().map(i -> 1);",
|
||||
" Mono.empty().doOnNext(i -> {});",
|
||||
" Mono.empty().doOnNext(i -> {}).onErrorComplete();",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Mono;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Mono.empty().map(i -> 1);",
|
||||
" Mono.empty();",
|
||||
" Mono.empty().onErrorComplete();",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,16 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return ImmutableSet.of(
|
||||
Mono.just(1).ignoreElement().thenReturn("foo"),
|
||||
Mono.just(2).then().thenReturn("bar"),
|
||||
Mono.just(3).then(Mono.just("baz")));
|
||||
Mono.just(3).then(Mono.just("baz")),
|
||||
Mono.empty().thenReturn("qux"));
|
||||
}
|
||||
|
||||
Mono<Boolean> testMonoThenReturnFalse() {
|
||||
return Mono.empty().hasElement();
|
||||
}
|
||||
|
||||
Mono<Optional<Object>> testMonoThenReturnOptionalEmpty() {
|
||||
return Mono.empty().singleOptional();
|
||||
}
|
||||
|
||||
Flux<Integer> testFluxTake() {
|
||||
@@ -196,7 +205,6 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Mono.just(1).switchIfEmpty(Mono.empty()),
|
||||
Mono.just(2).flux().next(),
|
||||
Mono.just(3).flux().singleOrEmpty(),
|
||||
Mono.<Void>empty().ignoreElement(),
|
||||
Mono.<Void>empty().then(),
|
||||
Mono.<ImmutableList<String>>empty().map(ImmutableList::copyOf));
|
||||
}
|
||||
@@ -368,14 +376,18 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return Mono.just("foo").thenMany(Mono.just("bar"));
|
||||
}
|
||||
|
||||
Flux<String> testFluxThenMany() {
|
||||
return Flux.just("foo").ignoreElements().thenMany(Flux.just("bar"));
|
||||
ImmutableSet<Flux<String>> testFluxThenMany() {
|
||||
return ImmutableSet.of(
|
||||
Flux.just("foo").ignoreElements().thenMany(Flux.just("bar")),
|
||||
Flux.<String>empty().concatWith(Mono.just("baz")),
|
||||
Flux.<String>empty().switchIfEmpty(Flux.just("qux", "quux")));
|
||||
}
|
||||
|
||||
ImmutableSet<Mono<?>> testMonoThenMono() {
|
||||
return ImmutableSet.of(
|
||||
Mono.just("foo").ignoreElement().then(Mono.just("bar")),
|
||||
Mono.just("baz").flux().then(Mono.just("qux")),
|
||||
Mono.empty().switchIfEmpty(Mono.never()),
|
||||
Mono.just("quux").thenEmpty(Mono.<Void>empty()));
|
||||
}
|
||||
|
||||
@@ -392,12 +404,12 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Mono.just("baz").transform(Mono::singleOptional));
|
||||
}
|
||||
|
||||
Mono<Number> testMonoCast() {
|
||||
return Mono.just(1).map(Number.class::cast);
|
||||
ImmutableSet<Mono<Number>> testMonoCast() {
|
||||
return ImmutableSet.of(Mono.just(1).map(Number.class::cast), Mono.empty().ofType(Number.class));
|
||||
}
|
||||
|
||||
Flux<Number> testFluxCast() {
|
||||
return Flux.just(1).map(Number.class::cast);
|
||||
ImmutableSet<Flux<Number>> testFluxCast() {
|
||||
return ImmutableSet.of(Flux.just(1).map(Number.class::cast), Flux.empty().ofType(Number.class));
|
||||
}
|
||||
|
||||
Mono<Number> testMonoOfType() {
|
||||
|
||||
@@ -148,7 +148,16 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return ImmutableSet.of(
|
||||
Mono.just(1).thenReturn("foo"),
|
||||
Mono.just(2).thenReturn("bar"),
|
||||
Mono.just(3).thenReturn("baz"));
|
||||
Mono.just(3).thenReturn("baz"),
|
||||
Mono.empty().thenReturn("qux"));
|
||||
}
|
||||
|
||||
Mono<Boolean> testMonoThenReturnFalse() {
|
||||
return Mono.empty().thenReturn(false);
|
||||
}
|
||||
|
||||
Mono<Optional<Object>> testMonoThenReturnOptionalEmpty() {
|
||||
return Mono.empty().thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
Flux<Integer> testFluxTake() {
|
||||
@@ -201,7 +210,6 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Mono.just(2),
|
||||
Mono.just(3),
|
||||
Mono.<Void>empty(),
|
||||
Mono.<Void>empty(),
|
||||
Mono.<ImmutableList<String>>empty());
|
||||
}
|
||||
|
||||
@@ -364,14 +372,18 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return Mono.just("foo").then(Mono.just("bar")).flux();
|
||||
}
|
||||
|
||||
Flux<String> testFluxThenMany() {
|
||||
return Flux.just("foo").thenMany(Flux.just("bar"));
|
||||
ImmutableSet<Flux<String>> testFluxThenMany() {
|
||||
return ImmutableSet.of(
|
||||
Flux.just("foo").thenMany(Flux.just("bar")),
|
||||
Flux.<String>empty().thenMany(Mono.just("baz")),
|
||||
Flux.<String>empty().thenMany(Flux.just("qux", "quux")));
|
||||
}
|
||||
|
||||
ImmutableSet<Mono<?>> testMonoThenMono() {
|
||||
return ImmutableSet.of(
|
||||
Mono.just("foo").then(Mono.just("bar")),
|
||||
Mono.just("baz").then(Mono.just("qux")),
|
||||
Mono.empty().then(Mono.never()),
|
||||
Mono.just("quux").then(Mono.<Void>empty()));
|
||||
}
|
||||
|
||||
@@ -387,12 +399,12 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Mono.just("baz").singleOptional());
|
||||
}
|
||||
|
||||
Mono<Number> testMonoCast() {
|
||||
return Mono.just(1).cast(Number.class);
|
||||
ImmutableSet<Mono<Number>> testMonoCast() {
|
||||
return ImmutableSet.of(Mono.just(1).cast(Number.class), Mono.empty().cast(Number.class));
|
||||
}
|
||||
|
||||
Flux<Number> testFluxCast() {
|
||||
return Flux.just(1).cast(Number.class);
|
||||
ImmutableSet<Flux<Number>> testFluxCast() {
|
||||
return ImmutableSet.of(Flux.just(1).cast(Number.class), Flux.empty().cast(Number.class));
|
||||
}
|
||||
|
||||
Mono<Number> testMonoOfType() {
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<url>https://error-prone.picnic.tech</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>error-prone-utils</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
|
||||
@@ -10,18 +10,24 @@ import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static com.google.errorprone.matchers.Matchers.typePredicateMatcher;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.NewClassTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -57,6 +63,7 @@ import java.util.stream.Stream;
|
||||
public final class IsEmpty implements Matcher<ExpressionTree> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Integer ZERO = 0;
|
||||
private static final Supplier<Type> VOID = type(Void.class.getCanonicalName());
|
||||
private static final Pattern EMPTY_INSTANCE_FACTORY_METHOD_PATTERN = Pattern.compile("empty.*");
|
||||
private static final Matcher<Tree> EMPTY_COLLECTION_CONSTRUCTOR_ARGUMENT =
|
||||
anyOf(isPrimitiveType(), isSubtypeOf(Comparator.class));
|
||||
@@ -73,7 +80,7 @@ public final class IsEmpty implements Matcher<ExpressionTree> {
|
||||
isSameType(TreeMap.class),
|
||||
isSameType(TreeSet.class),
|
||||
isSameType(Vector.class));
|
||||
private static final Matcher<ExpressionTree> EMPTY_INSTANCE_FACTORY =
|
||||
private static final Matcher<ExpressionTree> EMPTY_INSTANCE =
|
||||
anyOf(
|
||||
staticField(Collections.class.getCanonicalName(), "EMPTY_LIST"),
|
||||
staticField(Collections.class.getCanonicalName(), "EMPTY_MAP"),
|
||||
@@ -103,9 +110,10 @@ public final class IsEmpty implements Matcher<ExpressionTree> {
|
||||
"reactor.core.publisher.Mono",
|
||||
"reactor.util.context.Context")
|
||||
.named("empty"),
|
||||
staticMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.named("just")))));
|
||||
staticMethod().onDescendantOf("reactor.core.publisher.Flux").named("just")))),
|
||||
// XXX: We could also match `Iterable<Void>` and `Stream<Void>` subtypes, but those are
|
||||
// rarely or never seen in the wild.
|
||||
typePredicateMatcher(isSubTypeOf(generic(type("org.reactivestreams.Publisher"), VOID))));
|
||||
|
||||
/** Instantiates a new {@link IsEmpty} instance. */
|
||||
public IsEmpty() {}
|
||||
@@ -113,7 +121,7 @@ public final class IsEmpty implements Matcher<ExpressionTree> {
|
||||
@Override
|
||||
public boolean matches(ExpressionTree tree, VisitorState state) {
|
||||
return isEmptyArrayCreation(tree)
|
||||
|| EMPTY_INSTANCE_FACTORY.matches(tree, state)
|
||||
|| EMPTY_INSTANCE.matches(tree, state)
|
||||
|| isEmptyCollectionConstructor(tree, state);
|
||||
}
|
||||
|
||||
|
||||
@@ -429,6 +429,11 @@ final class IsEmptyTest {
|
||||
" // BUG: Diagnostic contains:",
|
||||
" return Flux.just();",
|
||||
" }",
|
||||
"",
|
||||
" Mono<Void> positive57() {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" return Mono.just(1).then();",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user