Compare commits

...

12 Commits

Author SHA1 Message Date
Rick Ossendrijver
c44b028f82 Add missing AssertJ expressions related to Iterables and #size() 2022-04-06 14:41:00 +02:00
Rick Ossendrijver
793a70c29b Configure static import policy for AssertThatSetContainsExactlyOneElement Refaster template (#32) 2022-01-24 10:02:34 +01:00
Rick Ossendrijver
63b4fae185 Introduce FluxFlatMapUsageCheck (#26)
And two (semi-)related Refaster templates.
2022-01-15 12:39:34 +01:00
Stephan Schroevers
73f8f056b4 Introduce ScheduledTransactionTraceCheck (#31) 2022-01-10 10:21:20 +01:00
Rick Ossendrijver
c1638066cd Define assorted StaticImportCheck exemptions
While there, require that most `org.springframework.http.MediaType` members are
statically imported.
2022-01-03 10:20:46 +01:00
Rick Ossendrijver
08e99fb54e Use matching lambda parameter names in Refaster before- and after-templates (#24) 2022-01-01 14:04:37 +01:00
Rick Ossendrijver
00012c6aa8 Extend StaticImportCheck test coverage (#21) 2021-11-14 14:39:15 +01:00
Rick Ossendrijver
193589d193 Require static import of SpringBootTest.WebEnvironment constants (#19) 2021-11-14 14:26:17 +01:00
Stephan Schroevers
1a1588d413 Upgrade Error Prone 2.9.0 -> 2.10.0 2021-11-14 13:50:26 +01:00
Stephan Schroevers
6656bd8186 Assorted upgrades 2021-11-14 13:50:26 +01:00
Rick Ossendrijver
26a9b46de7 Add Refaster rules for StepVerifier creation (#18) 2021-11-14 13:48:39 +01:00
Phil Werli
17e74f778b Add Refaster rules for redundant Flux#concat invocations (#20) 2021-11-13 18:04:49 +01:00
16 changed files with 715 additions and 31 deletions

View File

@@ -56,6 +56,11 @@
<artifactId>jackson-annotations</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
@@ -76,6 +81,11 @@
<artifactId>guava</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
@@ -186,6 +196,11 @@
<artifactId>spring-webflux</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>

View File

@@ -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
* 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);
}
}

View File

@@ -0,0 +1,88 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.isType;
import com.google.auto.common.AnnotationMirrors;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
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.MethodTreeMatcher;
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.matchers.MultiMatcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
/**
* A {@link BugChecker} which flags methods with Spring's {@code @Scheduled} annotation that lack
* New Relic Agent's {@code @Trace(dispatcher = true)}.
*/
@AutoService(BugChecker.class)
@BugPattern(
name = "ScheduledTransactionTrace",
summary = "Scheduled operation must start a new New Relic transaction",
linkType = LinkType.NONE,
severity = SeverityLevel.ERROR,
tags = StandardTags.LIKELY_ERROR)
public final class ScheduledTransactionTraceCheck extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String TRACE_ANNOTATION_FQCN = "com.newrelic.api.agent.Trace";
private static final Matcher<Tree> IS_SCHEDULED =
hasAnnotation("org.springframework.scheduling.annotation.Scheduled");
private static final MultiMatcher<Tree, AnnotationTree> TRACE_ANNOTATION =
annotations(AT_LEAST_ONE, isType(TRACE_ANNOTATION_FQCN));
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!IS_SCHEDULED.matches(tree, state)) {
return Description.NO_MATCH;
}
ImmutableList<AnnotationTree> traceAnnotations =
TRACE_ANNOTATION.multiMatchResult(tree, state).matchingNodes();
if (traceAnnotations.isEmpty()) {
/* This method completely lacks the `@Trace` annotation; add it. */
return describeMatch(
tree,
SuggestedFix.builder()
.addImport(TRACE_ANNOTATION_FQCN)
.prefixWith(tree, "@Trace(dispatcher = true)")
.build());
}
AnnotationTree traceAnnotation = Iterables.getOnlyElement(traceAnnotations);
if (isCorrectAnnotation(traceAnnotation)) {
return Description.NO_MATCH;
}
/*
* The `@Trace` annotation is present but does not specify `dispatcher = true`. Add or update
* the `dispatcher` annotation element.
*/
return describeMatch(
traceAnnotation,
SuggestedFixes.updateAnnotationArgumentValues(
traceAnnotation, "dispatcher", ImmutableList.of("true"))
.build());
}
private static boolean isCorrectAnnotation(AnnotationTree traceAnnotation) {
return Boolean.TRUE.equals(
AnnotationMirrors.getAnnotationValue(
ASTHelpers.getAnnotationMirror(traceAnnotation), "dispatcher")
.getValue());
}
}

View File

@@ -19,14 +19,18 @@ import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
/** A {@link BugChecker} which flags methods that can and should be statically imported. */
/**
* A {@link BugChecker} which flags methods and constants that can and should be statically
* imported.
*/
// XXX: Tricky cases:
// - `org.springframework.http.MediaType` (do except for `ALL`?)
// - `org.springframework.http.HttpStatus` (not always an improvement, and `valueOf` must
// certainly be excluded)
// - `com.google.common.collect.Tables`
@@ -42,15 +46,19 @@ import java.util.Optional;
@AutoService(BugChecker.class)
@BugPattern(
name = "StaticImport",
summary = "Method should be statically imported",
summary = "Identifier should be statically imported",
linkType = LinkType.NONE,
severity = SeverityLevel.SUGGESTION,
tags = StandardTags.SIMPLIFICATION)
public final class StaticImportCheck extends BugChecker implements MemberSelectTreeMatcher {
private static final long serialVersionUID = 1L;
/**
* Types whose members should be statically imported, unless exempted by {@link
* #STATIC_IMPORT_EXEMPTED_MEMBERS} or {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS}.
*/
@VisibleForTesting
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_CLASSES =
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_TYPES =
ImmutableSet.of(
"com.google.common.base.Preconditions",
"com.google.common.base.Predicates",
@@ -80,14 +88,17 @@ public final class StaticImportCheck extends BugChecker implements MemberSelectT
"org.mockito.Answers",
"org.mockito.ArgumentMatchers",
"org.mockito.Mockito",
"org.springframework.boot.test.context.SpringBootTest.WebEnvironment",
"org.springframework.format.annotation.DateTimeFormat.ISO",
"org.springframework.http.HttpHeaders",
"org.springframework.http.HttpMethod",
"org.springframework.http.MediaType",
"org.testng.Assert",
"reactor.function.TupleUtils");
/** Type members that should be statically imported. */
@VisibleForTesting
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_METHODS =
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_MEMBERS =
ImmutableSetMultimap.<String, String>builder()
.putAll(
"com.google.common.collect.ImmutableListMultimap",
@@ -122,9 +133,41 @@ public final class StaticImportCheck extends BugChecker implements MemberSelectT
.putAll("com.google.common.collect.Comparators", "emptiesFirst", "emptiesLast")
.build();
/**
* Type members that should never be statically imported.
*
* <p>Identifiers listed by {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS} should be omitted from
* this collection.
*/
@VisibleForTesting
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_EXEMPTED_MEMBERS =
ImmutableSetMultimap.<String, String>builder()
.put("com.mongodb.client.model.Filters", "empty")
.put("org.springframework.http.MediaType", "ALL")
.build();
/**
* Identifiers that should never be statically imported.
*
* <p>This should be a superset of the identifiers flagged by {@link
* com.google.errorprone.bugpatterns.BadImport}.
*/
@VisibleForTesting
static final ImmutableSet<String> STATIC_IMPORT_EXEMPTED_IDENTIFIERS =
ImmutableSet.of(
"builder",
"create",
"copyOf",
"from",
"getDefaultInstance",
"INSTANCE",
"newBuilder",
"of",
"valueOf");
@Override
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
if (!isCandidate(state)) {
if (!isCandidateContext(state) || !isCandidate(tree)) {
return Description.NO_MATCH;
}
@@ -139,7 +182,7 @@ public final class StaticImportCheck extends BugChecker implements MemberSelectT
.orElse(Description.NO_MATCH);
}
private static boolean isCandidate(VisitorState state) {
private static boolean isCandidateContext(VisitorState state) {
Tree parentTree =
requireNonNull(state.getPath().getParentPath(), "MemberSelectTree lacks enclosing node")
.getLeaf();
@@ -154,6 +197,17 @@ public final class StaticImportCheck extends BugChecker implements MemberSelectT
}
}
private static boolean isCandidate(MemberSelectTree tree) {
String identifier = tree.getIdentifier().toString();
if (STATIC_IMPORT_EXEMPTED_IDENTIFIERS.contains(identifier)) {
return false;
}
Type type = ASTHelpers.getType(tree.getExpression());
return type != null
&& !STATIC_IMPORT_EXEMPTED_MEMBERS.containsEntry(type.toString(), identifier);
}
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) {
String canonicalName = importInfo.canonicalName();
return importInfo
@@ -161,8 +215,8 @@ public final class StaticImportCheck extends BugChecker implements MemberSelectT
.toJavaUtil()
.filter(
name ->
STATIC_IMPORT_CANDIDATE_CLASSES.contains(canonicalName)
|| STATIC_IMPORT_CANDIDATE_METHODS.containsEntry(canonicalName, name));
STATIC_IMPORT_CANDIDATE_TYPES.contains(canonicalName)
|| STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(canonicalName, name));
}
private static Optional<Fix> tryStaticImport(

View File

@@ -365,6 +365,7 @@ final class AssertJTemplates {
}
@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
ObjectEnumerableAssert<?, S> after(Set<S> set, T element) {
return assertThat(set).containsExactly(element);
}
@@ -441,6 +442,7 @@ final class AssertJTemplates {
return Refaster.anyOf(
assertThat(iterable.iterator().hasNext()).isTrue(),
assertThat(Iterables.size(iterable)).isNotEqualTo(0),
assertThat(Iterables.size(iterable)).isNotEqualTo(0L),
assertThat(Iterables.size(iterable)).isPositive());
}
@@ -449,6 +451,7 @@ final class AssertJTemplates {
return Refaster.anyOf(
assertThat(iterable.isEmpty()).isFalse(),
assertThat(iterable.size()).isNotEqualTo(0),
assertThat(iterable.size()).isNotEqualTo(0L),
assertThat(iterable.size()).isPositive());
}
@@ -664,6 +667,7 @@ final class AssertJTemplates {
return Refaster.anyOf(
assertThat(map.isEmpty()).isFalse(),
assertThat(map.size()).isNotEqualTo(0),
assertThat(map.size()).isNotEqualTo(0L),
assertThat(map.size()).isPositive(),
assertThat(Refaster.anyOf(map.keySet(), map.values())).isNotEmpty());
}

View File

@@ -162,7 +162,7 @@ final class ImmutableSetMultimapTemplates {
@AfterTemplate
ImmutableSetMultimap<K, V2> after(Multimap<K, V1> multimap) {
return ImmutableSetMultimap.copyOf(
Multimaps.transformValues(multimap, v -> valueTransformation(v)));
Multimaps.transformValues(multimap, e -> valueTransformation(e)));
}
}

View File

@@ -14,6 +14,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.annotation.Nullable;
/** Refaster templates related to expressions dealing with {@link Optional}s. */
final class OptionalTemplates {
@@ -24,7 +25,7 @@ final class OptionalTemplates {
// parentheses around the null check, but that's currently not the case. Try to fix that.
@BeforeTemplate
@SuppressWarnings("TernaryOperatorOptionalNegativeFiltering" /* Special case. */)
Optional<T> before(T object) {
Optional<T> before(@Nullable T object) {
return object == null ? Optional.empty() : Optional.of(object);
}

View File

@@ -142,6 +142,35 @@ final class ReactorTemplates {
}
}
/** Prefer {@link Flux#concatMap(Function)} over more contrived alternatives. */
static final class FluxConcatMap<T, S> {
@BeforeTemplate
Flux<S> before(Flux<T> flux, Function<? super T, ? extends Publisher<? extends S>> function) {
return Refaster.anyOf(flux.flatMap(function, 1), flux.flatMapSequential(function, 1));
}
@AfterTemplate
Flux<S> after(Flux<T> flux, Function<? super T, ? extends Publisher<? extends S>> function) {
return flux.concatMap(function);
}
}
/**
* Prefer {@link Flux#concatMapIterable(Function)} over {@link Flux#concatMapIterable(Function)},
* as the former has equivalent semantics but a clearer name.
*/
static final class FluxConcatMapIterable<T, S> {
@BeforeTemplate
Flux<S> before(Flux<T> flux, Function<? super T, ? extends Iterable<? extends S>> function) {
return flux.flatMapIterable(function);
}
@AfterTemplate
Flux<S> after(Flux<T> flux, Function<? super T, ? extends Iterable<? extends S>> function) {
return flux.concatMapIterable(function);
}
}
/**
* Don't use {@link Mono#flatMapMany(Function)} to implicitly convert a {@link Mono} to a {@link
* Flux}.
@@ -161,6 +190,32 @@ final class ReactorTemplates {
}
}
/** Prefer {@link Mono#flux()}} over more contrived alternatives. */
static final class MonoFlux<T> {
@BeforeTemplate
Flux<T> before(Mono<T> mono) {
return Flux.concat(mono);
}
@AfterTemplate
Flux<T> after(Mono<T> mono) {
return mono.flux();
}
}
/** 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.
*/
@@ -194,6 +249,32 @@ final class ReactorTemplates {
}
}
/** Prefer {@link Mono#as(Function)} when creating a {@link StepVerifier}. */
static final class StepVerifierFromMono<T> {
@BeforeTemplate
StepVerifier.FirstStep<? extends T> before(Mono<T> mono) {
return StepVerifier.create(mono);
}
@AfterTemplate
StepVerifier.FirstStep<? extends T> after(Mono<T> mono) {
return mono.as(StepVerifier::create);
}
}
/** Prefer {@link Flux#as(Function)} when creating a {@link StepVerifier}. */
static final class StepVerifierFromFlux<T> {
@BeforeTemplate
StepVerifier.FirstStep<? extends T> before(Flux<T> flux) {
return StepVerifier.create(flux);
}
@AfterTemplate
StepVerifier.FirstStep<? extends T> after(Flux<T> flux) {
return flux.as(StepVerifier::create);
}
}
/** Don't unnecessarily call {@link StepVerifier.Step#expectNext(Object[])}. */
static final class StepVerifierStepExpectNextEmpty<T> {
@BeforeTemplate

View File

@@ -14,6 +14,7 @@ import com.google.errorprone.refaster.annotation.BeforeTemplate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import javax.annotation.Nullable;
/** Refaster templates related to expressions dealing with {@link String}s. */
// XXX: Should we prefer `s -> !s.isEmpty()` or `not(String::isEmpty)`?
@@ -37,7 +38,7 @@ final class StringTemplates {
/** Prefer {@link Strings#isNullOrEmpty(String)} over the more verbose alternative. */
static final class StringIsNullOrEmpty {
@BeforeTemplate
boolean before(String str) {
boolean before(@Nullable String str) {
return str == null || str.isEmpty();
}

View File

@@ -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);",
"",
" 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;",
"",
" 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();
}
}

View File

@@ -0,0 +1,99 @@
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;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
public final class ScheduledTransactionTraceCheckTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(ScheduledTransactionTraceCheck.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(ScheduledTransactionTraceCheck.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import com.newrelic.api.agent.Trace;",
"import org.springframework.scheduling.annotation.Scheduled;",
"",
"class A {",
" void notScheduled() {}",
"",
" @Scheduled(fixedDelay = 1)",
" // BUG: Diagnostic contains:",
" void scheduledButNotTraced() {}",
"",
" @Scheduled(fixedDelay = 1)",
" // BUG: Diagnostic contains:",
" @Trace",
" void scheduledButImproperlyTraced1() {}",
"",
" @Scheduled(fixedDelay = 1)",
" // BUG: Diagnostic contains:",
" @Trace(dispatcher = false)",
" void scheduledButImproperlyTraced2() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace(dispatcher = true)",
" void scheduledAndProperlyTraced() {}",
"}")
.doTest();
}
// XXX: Enable this test for all JREs once https://github.com/google/error-prone/pull/2820 is
// merged and released.
@Test
@DisabledForJreRange(min = JRE.JAVA_12)
void replacement() {
refactoringTestHelper
.addInputLines(
"in/A.java",
"import com.newrelic.api.agent.Trace;",
"import org.springframework.scheduling.annotation.Scheduled;",
"",
"class A {",
" @Scheduled(fixedDelay = 1)",
" void scheduledButNotTraced() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace",
" void scheduledButImproperlyTraced1() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace(dispatcher = false)",
" void scheduledButImproperlyTraced2() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace(leaf = true)",
" void scheduledButImproperlyTraced3() {}",
"}")
.addOutputLines(
"out/A.java",
"import com.newrelic.api.agent.Trace;",
"import org.springframework.scheduling.annotation.Scheduled;",
"",
"class A {",
" @Trace(dispatcher = true)",
" @Scheduled(fixedDelay = 1)",
" void scheduledButNotTraced() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace(dispatcher = true)",
" void scheduledButImproperlyTraced1() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace(dispatcher = true)",
" void scheduledButImproperlyTraced2() {}",
"",
" @Scheduled(fixedDelay = 1)",
" @Trace(dispatcher = true, leaf = true)",
" void scheduledButImproperlyTraced3() {}",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -3,6 +3,7 @@ package tech.picnic.errorprone.bugpatterns;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
@@ -14,8 +15,20 @@ public final class StaticImportCheckTest {
@Test
void candidateMethodsAreNotRedundant() {
assertThat(StaticImportCheck.STATIC_IMPORT_CANDIDATE_METHODS.keySet())
.doesNotContainAnyElementsOf(StaticImportCheck.STATIC_IMPORT_CANDIDATE_CLASSES);
assertThat(StaticImportCheck.STATIC_IMPORT_CANDIDATE_MEMBERS.keySet())
.doesNotContainAnyElementsOf(StaticImportCheck.STATIC_IMPORT_CANDIDATE_TYPES);
}
@Test
void exemptedMembersAreNotVacuous() {
assertThat(StaticImportCheck.STATIC_IMPORT_EXEMPTED_MEMBERS.keySet())
.isSubsetOf(StaticImportCheck.STATIC_IMPORT_CANDIDATE_TYPES);
}
@Test
void exemptedMembersAreNotRedundant() {
assertThat(StaticImportCheck.STATIC_IMPORT_EXEMPTED_MEMBERS.values())
.doesNotContainAnyElementsOf(StaticImportCheck.STATIC_IMPORT_EXEMPTED_IDENTIFIERS);
}
@Test
@@ -27,6 +40,7 @@ public final class StaticImportCheckTest {
"import static com.google.common.collect.ImmutableSet.toImmutableSet;",
"import static java.nio.charset.StandardCharsets.UTF_8;",
"import static java.util.function.Predicate.not;",
"import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;",
"",
"import com.google.common.base.Predicates;",
"import com.google.common.collect.ImmutableMap;",
@@ -35,6 +49,9 @@ public final class StaticImportCheckTest {
"import java.nio.charset.StandardCharsets;",
"import java.util.Optional;",
"import java.util.function.Predicate;",
"import org.springframework.boot.test.context.SpringBootTest;",
"import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;",
"import org.springframework.http.MediaType;",
"",
"class A {",
" void m() {",
@@ -68,6 +85,15 @@ public final class StaticImportCheckTest {
" Object o1 = StandardCharsets.UTF_8;",
" Object o2 = UTF_8;",
"",
" // BUG: Diagnostic contains:",
" Object e1 = WebEnvironment.RANDOM_PORT;",
" Object e2 = RANDOM_PORT;",
"",
" // Not flagged because `MediaType.ALL` is exempted.",
" MediaType t1 = MediaType.ALL;",
" // BUG: Diagnostic contains:",
" MediaType t2 = MediaType.APPLICATION_JSON;",
"",
" Optional.empty();",
" }",
"",
@@ -87,8 +113,13 @@ public final class StaticImportCheckTest {
"import com.google.common.collect.ImmutableMap;",
"import com.google.common.collect.ImmutableSet;",
"import java.nio.charset.StandardCharsets;",
"import java.util.Objects;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.springframework.format.annotation.DateTimeFormat;",
"import org.springframework.format.annotation.DateTimeFormat.ISO;",
"import org.springframework.boot.test.context.SpringBootTest;",
"import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;",
"import org.springframework.http.MediaType;",
"",
"class A {",
" void m1() {",
@@ -101,7 +132,17 @@ public final class StaticImportCheckTest {
" Predicates.not(null);",
" not(null);",
"",
" Arguments.arguments(\"foo\");",
"",
" Objects.requireNonNull(\"bar\");",
"",
" Object o = StandardCharsets.UTF_8;",
"",
" ImmutableSet.of(",
" MediaType.ALL,",
" MediaType.APPLICATION_XHTML_XML,",
" MediaType.TEXT_HTML,",
" MediaType.valueOf(\"image/webp\"));",
" }",
"",
" void m2(",
@@ -113,23 +154,36 @@ public final class StaticImportCheckTest {
" @DateTimeFormat(iso = ISO.DATE) String date,",
" @DateTimeFormat(iso = ISO.DATE_TIME) String dateTime,",
" @DateTimeFormat(iso = ISO.TIME) String time) {}",
"",
" @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)",
" final class Test {}",
"}")
.addOutputLines(
"out/A.java",
"import static com.google.common.collect.ImmutableMap.toImmutableMap;",
"import static com.google.common.collect.ImmutableSet.toImmutableSet;",
"import static java.nio.charset.StandardCharsets.UTF_8;",
"import static java.util.Objects.requireNonNull;",
"import static java.util.function.Predicate.not;",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;",
"import static org.springframework.format.annotation.DateTimeFormat.ISO.DATE;",
"import static org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME;",
"import static org.springframework.format.annotation.DateTimeFormat.ISO.TIME;",
"import static org.springframework.http.MediaType.APPLICATION_XHTML_XML;",
"import static org.springframework.http.MediaType.TEXT_HTML;",
"",
"import com.google.common.base.Predicates;",
"import com.google.common.collect.ImmutableMap;",
"import com.google.common.collect.ImmutableSet;",
"import java.nio.charset.StandardCharsets;",
"import java.util.Objects;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.springframework.boot.test.context.SpringBootTest;",
"import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;",
"import org.springframework.format.annotation.DateTimeFormat;",
"import org.springframework.format.annotation.DateTimeFormat.ISO;",
"import org.springframework.http.MediaType;",
"",
"class A {",
" void m1() {",
@@ -142,7 +196,17 @@ public final class StaticImportCheckTest {
" Predicates.not(null);",
" not(null);",
"",
" arguments(\"foo\");",
"",
" requireNonNull(\"bar\");",
"",
" Object o = UTF_8;",
"",
" ImmutableSet.of(",
" MediaType.ALL,",
" APPLICATION_XHTML_XML,",
" TEXT_HTML,",
" MediaType.valueOf(\"image/webp\"));",
" }",
"",
" void m2(",
@@ -154,7 +218,10 @@ public final class StaticImportCheckTest {
" @DateTimeFormat(iso = DATE) String date,",
" @DateTimeFormat(iso = DATE_TIME) String dateTime,",
" @DateTimeFormat(iso = TIME) String time) {}",
"",
" @SpringBootTest(webEnvironment = RANDOM_PORT)",
" final class Test {}",
"}")
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -53,7 +53,7 @@ final class ImmutableSetMultimapTemplatesTest implements RefasterTemplateTestCas
ImmutableSetMultimap<String, Integer> testTransformMultimapValuesToImmutableSetMultimap() {
return ImmutableSetMultimap.copyOf(
Multimaps.transformValues(ImmutableSetMultimap.of("foo", 1L), v -> Math.toIntExact(v)));
Multimaps.transformValues(ImmutableSetMultimap.of("foo", 1L), e -> Math.toIntExact(e)));
}
ImmutableSet<ImmutableSetMultimap<String, Integer>>

View File

@@ -1,5 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.time.Duration;
import java.util.Optional;
@@ -45,10 +46,27 @@ final class ReactorTemplatesTest implements RefasterTemplateTestCase {
Flux.just(1).switchIfEmpty(Mono.empty()), Flux.just(2).switchIfEmpty(Flux.empty()));
}
ImmutableSet<Flux<Integer>> testFluxConcatMap() {
return ImmutableSet.of(
Flux.just(1).flatMap(Mono::just, 1), Flux.just(2).flatMapSequential(Mono::just, 1));
}
Flux<Integer> testFluxConcatMapIterable() {
return Flux.just(1, 2).flatMapIterable(ImmutableList::of);
}
Flux<String> testMonoFlatMapToFlux() {
return Mono.just("foo").flatMapMany(s -> Mono.just(s + s));
}
Flux<String> testMonoFlux() {
return Flux.concat(Mono.just("foo"));
}
Flux<String> testFluxIdentity() {
return Flux.concat(Flux.just("foo"));
}
ImmutableSet<Mono<Optional<String>>> testMonoCollectToOptional() {
return ImmutableSet.of(
Mono.just("foo").map(Optional::of).defaultIfEmpty(Optional.empty()),
@@ -59,6 +77,14 @@ final class ReactorTemplatesTest implements RefasterTemplateTestCase {
return ImmutableSet.of(PublisherProbe.of(Mono.empty()), PublisherProbe.of(Flux.empty()));
}
StepVerifier.FirstStep<Integer> testStepVerifierFromMono() {
return StepVerifier.create(Mono.just(1));
}
StepVerifier.FirstStep<Integer> testStepVerifierFromFlux() {
return StepVerifier.create(Flux.just(1));
}
StepVerifier.Step<Integer> testStepVerifierStepExpectNextEmpty() {
return StepVerifier.create(Mono.just(0)).expectNext();
}

View File

@@ -2,6 +2,7 @@ package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.MoreCollectors.toOptional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.time.Duration;
import java.util.Optional;
@@ -46,10 +47,26 @@ final class ReactorTemplatesTest implements RefasterTemplateTestCase {
return ImmutableSet.of(Flux.just(1), Flux.just(2));
}
ImmutableSet<Flux<Integer>> testFluxConcatMap() {
return ImmutableSet.of(Flux.just(1).concatMap(Mono::just), Flux.just(2).concatMap(Mono::just));
}
Flux<Integer> testFluxConcatMapIterable() {
return Flux.just(1, 2).concatMapIterable(ImmutableList::of);
}
Flux<String> testMonoFlatMapToFlux() {
return Mono.just("foo").flatMap(s -> Mono.just(s + s)).flux();
}
Flux<String> testMonoFlux() {
return Mono.just("foo").flux();
}
Flux<String> testFluxIdentity() {
return Flux.just("foo");
}
ImmutableSet<Mono<Optional<String>>> testMonoCollectToOptional() {
return ImmutableSet.of(
Mono.just("foo").flux().collect(toOptional()),
@@ -60,6 +77,14 @@ final class ReactorTemplatesTest implements RefasterTemplateTestCase {
return ImmutableSet.of(PublisherProbe.empty(), PublisherProbe.empty());
}
StepVerifier.FirstStep<Integer> testStepVerifierFromMono() {
return Mono.just(1).as(StepVerifier::create);
}
StepVerifier.FirstStep<Integer> testStepVerifierFromFlux() {
return Flux.just(1).as(StepVerifier::create);
}
StepVerifier.Step<Integer> testStepVerifierStepExpectNextEmpty() {
return StepVerifier.create(Mono.just(0));
}

45
pom.xml
View File

@@ -125,17 +125,17 @@
<!-- Dependency and plugin versions that are referenced in more than
one place. We use these to keep dependencies in sync. Version numbers
that need to be referenced only once should *not* be listed here. -->
<version.auto-service>1.0</version.auto-service>
<version.auto-service>1.0.1</version.auto-service>
<version.auto-value>1.8.2</version.auto-value>
<version.error-prone>${version.error-prone-orig}</version.error-prone>
<version.error-prone-fork>v${version.error-prone-orig}-picnic-1</version.error-prone-fork>
<version.error-prone-orig>2.9.0</version.error-prone-orig>
<version.error-prone-orig>2.10.0</version.error-prone-orig>
<version.error-prone-slf4j>0.1.4</version.error-prone-slf4j>
<version.findbugs-format-string>3.0.0</version.findbugs-format-string>
<version.guava-beta-checker>1.0</version.guava-beta-checker>
<version.jdk>11</version.jdk>
<version.maven>3.6.3</version.maven>
<version.mockito>3.12.4</version.mockito>
<version.mockito>4.0.0</version.mockito>
<version.nopen-checker>1.0.1</version.nopen-checker>
<version.nullaway>0.9.2</version.nullaway>
<!-- XXX: Two other dependencies are potentially of interest:
@@ -146,7 +146,7 @@
that we can pick them up. (But in case of `baseline-refaster-rules`
perhaps we can simply incorporate all of them.) -->
<version.palantir-assertj-automation>0.3.0</version.palantir-assertj-automation>
<version.palantir-baseline>4.25.0</version.palantir-baseline>
<version.palantir-baseline>4.42.0</version.palantir-baseline>
<version.surefire>2.22.2</version.surefire>
</properties>
@@ -190,14 +190,14 @@
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.12.5</version>
<version>2.13.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<version>1.1.2</version>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
@@ -241,7 +241,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-bom</artifactId>
<version>31.0-jre</version>
<version>31.0.1-jre</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -257,6 +257,11 @@
<artifactId>nopen-checker</artifactId>
<version>${version.nopen-checker}</version>
</dependency>
<dependency>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-api</artifactId>
<version>7.4.3</version>
</dependency>
<!-- Specified as a workaround for
https://github.com/mojohaus/versions-maven-plugin/issues/244. -->
<dependency>
@@ -281,7 +286,7 @@
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>2020.0.11</version>
<version>2020.0.13</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -293,12 +298,12 @@
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.2</version>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.1.10</version>
<version>2.1.11</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
@@ -335,7 +340,7 @@
<dependency>
<groupId>org.checkerframework</groupId>
<artifactId>checker-qual</artifactId>
<version>3.18.0</version>
<version>3.19.0</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
@@ -367,10 +372,15 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.3.10</version>
<version>5.3.13</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
@@ -681,7 +691,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>9.0</version>
<version>9.1</version>
</dependency>
</dependencies>
<executions>
@@ -1102,7 +1112,7 @@
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.7.0</version>
<version>1.7.3</version>
<configuration>
<!-- Use multiple threads to speed things up. Extend
timeouts to prevent false positives as a result of
@@ -1130,7 +1140,7 @@
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.0.2155</version>
<version>3.9.1.2184</version>
</plugin>
</plugins>
</pluginManagement>
@@ -1446,8 +1456,13 @@
-Xep:BetaApi:OFF
<!-- We don't target JDK 7. -->
-Xep:Java7ApiChecker:OFF
<!-- We don't target JDK 8. -->
-Xep:Java8ApiChecker:OFF
<!-- We don't target Android. -->
-Xep:StaticOrDefaultInterfaceMethod:OFF
<!-- XXX: This check flags false positives.
See https://github.com/google/error-prone/issues/2679. -->
-Xep:VoidMissingNullable:OFF
-XepOpt:NullAway:AnnotatedPackages=tech.picnic
-XepOpt:NullAway:AssertsEnabled=true
-XepOpt:NullAway:CheckOptionalEmptiness=true