New posts
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>annotation-processor</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>builder-processor</artifactId>
|
||||
<name>builder-processor</name>
|
||||
<description>The @GenerateBuilder annotation and the annotation processor that implements it.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- This module ships META-INF/services/javax.annotation.processing.Processor, which
|
||||
names a class that does not exist yet while we are compiling it. Switch annotation
|
||||
processing off for this module's own compilation. -->
|
||||
<proc>none</proc>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package com.ankurm.builder;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.Filer;
|
||||
import javax.annotation.processing.Messager;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.annotation.processing.SupportedOptions;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.lang.model.util.ElementFilter;
|
||||
import javax.lang.model.util.Elements;
|
||||
import javax.lang.model.util.Types;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.JavaFileObject;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Generates a fluent builder for every type annotated with {@link GenerateBuilder}.
|
||||
*
|
||||
* <p>Pass {@code -Abuilder.debug=true} to javac to see one NOTE per processing round.
|
||||
*/
|
||||
@SupportedAnnotationTypes("com.ankurm.builder.GenerateBuilder")
|
||||
@SupportedOptions("builder.debug")
|
||||
public final class BuilderProcessor extends AbstractProcessor {
|
||||
|
||||
private Elements elements;
|
||||
private Types types;
|
||||
private Filer filer;
|
||||
private Messager messager;
|
||||
private boolean debug;
|
||||
private int round;
|
||||
|
||||
@Override
|
||||
public synchronized void init(ProcessingEnvironment env) {
|
||||
super.init(env);
|
||||
this.elements = env.getElementUtils();
|
||||
this.types = env.getTypeUtils();
|
||||
this.filer = env.getFiler();
|
||||
this.messager = env.getMessager();
|
||||
this.debug = "true".equals(env.getOptions().get("builder.debug"));
|
||||
}
|
||||
|
||||
/** Without this, AbstractProcessor answers RELEASE_6 and javac warns. */
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.latestSupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
round++;
|
||||
if (debug) {
|
||||
messager.printMessage(Diagnostic.Kind.NOTE,
|
||||
"builder round " + round + ": annotations=" + annotations
|
||||
+ " roots=" + roundEnv.getRootElements()
|
||||
+ " processingOver=" + roundEnv.processingOver());
|
||||
}
|
||||
for (Element element : roundEnv.getElementsAnnotatedWith(GenerateBuilder.class)) {
|
||||
generate(element);
|
||||
}
|
||||
return true; // we claim GenerateBuilder: no other processor needs to see it
|
||||
}
|
||||
|
||||
private void generate(Element element) {
|
||||
if (element.getKind() != ElementKind.CLASS && element.getKind() != ElementKind.RECORD) {
|
||||
error(element, "@GenerateBuilder can only be applied to a class or a record, not to "
|
||||
+ element.getKind().toString().toLowerCase());
|
||||
return;
|
||||
}
|
||||
TypeElement type = (TypeElement) element;
|
||||
if (type.getModifiers().contains(Modifier.ABSTRACT)) {
|
||||
error(type, "@GenerateBuilder cannot be applied to abstract class " + type.getSimpleName());
|
||||
return;
|
||||
}
|
||||
if (!type.getTypeParameters().isEmpty()) {
|
||||
error(type, "@GenerateBuilder does not support generic types such as " + type.getSimpleName());
|
||||
return;
|
||||
}
|
||||
if (type.getNestingKind().isNested()) {
|
||||
error(type, "@GenerateBuilder supports top-level types only; " + type.getSimpleName() + " is nested");
|
||||
return;
|
||||
}
|
||||
List<ExecutableElement> ctors = ElementFilter.constructorsIn(type.getEnclosedElements()).stream()
|
||||
.filter(c -> !c.getModifiers().contains(Modifier.PRIVATE))
|
||||
.toList();
|
||||
if (ctors.size() != 1) {
|
||||
error(type, type.getSimpleName() + " must have exactly one non-private constructor, but has "
|
||||
+ ctors.size());
|
||||
return;
|
||||
}
|
||||
ExecutableElement ctor = ctors.get(0);
|
||||
|
||||
GenerateBuilder config = type.getAnnotation(GenerateBuilder.class);
|
||||
String pkg = elements.getPackageOf(type).getQualifiedName().toString();
|
||||
String builderName = config.name().isEmpty() ? type.getSimpleName() + "Builder" : config.name();
|
||||
String source = render(pkg, builderName, type, ctor, config.prefix());
|
||||
|
||||
String qualified = pkg.isEmpty() ? builderName : pkg + "." + builderName;
|
||||
try {
|
||||
JavaFileObject file = filer.createSourceFile(qualified, type); // originating element
|
||||
try (Writer w = file.openWriter()) {
|
||||
w.write(source);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
error(type, "could not write " + qualified + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String render(String pkg, String builderName, TypeElement type, ExecutableElement ctor, String prefix) {
|
||||
TypeMirror listType = types.erasure(elements.getTypeElement("java.util.List").asType());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!pkg.isEmpty()) {
|
||||
sb.append("package ").append(pkg).append(";\n\n");
|
||||
}
|
||||
sb.append("// Generated by ").append(BuilderProcessor.class.getName()).append(" - do not edit.\n");
|
||||
sb.append("public final class ").append(builderName).append(" {\n\n");
|
||||
|
||||
for (VariableElement p : ctor.getParameters()) {
|
||||
sb.append(" private ").append(p.asType()).append(' ').append(p.getSimpleName()).append(";\n");
|
||||
}
|
||||
for (VariableElement p : ctor.getParameters()) {
|
||||
String n = p.getSimpleName().toString();
|
||||
sb.append("\n public ").append(builderName).append(' ').append(methodName(prefix, n))
|
||||
.append('(').append(p.asType()).append(' ').append(n).append(") {\n")
|
||||
.append(" this.").append(n).append(" = ").append(n).append(";\n")
|
||||
.append(" return this;\n }\n");
|
||||
|
||||
// Types in action: for a List<E> parameter, also generate addTo<Name>(E).
|
||||
if (p.asType().getKind() == TypeKind.DECLARED
|
||||
&& types.isAssignable(types.erasure(p.asType()), listType)) {
|
||||
List<? extends TypeMirror> args = ((DeclaredType) p.asType()).getTypeArguments();
|
||||
if (args.size() == 1 && args.get(0).getKind() == TypeKind.DECLARED) {
|
||||
String e = args.get(0).toString();
|
||||
sb.append("\n public ").append(builderName).append(" addTo").append(capitalize(n))
|
||||
.append('(').append(e).append(" element) {\n")
|
||||
.append(" java.util.List<").append(e).append("> copy = this.").append(n)
|
||||
.append(" == null ? new java.util.ArrayList<>() : new java.util.ArrayList<>(this.")
|
||||
.append(n).append(");\n")
|
||||
.append(" copy.add(element);\n")
|
||||
.append(" this.").append(n).append(" = copy;\n")
|
||||
.append(" return this;\n }\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n public ").append(type.getQualifiedName()).append(" build() {\n")
|
||||
.append(" return new ").append(type.getQualifiedName()).append('(');
|
||||
String sep = "";
|
||||
for (VariableElement p : ctor.getParameters()) {
|
||||
sb.append(sep).append(p.getSimpleName());
|
||||
sep = ", ";
|
||||
}
|
||||
sb.append(");\n }\n}\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String methodName(String prefix, String property) {
|
||||
return prefix.isEmpty() ? property : prefix + capitalize(property);
|
||||
}
|
||||
|
||||
private static String capitalize(String s) {
|
||||
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
|
||||
}
|
||||
|
||||
private void error(Element where, String message) {
|
||||
messager.printMessage(Diagnostic.Kind.ERROR, message, where);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ankurm.builder;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Asks {@link BuilderProcessor} to generate a fluent builder for the annotated class or record.
|
||||
*
|
||||
* <p>The builder is built from the parameters of the type's single non-private constructor (for a
|
||||
* record, that is the canonical constructor). SOURCE retention: only the compiler ever needs to see
|
||||
* this annotation, so it leaves no trace in the compiled class file.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface GenerateBuilder {
|
||||
|
||||
/** Name of the generated class. Empty means {@code <SimpleName>Builder}. */
|
||||
String name() default "";
|
||||
|
||||
/** Prefix for setter-style methods, e.g. {@code "with"} gives {@code withName(..)}. Empty means none. */
|
||||
String prefix() default "";
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.ankurm.builder.BuilderProcessor
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.ankurm.builder;
|
||||
|
||||
import javax.annotation.processing.Processor;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.DiagnosticCollector;
|
||||
import javax.tools.FileObject;
|
||||
import javax.tools.ForwardingJavaFileManager;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileManager;
|
||||
import javax.tools.JavaFileObject;
|
||||
import javax.tools.SimpleJavaFileObject;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
import javax.tools.ToolProvider;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Compiles Java source held in strings, entirely in memory, and keeps everything javac produced. */
|
||||
final class Compilation {
|
||||
|
||||
final boolean success;
|
||||
final List<Diagnostic<? extends JavaFileObject>> diagnostics;
|
||||
/** Generated .java files by binary class name. */
|
||||
final Map<String, String> generatedSources;
|
||||
/** Compiled classes by binary class name. */
|
||||
final Map<String, byte[]> classes;
|
||||
|
||||
private Compilation(boolean success, List<Diagnostic<? extends JavaFileObject>> d,
|
||||
Map<String, String> sources, Map<String, byte[]> classes) {
|
||||
this.success = success;
|
||||
this.diagnostics = d;
|
||||
this.generatedSources = sources;
|
||||
this.classes = classes;
|
||||
}
|
||||
|
||||
/** Location of the compiled GenerateBuilder annotation, so the in-memory compile can see it. */
|
||||
static String annotationClasspath() throws Exception {
|
||||
return Path.of(GenerateBuilder.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toString();
|
||||
}
|
||||
|
||||
static Compilation compile(Map<String, String> sources, List<Processor> processors, List<String> extraOptions)
|
||||
throws Exception {
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
DiagnosticCollector<JavaFileObject> diags = new DiagnosticCollector<>();
|
||||
StandardJavaFileManager std = compiler.getStandardFileManager(diags, null, StandardCharsets.UTF_8);
|
||||
Map<String, String> generated = new LinkedHashMap<>();
|
||||
Map<String, byte[]> classes = new LinkedHashMap<>();
|
||||
|
||||
JavaFileManager fm = new ForwardingJavaFileManager<>(std) {
|
||||
@Override
|
||||
public JavaFileObject getJavaFileForOutput(Location location, String className,
|
||||
JavaFileObject.Kind kind, FileObject sibling) {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
return new SimpleJavaFileObject(URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind) {
|
||||
/** javac reads generated sources back in the next round. */
|
||||
@Override
|
||||
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
|
||||
return bytes.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.OutputStream openOutputStream() {
|
||||
return new java.io.FilterOutputStream(bytes) {
|
||||
@Override
|
||||
public void close() throws java.io.IOException {
|
||||
super.close();
|
||||
if (kind == Kind.SOURCE) {
|
||||
generated.put(className, bytes.toString(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
classes.put(className, bytes.toByteArray());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
List<JavaFileObject> units = new ArrayList<>();
|
||||
sources.forEach((name, text) -> units.add(new SimpleJavaFileObject(
|
||||
URI.create("string:///" + name.replace('.', '/') + ".java"), JavaFileObject.Kind.SOURCE) {
|
||||
@Override
|
||||
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
|
||||
return text;
|
||||
}
|
||||
}));
|
||||
|
||||
List<String> options = new ArrayList<>(List.of("-classpath", annotationClasspath(), "--release", "25"));
|
||||
options.addAll(extraOptions);
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diags, options, null, units);
|
||||
if (processors != null) {
|
||||
task.setProcessors(processors);
|
||||
}
|
||||
boolean ok = task.call();
|
||||
return new Compilation(ok, diags.getDiagnostics(), generated, classes);
|
||||
}
|
||||
|
||||
String render(Diagnostic<? extends JavaFileObject> d) {
|
||||
String where = d.getSource() == null ? "" : " " + d.getSource().getName().replace("string:///", "")
|
||||
+ ":" + d.getLineNumber();
|
||||
return d.getKind() + where + ": " + d.getMessage(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ankurm.builder;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import java.util.Set;
|
||||
|
||||
/** A processor that forgot @SupportedSourceVersion and getSupportedSourceVersion(). Used to capture javac's warning. */
|
||||
@SupportedAnnotationTypes("*")
|
||||
final class LazyProcessor extends AbstractProcessor {
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package com.ankurm.builder;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import javax.tools.Diagnostic;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests the processor by running javac in memory. Doubles as the transcript writer for
|
||||
* output/09-processor-harness.txt: every line printed there is also asserted here.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class ProcessorCompileTest {
|
||||
|
||||
private static final StringBuilder TRANSCRIPT = new StringBuilder();
|
||||
|
||||
private static final String PERSON = """
|
||||
package com.acme;
|
||||
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
import java.util.List;
|
||||
|
||||
@GenerateBuilder
|
||||
public record Person(String name, int age, List<String> nicknames) { }
|
||||
""";
|
||||
|
||||
private static final String MAIN = """
|
||||
package com.acme;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
Person p = new PersonBuilder().name("Asha").age(34).build();
|
||||
System.out.println(p);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static void say(String line) {
|
||||
TRANSCRIPT.append(line).append('\n');
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void generatesBuilderAndTheGeneratedSourceCompilesInTheNextRound() throws Exception {
|
||||
Compilation c = Compilation.compile(
|
||||
Map.of("com.acme.Person", PERSON, "com.acme.Main", MAIN),
|
||||
List.of(new BuilderProcessor()), List.of());
|
||||
assertTrue(c.success, () -> c.diagnostics.toString());
|
||||
assertEquals(List.of("com.acme.PersonBuilder"), List.copyOf(c.generatedSources.keySet()));
|
||||
String src = c.generatedSources.get("com.acme.PersonBuilder");
|
||||
assertTrue(src.contains("public final class PersonBuilder"));
|
||||
assertTrue(src.contains("public PersonBuilder name(java.lang.String name)"));
|
||||
assertTrue(src.contains("public PersonBuilder addToNicknames(java.lang.String element)"));
|
||||
assertTrue(src.contains("public com.acme.Person build()"));
|
||||
assertTrue(c.classes.containsKey("com.acme.PersonBuilder"));
|
||||
assertTrue(c.classes.containsKey("com.acme.Main"), "Main uses a class that only exists after round 1");
|
||||
say("== 1. in-memory compile: Person + Main, BuilderProcessor supplied directly");
|
||||
say("success=" + c.success + " generated=" + c.generatedSources.keySet());
|
||||
say("classes=" + new java.util.TreeSet<>(c.classes.keySet()));
|
||||
say("");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void badTargetsAreRejectedWithErrorsPointingAtTheSource() throws Exception {
|
||||
Map<String, String> cases = new LinkedHashMap<>();
|
||||
cases.put("interface", """
|
||||
package com.acme;
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
@GenerateBuilder
|
||||
public interface Shape { }
|
||||
""");
|
||||
cases.put("abstract class", """
|
||||
package com.acme;
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
@GenerateBuilder
|
||||
public abstract class Animal { }
|
||||
""");
|
||||
cases.put("two constructors", """
|
||||
package com.acme;
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
@GenerateBuilder
|
||||
public class Point {
|
||||
public Point() { }
|
||||
public Point(int x) { }
|
||||
}
|
||||
""");
|
||||
cases.put("generic class", """
|
||||
package com.acme;
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
@GenerateBuilder
|
||||
public class Box<T> {
|
||||
public Box(T value) { }
|
||||
}
|
||||
""");
|
||||
say("== 2. error cases: javac diagnostics produced via Messager");
|
||||
for (var entry : cases.entrySet()) {
|
||||
String name = entry.getValue().lines().filter(l -> l.contains(" class ") || l.contains("interface ")
|
||||
|| l.contains("record ")).findFirst().orElseThrow();
|
||||
String typeName = name.replaceAll(".*(class|interface) (\\w+).*", "$2");
|
||||
Compilation c = Compilation.compile(Map.of("com.acme." + typeName, entry.getValue()),
|
||||
List.of(new BuilderProcessor()), List.of());
|
||||
assertFalse(c.success, entry.getKey());
|
||||
var errors = c.diagnostics.stream().filter(d -> d.getKind() == Diagnostic.Kind.ERROR).toList();
|
||||
assertEquals(1, errors.size(), entry.getKey());
|
||||
assertEquals(4L, errors.get(0).getLineNumber(), "error is attached to the type declaration line");
|
||||
assertTrue(c.generatedSources.isEmpty());
|
||||
say(entry.getKey() + " -> " + c.render(errors.get(0)));
|
||||
}
|
||||
say("");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void roundsAreVisibleThroughTheDebugOption() throws Exception {
|
||||
Compilation c = Compilation.compile(
|
||||
Map.of("com.acme.Person", PERSON),
|
||||
List.of(new BuilderProcessor()), List.of("-Abuilder.debug=true"));
|
||||
assertTrue(c.success);
|
||||
List<String> notes = c.diagnostics.stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.NOTE)
|
||||
.map(d -> d.getMessage(null)).toList();
|
||||
assertEquals(3, notes.size(), notes::toString);
|
||||
assertTrue(notes.get(0).startsWith("builder round 1: annotations=[com.ankurm.builder.GenerateBuilder]"));
|
||||
assertTrue(notes.get(0).endsWith("processingOver=false"));
|
||||
assertTrue(notes.get(1).startsWith("builder round 2: annotations=[]"));
|
||||
assertTrue(notes.get(1).contains("roots=[com.acme.PersonBuilder]"));
|
||||
assertTrue(notes.get(2).startsWith("builder round 3: annotations=[]"));
|
||||
assertTrue(notes.get(2).endsWith("processingOver=true"));
|
||||
say("== 3. rounds, with -Abuilder.debug=true");
|
||||
notes.forEach(ProcessorCompileTest::say);
|
||||
say("");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
void processorIsDiscoveredFromMetaInfServicesOnTheProcessorPath() throws Exception {
|
||||
Path classes = Path.of(Compilation.annotationClasspath());
|
||||
assertTrue(Files.isDirectory(classes), "expected target/classes; got " + classes);
|
||||
assertTrue(Files.exists(classes.resolve("META-INF/services/javax.annotation.processing.Processor")));
|
||||
Compilation c = Compilation.compile(
|
||||
Map.of("com.acme.Person", PERSON, "com.acme.Main", MAIN),
|
||||
null, List.of("-processorpath", classes.toString()));
|
||||
assertTrue(c.success, () -> c.diagnostics.toString());
|
||||
assertTrue(c.generatedSources.containsKey("com.acme.PersonBuilder"));
|
||||
say("== 4. no setProcessors(): discovered through META-INF/services via -processorpath");
|
||||
say("success=" + c.success + " generated=" + c.generatedSources.keySet());
|
||||
say("");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
void procNoneMeansNoGeneratedClassAndTheCallerFails() throws Exception {
|
||||
Path classes = Path.of(Compilation.annotationClasspath());
|
||||
Compilation c = Compilation.compile(
|
||||
Map.of("com.acme.Person", PERSON, "com.acme.Main", MAIN),
|
||||
null, List.of("-processorpath", classes.toString(), "-proc:none"));
|
||||
assertFalse(c.success);
|
||||
assertTrue(c.generatedSources.isEmpty());
|
||||
var errors = c.diagnostics.stream().filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.map(d -> c.render(d).lines().findFirst().orElseThrow()).toList();
|
||||
assertTrue(errors.stream().anyMatch(e -> e.contains("cannot find symbol")), errors::toString);
|
||||
say("== 5. -proc:none: the processor never runs");
|
||||
say("success=" + c.success + " generated=" + c.generatedSources.keySet());
|
||||
errors.forEach(ProcessorCompileTest::say);
|
||||
say("");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
void aProcessorWithoutSupportedSourceVersionGetsAWarning() throws Exception {
|
||||
Compilation c = Compilation.compile(
|
||||
Map.of("com.acme.Empty", "package com.acme; public class Empty { }"),
|
||||
List.of(new LazyProcessor()), List.of());
|
||||
assertTrue(c.success);
|
||||
var warnings = c.diagnostics.stream().filter(d -> d.getKind() == Diagnostic.Kind.WARNING
|
||||
|| d.getKind() == Diagnostic.Kind.MANDATORY_WARNING).map(d -> c.render(d)).toList();
|
||||
assertFalse(warnings.isEmpty(), "expected a source-version warning");
|
||||
say("== 6. a processor that forgot @SupportedSourceVersion");
|
||||
warnings.forEach(ProcessorCompileTest::say);
|
||||
say("");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void writeTranscript() throws Exception {
|
||||
String dir = System.getProperty("output.dir");
|
||||
if (dir != null) {
|
||||
Files.createDirectories(Path.of(dir));
|
||||
Files.writeString(Path.of(dir, "09-processor-harness.txt"), TRANSCRIPT.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user