-
Notifications
You must be signed in to change notification settings - Fork 39
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 FluxFlatMapUsageCheck
#26
Changes from all commits
bb8b8dc
e29539c
5486c77
516d3f9
a30eb0b
ba13b73
5d362c2
f68492e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package tech.picnic.errorprone.bugpatterns; | ||
|
||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; | ||
|
||
import com.google.auto.service.AutoService; | ||
import com.google.common.collect.Iterables; | ||
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.MemberReferenceTreeMatcher; | ||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; | ||
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.sun.source.tree.ExpressionTree; | ||
import com.sun.source.tree.MemberReferenceTree; | ||
import com.sun.source.tree.MethodInvocationTree; | ||
import java.util.function.Function; | ||
import java.util.function.Supplier; | ||
import reactor.core.publisher.Flux; | ||
|
||
/** | ||
* A {@link BugChecker} which flags usages of {@link Flux#flatMap(Function)} and {@link | ||
* Flux#flatMapSequential(Function)}. | ||
* | ||
* <p>{@link Flux#flatMap(Function)} and {@link Flux#flatMapSequential(Function)} eagerly perform up | ||
* to {@link reactor.util.concurrent.Queues#SMALL_BUFFER_SIZE} subscriptions. Additionally, the | ||
* former interleaves values as they are emitted, yielding nondeterministic results. In most cases | ||
* {@link Flux#concatMap(Function)} should be preferred, as it produces consistent results and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we then also not include There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good one, I think it would make sense to just pick one ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So IIUC:
Based on this I guess all we need is a Refaster check to replace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushed a commit 😄 ! |
||
* avoids potentially saturating the thread pool on which subscription happens. If {@code | ||
* concatMap}'s single-subscription semantics are undesirable one should invoke a {@code flatMap} or | ||
* {@code flatMapSequential} overload with an explicit concurrency level. | ||
* | ||
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function, Supplier)} is not flagged | ||
* by this check because there is no clear alternative to point to. | ||
*/ | ||
@AutoService(BugChecker.class) | ||
@BugPattern( | ||
name = "FluxFlatMapUsage", | ||
summary = | ||
"`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; " | ||
+ "please use `Flux#concatMap` or explicitly specify the desired amount of concurrency", | ||
linkType = LinkType.NONE, | ||
severity = SeverityLevel.ERROR, | ||
tags = StandardTags.LIKELY_ERROR) | ||
public final class FluxFlatMapUsageCheck extends BugChecker | ||
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher { | ||
private static final long serialVersionUID = 1L; | ||
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY"; | ||
private static final Matcher<ExpressionTree> FLUX_FLATMAP = | ||
instanceMethod() | ||
.onDescendantOf("reactor.core.publisher.Flux") | ||
.namedAnyOf("flatMap", "flatMapSequential") | ||
.withParameters(Function.class.getName()); | ||
|
||
@Override | ||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { | ||
if (!FLUX_FLATMAP.matches(tree, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
return buildDescription(tree) | ||
.addFix(SuggestedFixes.renameMethodInvocation(tree, "concatMap", state)) | ||
.addFix( | ||
SuggestedFix.builder() | ||
.postfixWith( | ||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME) | ||
.build()) | ||
.build(); | ||
} | ||
|
||
@Override | ||
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { | ||
if (!FLUX_FLATMAP.matches(tree, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
// Method references are expected to occur very infrequently; generating both variants of | ||
// suggested fixes is not worth the trouble. | ||
return describeMatch(tree); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package tech.picnic.errorprone.bugpatterns; | ||
|
||
import static com.google.errorprone.BugCheckerRefactoringTestHelper.newInstance; | ||
|
||
import com.google.errorprone.BugCheckerRefactoringTestHelper; | ||
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers; | ||
import com.google.errorprone.CompilationTestHelper; | ||
import org.junit.jupiter.api.Test; | ||
|
||
final class FluxFlatMapUsageCheckTest { | ||
private final CompilationTestHelper compilationTestHelper = | ||
CompilationTestHelper.newInstance(FluxFlatMapUsageCheck.class, getClass()); | ||
private final BugCheckerRefactoringTestHelper refactoringTestHelper = | ||
newInstance(FluxFlatMapUsageCheck.class, getClass()); | ||
|
||
@Test | ||
void identification() { | ||
compilationTestHelper | ||
.addSourceLines( | ||
"A.java", | ||
"import java.util.function.BiFunction;", | ||
"import java.util.function.Function;", | ||
"import reactor.core.publisher.Mono;", | ||
"import reactor.core.publisher.Flux;", | ||
"", | ||
"class A {", | ||
" void m() {", | ||
" // BUG: Diagnostic contains:", | ||
" Flux.just(1).flatMap(Flux::just);", | ||
" // BUG: Diagnostic contains:", | ||
" Flux.just(1).<String>flatMap(i -> Flux.just(String.valueOf(i)));", | ||
" // BUG: Diagnostic contains:", | ||
" Flux.just(1).flatMapSequential(Flux::just);", | ||
" // BUG: Diagnostic contains:", | ||
" Flux.just(1).<String>flatMapSequential(i -> Flux.just(String.valueOf(i)));", | ||
"", | ||
" Mono.just(1).flatMap(Mono::just);", | ||
" Flux.just(1).concatMap(Flux::just);", | ||
"", | ||
" Flux.just(1).flatMap(Flux::just, 1);", | ||
" Flux.just(1).flatMap(Flux::just, 1, 1);", | ||
" Flux.just(1).flatMap(Flux::just, throwable -> Flux.empty(), Flux::empty);", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This overload has the same implicit reliance on the default concurrency level as the unary variant. However, there is no more elaborate overload we can refer to. So perhaps we should just call out this observation in the main code, without further flagging this method. |
||
"", | ||
" Flux.just(1).flatMapSequential(Flux::just, 1);", | ||
" Flux.just(1).flatMapSequential(Flux::just, 1, 1);", | ||
"", | ||
" // BUG: Diagnostic contains:", | ||
" this.<String, Flux<String>>sink(Flux::flatMap);", | ||
" // BUG: Diagnostic contains:", | ||
" this.<Integer, Flux<Integer>>sink(Flux::<Integer>flatMap);", | ||
"", | ||
" // BUG: Diagnostic contains:", | ||
" this.<String, Flux<String>>sink(Flux::flatMapSequential);", | ||
" // BUG: Diagnostic contains:", | ||
" this.<Integer, Flux<Integer>>sink(Flux::<Integer>flatMapSequential);", | ||
"", | ||
" this.<String, Mono<String>>sink(Mono::flatMap);", | ||
" }", | ||
"", | ||
" private <T, P> void sink(BiFunction<P, Function<T, P>, P> fun) {}", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
void replacementFirstSuggestedFix() { | ||
refactoringTestHelper | ||
.setFixChooser(FixChoosers.FIRST) | ||
.addInputLines( | ||
"in/A.java", | ||
"import reactor.core.publisher.Flux;", | ||
"", | ||
"class A {", | ||
" void m() {", | ||
" Flux.just(1).flatMap(Flux::just);", | ||
" Flux.just(1).flatMapSequential(Flux::just);", | ||
" }", | ||
"}") | ||
.addOutputLines( | ||
"out/A.java", | ||
"import reactor.core.publisher.Flux;", | ||
"", | ||
"class A {", | ||
" void m() {", | ||
" Flux.just(1).concatMap(Flux::just);", | ||
" Flux.just(1).concatMap(Flux::just);", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
void replacementSecondSuggestedFix() { | ||
refactoringTestHelper | ||
.setFixChooser(FixChoosers.SECOND) | ||
.addInputLines( | ||
"in/A.java", | ||
"import reactor.core.publisher.Flux;", | ||
"", | ||
"class A {", | ||
" private static final int MAX_CONCURRENCY = 8;", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just checking, is this constant necessary in the before case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. The suggested fix does not introduce this constant, so strictly speaking it yields non-compilable code, which (In theory we could update the code to suggest a |
||
"", | ||
" void m() {", | ||
" Flux.just(1).flatMap(Flux::just);", | ||
" Flux.just(1).flatMapSequential(Flux::just);", | ||
" }", | ||
"}") | ||
.addOutputLines( | ||
"out/A.java", | ||
"import reactor.core.publisher.Flux;", | ||
"", | ||
"class A {", | ||
" private static final int MAX_CONCURRENCY = 8;", | ||
"", | ||
" void m() {", | ||
" Flux.just(1).flatMap(Flux::just, MAX_CONCURRENCY);", | ||
" Flux.just(1).flatMapSequential(Flux::just, MAX_CONCURRENCY);", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not entirely sure about this first two sentences, are they still correct 😬 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite; will push something :)