This commit is contained in:
Stephan Schroevers
2024-04-27 16:26:35 +02:00
parent 2b7a555c05
commit d85f2dd6a8
11 changed files with 303 additions and 28 deletions

View File

@@ -277,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>
@@ -285,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>

View File

@@ -58,6 +58,12 @@ public class ReactorRulesBenchmarks {
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.

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.documentation;
package tech.picnic.errorprone.refaster.benchmark;
import static com.google.common.base.Preconditions.checkArgument;
@@ -13,16 +13,15 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** A compiler {@link Plugin} that generates JMH benchmarks for Refaster rules. */
// XXX: Move logic to separate package and Maven module.
// XXX: Review whether this can be an annotation processor instead.
@AutoService(Plugin.class)
public final class RefasterBenchmarkGenerator implements Plugin {
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 RefasterBenchmarkGenerator} instance. */
public RefasterBenchmarkGenerator() {}
/** Instantiates a new {@link RefasterRuleBenchmarkGenerator} instance. */
public RefasterRuleBenchmarkGenerator() {}
@Override
public String getName() {
@@ -34,9 +33,9 @@ public final class RefasterBenchmarkGenerator implements Plugin {
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.
// collection. (But how do we then determine the base directory?)
javacTask.addTaskListener(
new RefasterBenchmarkGeneratorTaskListener(
new RefasterRuleBenchmarkGeneratorTaskListener(
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
}

View File

@@ -1,10 +1,18 @@
package tech.picnic.errorprone.documentation;
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;
@@ -14,19 +22,22 @@ 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.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.tools.JavaFileObject;
import org.jspecify.annotations.Nullable;
// XXX: Document.
final class RefasterBenchmarkGeneratorTaskListener implements TaskListener {
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;
RefasterBenchmarkGeneratorTaskListener(Context context, Path outputPath) {
RefasterRuleBenchmarkGeneratorTaskListener(Context context, Path outputPath) {
this.context = context;
this.outputPath = outputPath;
}
@@ -55,18 +66,15 @@ final class RefasterBenchmarkGeneratorTaskListener implements TaskListener {
VisitorState.createForUtilityPurposes(context)
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
// XXX: Make static.
Matcher<Tree> isBenchmarked =
Matchers.hasAnnotation("tech.picnic.errorprone.refaster.annotation.Benchmarked");
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 || isBenchmarked.matches(classTree, state);
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.
}
@@ -75,6 +83,29 @@ final class RefasterBenchmarkGeneratorTaskListener implements TaskListener {
}.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.
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);
@@ -83,12 +114,4 @@ final class RefasterBenchmarkGeneratorTaskListener implements TaskListener {
String.format("Error while creating directory with path '%s'", outputPath), e);
}
}
private <T> void writeToFile(String identifier, String className, T data) {
Json.write(outputPath.resolve(String.format("%s-%s.json", identifier, className)), data);
}
private static String getSimpleClassName(URI path) {
return Paths.get(path).getFileName().toString().replace(".java", "");
}
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package tech.picnic.errorprone.refaster.benchmark;
final class RefasterRuleBenchmarkGeneratorTest {
// XXX: TBD. See `DocumentationGeneratorTest`.
}

View File

@@ -18,10 +18,14 @@ public @interface Benchmarked {
// - 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.
// Once configuration is supported, annotations on nested classes should override the
// configuration specified by outer classes.
// XXX: Explain use. Allow restriction by name?
public @interface Param {
@Target(ElementType.FIELD)
@interface Param {}
}
// XXX: Explain use
@Target(ElementType.METHOD)
@interface OnResult {}
}