mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 08:11:25 +00:00
Compare commits
4 Commits
v0.19.0
...
sschroever
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5f825b9ff | ||
|
|
d85f2dd6a8 | ||
|
|
2b7a555c05 | ||
|
|
ca85533b0d |
@@ -179,6 +179,11 @@
|
||||
<artifactId>mongodb-driver-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-core</artifactId>
|
||||
@@ -272,6 +277,11 @@
|
||||
<artifactId>refaster-compiler</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-rule-benchmark-generator</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-support</artifactId>
|
||||
@@ -280,6 +290,7 @@
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs combine.children="append">
|
||||
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
|
||||
<arg>-Xplugin:RefasterRuleBenchmarkGenerator -XoutputDirectory=${project.build.directory}/generated-test-sources/test-annotations</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -22,9 +22,12 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
@@ -211,6 +214,27 @@ final class AssortedRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't defer to subscription-time non-reactive operations that can efficiently be performed
|
||||
* during assembly.
|
||||
*/
|
||||
// XXX: There are all kinds of variations on this theme. Generalize.
|
||||
// XXX: Drop or move to `ReactorRules`.
|
||||
static final class MonoFromSupplierOptionalMapOrElse<T, S> {
|
||||
@BeforeTemplate
|
||||
Mono<S> before(Optional<T> optional, Function<? super T, S> transformer, S value) {
|
||||
return Refaster.anyOf(
|
||||
Mono.justOrEmpty(optional).map(transformer),
|
||||
Mono.justOrEmpty(optional).mapNotNull(transformer))
|
||||
.defaultIfEmpty(value);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<? extends S> after(Optional<T> optional, Function<? super T, S> transformer, S value) {
|
||||
return Mono.fromSupplier(() -> optional.map(transformer).orElse(value));
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Don't unnecessarily pass a method reference to {@link Supplier#get()} or wrap this method
|
||||
// * in a lambda expression.
|
||||
|
||||
@@ -127,6 +127,20 @@ final class OptionalRules {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Optional#equals(Object)} over more contrived alternatives. */
|
||||
static final class NotOptionalEqualsOptional<T, S> {
|
||||
@BeforeTemplate
|
||||
boolean before(Optional<T> optional, S value) {
|
||||
return Refaster.anyOf(
|
||||
optional.filter(value::equals).isEmpty(), optional.stream().noneMatch(value::equals));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(Optional<T> optional, S value) {
|
||||
return !optional.equals(Optional.of(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't use the ternary operator to extract the first element of a possibly-empty {@link
|
||||
* Iterator} as an {@link Optional}.
|
||||
|
||||
@@ -53,6 +53,7 @@ import tech.picnic.errorprone.refaster.annotation.Description;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsEmpty;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsIdentityOperation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsLikelyTrivialComputation;
|
||||
import tech.picnic.errorprone.refaster.matchers.ThrowsCheckedException;
|
||||
|
||||
/** Refaster rules related to Reactor expressions and statements. */
|
||||
@@ -172,6 +173,7 @@ final class ReactorRules {
|
||||
*
|
||||
* <p>In particular, avoid mixing of the {@link Optional} and {@link Mono} APIs.
|
||||
*/
|
||||
// XXX: Below we now have the opposite advice. Test.
|
||||
static final class MonoFromOptionalSwitchIfEmpty<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(Optional<T> optional, Mono<T> mono) {
|
||||
@@ -301,6 +303,38 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't eagerly instantiate {@link Throwable}s for {@link Mono#error(Throwable)}; let the
|
||||
* framework do it on subscription.
|
||||
*/
|
||||
static final class MonoError<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(@NotMatches(IsLikelyTrivialComputation.class) Throwable throwable) {
|
||||
return Mono.error(throwable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Throwable throwable) {
|
||||
return Mono.error(() -> throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't eagerly instantiate {@link Throwable}s for {@link Flux#error(Throwable)}; let the
|
||||
* framework do it on subscription.
|
||||
*/
|
||||
static final class FluxError<T> {
|
||||
@BeforeTemplate
|
||||
Flux<T> before(@NotMatches(IsLikelyTrivialComputation.class) Throwable throwable) {
|
||||
return Flux.error(throwable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<T> after(Throwable throwable) {
|
||||
return Flux.error(() -> throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't unnecessarily defer {@link Mono#error(Throwable)}. */
|
||||
static final class MonoDeferredError<T> {
|
||||
@BeforeTemplate
|
||||
@@ -1698,6 +1732,22 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't defer to subscription-time non-reactive operations that can efficiently be performed
|
||||
* during assembly.
|
||||
*/
|
||||
static final class OptionalMapOrElse<T, S, M extends Mono<? extends S>> {
|
||||
@BeforeTemplate
|
||||
Mono<S> before(Optional<T> optional, Function<? super T, M> transformer, M mono) {
|
||||
return Mono.justOrEmpty(optional).flatMap(transformer).switchIfEmpty(mono);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<? extends S> after(Optional<T> optional, Function<? super T, M> transformer, M mono) {
|
||||
return optional.map(transformer).orElse(mono);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link reactor.util.context.Context#empty()}} over more verbose alternatives. */
|
||||
// XXX: Introduce Refaster rules or a `BugChecker` that maps `(Immutable)Map.of(k, v)` to
|
||||
// `Context.of(k, v)` and likewise for multi-pair overloads.
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.openjdk.jmh.results.format.ResultFormatType.JSON;
|
||||
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.MayOptionallyUse;
|
||||
import com.google.errorprone.refaster.annotation.Placeholder;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
import org.openjdk.jmh.profile.GCProfiler;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.picnic.errorprone.refaster.annotation.Benchmarked;
|
||||
|
||||
// XXX: Fix warmup and measurements etc.
|
||||
// XXX: Flag cases where the `before` code is faster, taking into account variation.
|
||||
@SuppressWarnings("FieldCanBeFinal") // XXX: Triggers CompilesWithFix!!!
|
||||
@State(Scope.Benchmark)
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||
@Fork(
|
||||
jvmArgs = {"-Xms256M", "-Xmx256M"},
|
||||
value = 1)
|
||||
@Warmup(iterations = 1)
|
||||
@Measurement(iterations = 10, time = 1)
|
||||
public class ReactorRulesBenchmarks {
|
||||
public static void main(String[] args) throws RunnerException {
|
||||
String testRegex = Pattern.quote(ReactorRulesBenchmarks.class.getCanonicalName());
|
||||
new Runner(
|
||||
new OptionsBuilder()
|
||||
.include(testRegex)
|
||||
.addProfiler(GCProfiler.class)
|
||||
.resultFormat(JSON) // XXX: Review.
|
||||
.build())
|
||||
.run();
|
||||
}
|
||||
|
||||
// XXX: What a benchmarked rule could look like.
|
||||
@Benchmarked
|
||||
static final class MonoFromOptionalSwitchIfEmpty<T> {
|
||||
// XXX: These parameters could perhaps be inferred in the common case.
|
||||
@Benchmarked.Param private final Optional<String> emptyOptional = Optional.empty();
|
||||
@Benchmarked.Param private final Optional<String> nonEmptyOptional = Optional.of("foo");
|
||||
@Benchmarked.Param private final Mono<String> emptyMono = Mono.<String>empty().hide();
|
||||
@Benchmarked.Param private final Mono<String> nonEmptyMono = Mono.just("bar").hide();
|
||||
|
||||
// XXX: Dropped to hide this class from `RefasterRuleCompilerTaskListener`: @BeforeTemplate
|
||||
Mono<T> before(Optional<T> optional, Mono<T> mono) {
|
||||
return optional.map(Mono::just).orElse(mono);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Optional<T> optional, Mono<T> mono) {
|
||||
return Mono.justOrEmpty(optional).switchIfEmpty(mono);
|
||||
}
|
||||
|
||||
// XXX: Methods such as this one could perhaps be inferred in the common case.
|
||||
@Benchmarked.OnResult
|
||||
T subscribe(Mono<T> mono) {
|
||||
return mono.block();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Variations like this would be generated.
|
||||
public static class MonoFromOptionalSwitchIfEmptyBenchmark extends ReactorRulesBenchmarks {
|
||||
private final MonoFromOptionalSwitchIfEmpty<String> rule = new MonoFromOptionalSwitchIfEmpty();
|
||||
// private final Optional<String> optional = Optional.of("foo");
|
||||
// private final Mono<String> mono = Mono.just("bar").hide();
|
||||
private final Optional<String> optional = Optional.empty();
|
||||
private final Mono<String> mono = Mono.<String>empty().hide();
|
||||
private final Mono<String> before = rule.before(optional, mono);
|
||||
private final Mono<String> after = rule.after(optional, mono);
|
||||
|
||||
@Benchmark
|
||||
public Mono<String> before() {
|
||||
return rule.before(optional, mono);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public String beforeSubscribe1() {
|
||||
return before.block();
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Mono<String> after() {
|
||||
return rule.after(optional, mono);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public String afterSubscribe() {
|
||||
return after.block();
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
|
||||
abstract static class MonoFlatMapToFlux<T, S> {
|
||||
@Placeholder(allowsIdentity = true)
|
||||
abstract Mono<S> transformation(@MayOptionallyUse T value);
|
||||
|
||||
// XXX: Dropped to hide this class from `RefasterRuleCompilerTaskListener`: @BeforeTemplate
|
||||
Flux<S> before(Mono<T> mono) {
|
||||
return mono.flatMapMany(v -> transformation(v));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Flux<S> after(Mono<T> mono) {
|
||||
return mono.flatMap(v -> transformation(v)).flux();
|
||||
}
|
||||
|
||||
@Benchmarked.OnResult
|
||||
Long subscribe(Flux<T> flux) {
|
||||
return flux.count().block();
|
||||
}
|
||||
|
||||
@Benchmarked.MinimalPlaceholder
|
||||
<X> Mono<X> identity(X value) {
|
||||
return Mono.just(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MonoFlatMapToFluxBenchmarks extends ReactorRulesBenchmarks {
|
||||
abstract static class Rule<T, S> {
|
||||
abstract Mono<S> transformation(T value);
|
||||
|
||||
Flux<S> before(Mono<T> mono) {
|
||||
return mono.flatMapMany(v -> transformation(v));
|
||||
}
|
||||
|
||||
Flux<S> after(Mono<T> mono) {
|
||||
return mono.flatMap(v -> transformation(v)).flux();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: For variantions, we could use `@Param(strings)` indirection, like in
|
||||
// https://github.com/openjdk/jmh/blob/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_35_Profilers.java
|
||||
|
||||
// XXX: Or we can make the benchmark abstract and have subclasses for each variant:
|
||||
// https://github.com/openjdk/jmh/blob/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_24_Inheritance.java
|
||||
|
||||
public static class MonoFlatMapToFluxStringStringBenchmark extends MonoFlatMapToFluxBenchmarks {
|
||||
private final Rule<String, String> rule =
|
||||
new Rule<>() {
|
||||
@Override
|
||||
Mono<String> transformation(String value) {
|
||||
return Mono.just(value);
|
||||
}
|
||||
};
|
||||
private final Mono<String> mono = Mono.just("foo");
|
||||
private final Flux<String> before = rule.before(mono);
|
||||
private final Flux<String> after = rule.after(mono);
|
||||
|
||||
@Benchmark
|
||||
public Flux<String> before() {
|
||||
return rule.before(mono);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Flux<String> after() {
|
||||
return rule.after(mono);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Long onResultOfBefore() {
|
||||
return before.count().block();
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Long onResultOfAfter() {
|
||||
return after.count().block();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,12 @@ final class OptionalRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Optional.of("baz").stream().anyMatch("qux"::equals));
|
||||
}
|
||||
|
||||
ImmutableSet<Boolean> testNotOptionalEqualsOptional() {
|
||||
return ImmutableSet.of(
|
||||
Optional.of("foo").filter("bar"::equals).isEmpty(),
|
||||
Optional.of("baz").stream().noneMatch("qux"::equals));
|
||||
}
|
||||
|
||||
ImmutableSet<Optional<String>> testOptionalFirstIteratorElement() {
|
||||
return ImmutableSet.of(
|
||||
ImmutableSet.of("foo").iterator().hasNext()
|
||||
|
||||
@@ -45,6 +45,12 @@ final class OptionalRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Optional.of("baz").equals(Optional.of("qux")));
|
||||
}
|
||||
|
||||
ImmutableSet<Boolean> testNotOptionalEqualsOptional() {
|
||||
return ImmutableSet.of(
|
||||
!Optional.of("foo").equals(Optional.of("bar")),
|
||||
!Optional.of("baz").equals(Optional.of("qux")));
|
||||
}
|
||||
|
||||
ImmutableSet<Optional<String>> testOptionalFirstIteratorElement() {
|
||||
return ImmutableSet.of(
|
||||
stream(ImmutableSet.of("foo").iterator()).findFirst(),
|
||||
|
||||
@@ -118,6 +118,16 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return Flux.just("foo", "bar").zipWithIterable(ImmutableSet.of(1, 2), String::repeat);
|
||||
}
|
||||
|
||||
ImmutableSet<?> testMonoError() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
return ImmutableSet.of(Mono.error(exception), Mono.error(new IllegalArgumentException()));
|
||||
}
|
||||
|
||||
ImmutableSet<?> testFluxError() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
return ImmutableSet.of(Flux.error(exception), Flux.error(new IllegalArgumentException()));
|
||||
}
|
||||
|
||||
Mono<Void> testMonoDeferredError() {
|
||||
return Mono.defer(() -> Mono.error(new IllegalStateException()));
|
||||
}
|
||||
@@ -575,6 +585,10 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
MathFlux.min(Flux.just(1), reverseOrder()), MathFlux.max(Flux.just(2), naturalOrder()));
|
||||
}
|
||||
|
||||
Mono<String> testOptionalMapOrElse() {
|
||||
return Mono.justOrEmpty(Optional.of("foo")).flatMap(Mono::just).switchIfEmpty(Mono.just("bar"));
|
||||
}
|
||||
|
||||
ImmutableSet<Context> testContextEmpty() {
|
||||
return ImmutableSet.of(Context.of(ImmutableMap.of()), Context.of(ImmutableMap.of(1, 2)));
|
||||
}
|
||||
|
||||
@@ -123,6 +123,16 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
.map(function(String::repeat));
|
||||
}
|
||||
|
||||
ImmutableSet<?> testMonoError() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
return ImmutableSet.of(Mono.error(exception), Mono.error(() -> new IllegalArgumentException()));
|
||||
}
|
||||
|
||||
ImmutableSet<?> testFluxError() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
return ImmutableSet.of(Flux.error(exception), Flux.error(() -> new IllegalArgumentException()));
|
||||
}
|
||||
|
||||
Mono<Void> testMonoDeferredError() {
|
||||
return Mono.error(() -> new IllegalStateException());
|
||||
}
|
||||
@@ -564,6 +574,10 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return ImmutableSet.of(MathFlux.max(Flux.just(1)), MathFlux.max(Flux.just(2)));
|
||||
}
|
||||
|
||||
Mono<String> testOptionalMapOrElse() {
|
||||
return Optional.of("foo").map(Mono::just).orElse(Mono.just("bar"));
|
||||
}
|
||||
|
||||
ImmutableSet<Context> testContextEmpty() {
|
||||
return ImmutableSet.of(Context.empty(), Context.of(ImmutableMap.of(1, 2)));
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs combine.children="append">
|
||||
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
|
||||
<!-- XXX: Enable `refaster-rule-benchmark-generator` here. -->
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
73
pom.xml
73
pom.xml
@@ -45,6 +45,7 @@
|
||||
<module>error-prone-guidelines</module>
|
||||
<module>error-prone-utils</module>
|
||||
<module>refaster-compiler</module>
|
||||
<module>refaster-rule-benchmark-generator</module>
|
||||
<module>refaster-runner</module>
|
||||
<module>refaster-support</module>
|
||||
<module>refaster-test-support</module>
|
||||
@@ -212,6 +213,7 @@
|
||||
<version.error-prone-slf4j>0.1.24</version.error-prone-slf4j>
|
||||
<version.guava-beta-checker>1.0</version.guava-beta-checker>
|
||||
<version.jdk>17</version.jdk>
|
||||
<version.jmh>1.37</version.jmh>
|
||||
<version.maven>3.9.5</version.maven>
|
||||
<version.mockito>5.11.0</version.mockito>
|
||||
<version.nopen-checker>1.0.1</version.nopen-checker>
|
||||
@@ -468,6 +470,11 @@
|
||||
<artifactId>mongodb-driver-core</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${version.jmh}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-templating</artifactId>
|
||||
@@ -932,6 +939,11 @@
|
||||
<artifactId>auto-service</artifactId>
|
||||
<version>${version.auto-service}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${version.jmh}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-templating</artifactId>
|
||||
@@ -2054,6 +2066,67 @@
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<!-- Enables execution of a JMH benchmark. Given a benchmark class
|
||||
`tech.picnic.MyBenchmark`, the following command (executed against
|
||||
the (sub)module in which the benchmark resides) will compile and
|
||||
execute said benchmark:
|
||||
|
||||
mvn process-test-classes -Dverification.skip \
|
||||
-Djmh.run-benchmark=tech.picnic.MyBenchmark
|
||||
-->
|
||||
<id>run-jmh-benchmark</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>jmh.run-benchmark</name>
|
||||
</property>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build-jmh-runtime-classpath</id>
|
||||
<goals>
|
||||
<goal>build-classpath</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputProperty>testClasspath</outputProperty>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>run-jmh-benchmark</id>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<phase>process-test-classes</phase>
|
||||
<configuration>
|
||||
<classpathScope>test</classpathScope>
|
||||
<mainClass>${jmh.run-benchmark}</mainClass>
|
||||
<systemProperties>
|
||||
<!-- The runtime classpath is defined
|
||||
in this way so that any JVMs forked by
|
||||
JMH will have the desired classpath. -->
|
||||
<systemProperty>
|
||||
<key>java.class.path</key>
|
||||
<value>${project.build.testOutputDirectory}${path.separator}${project.build.outputDirectory}${path.separator}${testClasspath}</value>
|
||||
</systemProperty>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<!-- This profile is auto-activated when performing a release. -->
|
||||
<id>release</id>
|
||||
|
||||
117
refaster-rule-benchmark-generator/pom.xml
Normal file
117
refaster-rule-benchmark-generator/pom.xml
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.16.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>refaster-rule-benchmark-generator</artifactId>
|
||||
|
||||
<name>Picnic :: Error Prone Support :: Refaster Rule Benchmark Generator</name>
|
||||
<!-- XXX: Review description: can this be an annotation processor? -->
|
||||
<description>Compiler plugin that derives JMH benchmarks from Refaster Rules.</description>
|
||||
<url>https://error-prone.picnic.tech</url>
|
||||
|
||||
<dependencies>
|
||||
<!-- XXX: Prune this list. -->
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_check_api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
<!-- XXX: Review scope. -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-support</artifactId>
|
||||
<!-- XXX: Review scope. -->
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-parameter-names</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto</groupId>
|
||||
<artifactId>auto-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- XXX: Explicitly declared as a workaround for
|
||||
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.picnic.errorprone.refaster.benchmark;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.Plugin;
|
||||
import com.sun.tools.javac.api.BasicJavacTask;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** A compiler {@link Plugin} that generates JMH benchmarks for Refaster rules. */
|
||||
// XXX: Review whether this can be an annotation processor instead.
|
||||
@AutoService(Plugin.class)
|
||||
public final class RefasterRuleBenchmarkGenerator implements Plugin {
|
||||
@VisibleForTesting static final String OUTPUT_DIRECTORY_FLAG = "-XoutputDirectory";
|
||||
private static final Pattern OUTPUT_DIRECTORY_FLAG_PATTERN =
|
||||
Pattern.compile(Pattern.quote(OUTPUT_DIRECTORY_FLAG) + "=(.*)");
|
||||
|
||||
/** Instantiates a new {@link RefasterRuleBenchmarkGenerator} instance. */
|
||||
public RefasterRuleBenchmarkGenerator() {}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavacTask javacTask, String... args) {
|
||||
checkArgument(args.length == 1, "Precisely one path must be provided");
|
||||
|
||||
// XXX: Drop all path logic: instead generate in the same package as the Refaster rule
|
||||
// collection. (But how do we then determine the base directory?)
|
||||
javacTask.addTaskListener(
|
||||
new RefasterRuleBenchmarkGeneratorTaskListener(
|
||||
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Path getOutputPath(String pathArg) {
|
||||
Matcher matcher = OUTPUT_DIRECTORY_FLAG_PATTERN.matcher(pathArg);
|
||||
checkArgument(
|
||||
matcher.matches(), "'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
|
||||
String path = matcher.group(1);
|
||||
try {
|
||||
return Path.of(path);
|
||||
} catch (InvalidPathException e) {
|
||||
throw new IllegalArgumentException(String.format("Invalid path '%s'", path), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package tech.picnic.errorprone.refaster.benchmark;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.Matchers;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskEvent.Kind;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.source.util.TreePathScanner;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import javax.tools.JavaFileObject;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
// XXX: Document.
|
||||
final class RefasterRuleBenchmarkGeneratorTaskListener implements TaskListener {
|
||||
private static final Matcher<Tree> IS_TEMPLATE =
|
||||
anyOf(hasAnnotation(BeforeTemplate.class), hasAnnotation(AfterTemplate.class));
|
||||
private static final Matcher<Tree> IS_BENCHMARKED =
|
||||
Matchers.hasAnnotation("tech.picnic.errorprone.refaster.annotation.Benchmarked");
|
||||
|
||||
private final Context context;
|
||||
private final Path outputPath;
|
||||
|
||||
RefasterRuleBenchmarkGeneratorTaskListener(Context context, Path outputPath) {
|
||||
this.context = context;
|
||||
this.outputPath = outputPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void started(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() == Kind.ANALYZE) {
|
||||
createOutputDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() != Kind.ANALYZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
if (sourceFile == null || compilationUnit == null || classTree == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VisitorState state =
|
||||
VisitorState.createForUtilityPurposes(context)
|
||||
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
|
||||
|
||||
new TreePathScanner<@Nullable Void, Boolean>() {
|
||||
@Override
|
||||
public @Nullable Void visitClass(ClassTree classTree, Boolean doBenchmark) {
|
||||
// XXX: Validate that `@Benchmarked` is only placed in contexts with at least one Refaster
|
||||
// rule.
|
||||
boolean inspectClass = doBenchmark || IS_BENCHMARKED.matches(classTree, state);
|
||||
|
||||
if (inspectClass) {
|
||||
System.out.println(handle(classTree, state));
|
||||
// XXX: If this class has a `@BeforeTemplate` method, generate a benchmark for it.
|
||||
}
|
||||
|
||||
return super.visitClass(classTree, inspectClass);
|
||||
}
|
||||
}.scan(compilationUnit, false);
|
||||
}
|
||||
|
||||
// XXX: Name? Scope?
|
||||
private static Rule handle(ClassTree classTree, VisitorState state) {
|
||||
ImmutableList<Rule.Method> methods =
|
||||
classTree.getMembers().stream()
|
||||
.filter(m -> IS_TEMPLATE.matches(m, state))
|
||||
.map(m -> process((MethodTree) m, state))
|
||||
.collect(toImmutableList());
|
||||
|
||||
Rule rule = new Rule(classTree, methods);
|
||||
return rule;
|
||||
}
|
||||
|
||||
private static Rule.Method process(MethodTree methodTree, VisitorState state) {
|
||||
// XXX: Initially, disallow `Refaster.x` usages.
|
||||
// XXX: Initially, disallow references to `@Placeholder` methods.
|
||||
// XXX: Disallow `void` methods. (Can't be black-holed.)
|
||||
return new Rule.Method(methodTree);
|
||||
}
|
||||
|
||||
// XXX: Move types down.
|
||||
record Rule(ClassTree tree, ImmutableList<Rule.Method> methods) {
|
||||
record Method(MethodTree tree) {}
|
||||
}
|
||||
|
||||
private void createOutputDirectory() {
|
||||
try {
|
||||
Files.createDirectories(outputPath);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Error while creating directory with path '%s'", outputPath), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.picnic.errorprone.refaster.benchmark;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.FileManagers;
|
||||
import com.google.errorprone.FileObjects;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
import com.sun.tools.javac.api.JavacTool;
|
||||
import com.sun.tools.javac.file.JavacFileManager;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
// XXX: This code is a near-duplicate of the identically named class in `documentation-support`.
|
||||
public final class Compilation {
|
||||
private Compilation() {}
|
||||
|
||||
public static void compileWithRefasterRuleBenchmarkGenerator(
|
||||
Path outputDirectory, String path, String... lines) {
|
||||
compileWithRefasterRuleBenchmarkGenerator(
|
||||
outputDirectory.toAbsolutePath().toString(), path, lines);
|
||||
}
|
||||
|
||||
public static void compileWithRefasterRuleBenchmarkGenerator(
|
||||
String outputDirectory, String path, String... lines) {
|
||||
/*
|
||||
* The compiler options specified here largely match those used by Error Prone's
|
||||
* `CompilationTestHelper`. A key difference is the stricter linting configuration. When
|
||||
* compiling using JDK 21+, these lint options also require that certain JDK modules are
|
||||
* explicitly exported.
|
||||
*/
|
||||
compile(
|
||||
ImmutableList.of(
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
|
||||
"-encoding",
|
||||
"UTF-8",
|
||||
"-parameters",
|
||||
"-proc:none",
|
||||
"-Werror",
|
||||
"-Xlint:all,-serial",
|
||||
"-Xplugin:RefasterRuleBenchmarkGenerator -XoutputDirectory=" + outputDirectory,
|
||||
"-XDdev",
|
||||
"-XDcompilePolicy=simple"),
|
||||
FileObjects.forSourceLines(path, lines));
|
||||
}
|
||||
|
||||
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
|
||||
JavacFileManager javacFileManager = FileManagers.testFileManager();
|
||||
JavaCompiler compiler = JavacTool.create();
|
||||
|
||||
List<Diagnostic<?>> diagnostics = new ArrayList<>();
|
||||
JavacTaskImpl task =
|
||||
(JavacTaskImpl)
|
||||
compiler.getTask(
|
||||
null,
|
||||
javacFileManager,
|
||||
diagnostics::add,
|
||||
options,
|
||||
ImmutableList.of(),
|
||||
ImmutableList.of(javaFileObject));
|
||||
|
||||
Boolean result = task.call();
|
||||
assertThat(diagnostics).isEmpty();
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.picnic.errorprone.refaster.benchmark;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
// XXX: Add relevant tests also present in `DocumentationGeneratorTaskListenerTest`.
|
||||
// XXX: Test package placement.
|
||||
final class RefasterRuleBenchmarkGeneratorTaskListenerTest {
|
||||
// XXX: Rename!
|
||||
@Test
|
||||
void xxx(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithRefasterRuleBenchmarkGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"""
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.util.Optional;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.picnic.errorprone.refaster.annotation.Benchmarked;
|
||||
|
||||
@Benchmarked
|
||||
final class MonoFromOptionalSwitchIfEmpty<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(Optional<T> optional, Mono<T> mono) {
|
||||
return optional.map(Mono::just).orElse(mono);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Optional<T> optional, Mono<T> mono) {
|
||||
return Mono.justOrEmpty(optional).switchIfEmpty(mono);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package tech.picnic.errorprone.refaster.benchmark;
|
||||
|
||||
final class RefasterRuleBenchmarkGeneratorTest {
|
||||
// XXX: TBD. See `DocumentationGeneratorTest`.
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package tech.picnic.errorprone.refaster.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that benchmark(s) should be generated for the annotated Refaster rule or group of
|
||||
* Refaster rules.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Benchmarked {
|
||||
// XXX: Make configurable. E.g.
|
||||
// - While not supported, don't complain about `Refaster.anyOf` or other `Refaster` utility method
|
||||
// usages.
|
||||
// - Specify warmup and measurement iterations.
|
||||
// - Specify output time unit.
|
||||
// - Value generation hints.f
|
||||
// Once configuration is supported, annotations on nested classes should override the
|
||||
// configuration specified by outer classes.
|
||||
|
||||
// XXX: Explain use. Allow restriction by name?
|
||||
@Target(ElementType.FIELD)
|
||||
@interface Param {}
|
||||
|
||||
// XXX: Explain use
|
||||
@Target(ElementType.METHOD)
|
||||
@interface OnResult {}
|
||||
|
||||
// XXX: Explain use
|
||||
@Target(ElementType.METHOD)
|
||||
@interface MinimalPlaceholder {}
|
||||
}
|
||||
Reference in New Issue
Block a user