Implement config compiler plugin v2 powered by annotation processing
This commit is contained in:
parent
712f059ba5
commit
56e2d5341c
|
@ -7,7 +7,7 @@ maven_group=io.gitlab.jfronny.libjf
|
|||
archive_base_name=libjf
|
||||
|
||||
dev_only_module=libjf-devutil
|
||||
non_mod_project=libjf-config-compiler-plugin
|
||||
non_mod_project=libjf-config-compiler-plugin, libjf-config-compiler-plugin-v2
|
||||
|
||||
modrinth_id=libjf
|
||||
modrinth_optional_dependencies=fabric-api
|
||||
|
@ -16,6 +16,7 @@ curseforge_optional_dependencies=fabric-api
|
|||
|
||||
fabric_version=0.68.1+1.19.3
|
||||
commons_version=1.0-SNAPSHOT
|
||||
gson_compile_version=1.1-SNAPSHOT
|
||||
modmenu_version=5.0.0-alpha.4
|
||||
|
||||
asm_version=9.4
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import io.gitlab.jfronny.scripts.*
|
||||
import javax.lang.model.element.Modifier.*
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
id("jf.maven-publish")
|
||||
id("jf.codegen")
|
||||
}
|
||||
|
||||
group = rootProject.group
|
||||
version = rootProject.version
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.frohnmeyer-wds.de/artifacts")
|
||||
maven("https://maven.fabricmc.net/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.gitlab.jfronny.gson:gson-compile-processor-core:${prop("gson_compile_version")}")
|
||||
implementation(project(":libjf-config-core-v1")) { isTransitive = false }
|
||||
implementation("org.jetbrains:annotations:23.0.0")
|
||||
implementation("io.gitlab.jfronny:commons:${prop("commons_version")}")
|
||||
implementation("io.gitlab.jfronny:commons-gson:${prop("commons_version")}")
|
||||
implementation("com.squareup:javapoet:1.13.0")
|
||||
testAnnotationProcessor(sourceSets.main.get().output)
|
||||
configurations.testAnnotationProcessor.get().extendsFrom(configurations.implementation.get())
|
||||
}
|
||||
|
||||
tasks.publish.get().dependsOn(tasks.build.get())
|
||||
rootProject.tasks.deployDebug.dependsOn(tasks.publish.get())
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
generate(project) {
|
||||
`class`("io.gitlab.jfronny.libjf.config.plugin", "BuildMetadata") {
|
||||
modifiers(PUBLIC)
|
||||
field("IS_RELEASE", project.hasProperty("release"), PUBLIC, STATIC, FINAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<JavaCompile>("compileTestJava") {
|
||||
options.compilerArgs.add("-AmodId=example-mod")
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.gitlab.jfronny.libjf.config.plugin;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
|
||||
public class Cl {
|
||||
|
||||
public static final ClassName MANIFOLD_EXTENSION = ClassName.get("manifold.ext.rt.api", "Extension");
|
||||
public static final ClassName MANIFOLD_THIS = ClassName.get("manifold.ext.rt.api", "This");
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.libjf.config.plugin;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
public record ConfigClass(TypeElement classElement, ClassName className, TypeName typeName, ClassName generatedClassName, String[] referencedConfigs) {
|
||||
public static ConfigClass of(TypeElement element, String[] referencedConfigs, boolean hasManifold) {
|
||||
ClassName className = ClassName.get(element);
|
||||
String pkg = hasManifold ? "gsoncompile.extensions." + className.packageName() + "." + className.simpleNames().get(0) : className.packageName();
|
||||
ClassName generatedClassName = ClassName.get(pkg, "JFC_" + className.simpleNames().get(0), className.simpleNames().subList(1, className.simpleNames().size()).toArray(String[]::new));
|
||||
return new ConfigClass(element, ClassName.get(element), TypeName.get(element.asType()), generatedClassName, referencedConfigs);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package io.gitlab.jfronny.libjf.config.plugin;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import io.gitlab.jfronny.commons.StringFormatter;
|
||||
import io.gitlab.jfronny.commons.throwable.Coerce;
|
||||
import io.gitlab.jfronny.gson.compile.processor.core.*;
|
||||
import io.gitlab.jfronny.gson.compile.processor.core.value.*;
|
||||
import io.gitlab.jfronny.gson.compile.processor.core.value.Properties;
|
||||
import io.gitlab.jfronny.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.libjf.config.api.v1.*;
|
||||
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
|
||||
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
|
||||
|
||||
import javax.annotation.processing.*;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.*;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.lang.model.util.ElementFilter;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_17)
|
||||
@SupportedAnnotationTypes2({JfConfig.class})
|
||||
@SupportedOptions({"modId"})
|
||||
public class ConfigProcessor extends AbstractProcessor2 {
|
||||
private Map<ClassName, TypeSpec.Builder> seen;
|
||||
private String modId = "null";
|
||||
|
||||
@Override
|
||||
public synchronized void init(ProcessingEnvironment processingEnv) {
|
||||
super.init(processingEnv);
|
||||
seen = new LinkedHashMap<>();
|
||||
if (!options.containsKey("modId")) message.printError("Lacking modId: can't set up config");
|
||||
else modId = options.get("modId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
|
||||
Set<ConfigClass> toGenerate = new LinkedHashSet<>();
|
||||
// Gather all serializable types
|
||||
annotations.stream().flatMap(s -> roundEnvironment.getElementsAnnotatedWith(s).stream()).forEach(element -> {
|
||||
JfConfig jc = element.getAnnotation(JfConfig.class);
|
||||
if (jc != null) {
|
||||
toGenerate.add(ConfigClass.of((TypeElement) element, jc.referencedConfigs(), hasManifold));
|
||||
}
|
||||
});
|
||||
// Generate adapters
|
||||
toGenerate.forEach(Coerce.<ConfigClass, ElementException>consumer(toProcess -> process(toProcess, toGenerate)).addHandler(ElementException.class, e -> e.printMessage(message)));
|
||||
for (var entry : seen.keySet().stream().collect(Collectors.groupingBy(ClassName::packageName)).entrySet()) {
|
||||
Map<List<String>, TypeSpec.Builder> known = entry.getValue().stream()
|
||||
.collect(Collectors.toMap(
|
||||
ClassName::simpleNames,
|
||||
seen::get,
|
||||
(u, v) -> u,
|
||||
() -> new TreeMap<>(StringListComparator.INSTANCE.reversed())
|
||||
));
|
||||
// Generate additional parent classes
|
||||
for (List<String> klazz : known.keySet().stream().toList()) {
|
||||
List<String> current = new LinkedList<>();
|
||||
for (String s : klazz) {
|
||||
current.add(s);
|
||||
TypeSpec.Builder builder = find(known, current);
|
||||
if (builder == null) {
|
||||
builder = TypeSpec.classBuilder(s).addModifiers(Modifier.PUBLIC);
|
||||
if (current.size() == 1 && hasManifold) builder.addAnnotation(Cl.MANIFOLD_EXTENSION);
|
||||
known.put(List.copyOf(current), builder);
|
||||
}
|
||||
if (current.size() > 1) builder.addModifiers(Modifier.STATIC);
|
||||
}
|
||||
}
|
||||
// Add to parent class
|
||||
for (var entry1 : known.entrySet()) {
|
||||
if (entry1.getKey().size() == 1) continue;
|
||||
find(known, entry1.getKey().subList(0, entry1.getKey().size() - 1)).addType(entry1.getValue().build());
|
||||
}
|
||||
// Print
|
||||
// System.out.println("Got " + known.size() + " classes");
|
||||
// for (var entry1 : known.entrySet()) {
|
||||
// System.out.println("Class " + entry.key + '.' + String.join(".", entry1.key));
|
||||
// for (TypeSpec typeSpec : entry1.value.typeSpecs) {
|
||||
// System.out.println("- " + typeSpec.name);
|
||||
// }
|
||||
// }
|
||||
// Write top-level classes
|
||||
for (var entry1 : known.entrySet()) {
|
||||
if (entry1.getKey().size() == 1) {
|
||||
JavaFile javaFile = JavaFile.builder(entry.getKey(), entry1.getValue().build())
|
||||
.skipJavaLangImports(true)
|
||||
.indent(" ")
|
||||
.build();
|
||||
try {
|
||||
javaFile.writeTo(filer);
|
||||
} catch (IOException e) {
|
||||
message.printMessage(Diagnostic.Kind.ERROR, "Could not write source: " + StringFormatter.toString(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
seen.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
private <K, V> V find(Map<List<K>, V> map, List<K> key) {
|
||||
for (var entry : map.entrySet()) if (entry.getKey().equals(key)) return entry.getValue();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void process(ConfigClass toProcess, Set<ConfigClass> other) throws ElementException {
|
||||
if (seen.containsKey(toProcess.generatedClassName())) return; // Don't process the same class more than once
|
||||
|
||||
TypeSpec.Builder spec = TypeSpec.classBuilder(toProcess.generatedClassName().simpleName())
|
||||
.addOriginatingElement(toProcess.classElement())
|
||||
.addModifiers(Modifier.PUBLIC);
|
||||
|
||||
seen.put(toProcess.generatedClassName(), spec);
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.addStatement("$T.getInstance().migrateFiles($S)", ConfigHolder.class, modId);
|
||||
code.add("INSTANCE = $T.create($S).register(builder0 -> builder0", DSL.class, modId).indent();
|
||||
for (String s : toProcess.referencedConfigs()) code.add("\n.referenceConfig($S)", s);
|
||||
process(toProcess.classElement(), code, new AtomicInteger(0));
|
||||
code.unindent().add("\n);\n");
|
||||
|
||||
spec.addField(FieldSpec.builder(double.class, "Infinity", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL).initializer("$T.POSITIVE_INFINITY", Double.class).build());
|
||||
spec.addStaticBlock(code.build());
|
||||
spec.addField(FieldSpec.builder(ConfigInstance.class, "INSTANCE", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).build());
|
||||
spec.addMethod(MethodSpec.methodBuilder("write").addModifiers(Modifier.PUBLIC, Modifier.STATIC).addCode("INSTANCE.write();").build());
|
||||
spec.addMethod(MethodSpec.methodBuilder("load").addModifiers(Modifier.PUBLIC, Modifier.STATIC).addCode("INSTANCE.load();").build());
|
||||
}
|
||||
|
||||
private void process(TypeElement source, CodeBlock.Builder code, AtomicInteger i) {
|
||||
for (TypeElement klazz : ElementFilter.typesIn(source.getEnclosedElements())) {
|
||||
Category v = klazz.getAnnotation(Category.class);
|
||||
if (v != null) {
|
||||
String name = klazz.getSimpleName().toString();
|
||||
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
||||
code.add("\n.category($S, builder$L -> builder$L", name, i.incrementAndGet(), i.get()).indent();
|
||||
for (String s : v.referencedConfigs()) code.add("\n.referencedConfigs($S)", s);
|
||||
process(klazz, code, i);
|
||||
code.unindent().add("\n)");
|
||||
}
|
||||
}
|
||||
for (VariableElement field : ElementFilter.fieldsIn(source.getEnclosedElements())) {
|
||||
if (field.getModifiers().containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC))) {
|
||||
Entry e = field.getAnnotation(Entry.class);
|
||||
if (e == null) continue;
|
||||
String name = field.getSimpleName().toString();
|
||||
TypeMirror tm = unbox(field.asType());
|
||||
code.add("\n.value($S, $T.$L, ", name, source, name);
|
||||
DeclaredType declared = TypeHelper.asDeclaredType(tm);
|
||||
if (declared != null && declared.asElement().getKind() == ElementKind.ENUM) {
|
||||
code.add("$T.class, () -> $T.$L, value -> $T.$L = value", declared, source, name, source, name);
|
||||
} else if (tm.toString().equals(String.class.getCanonicalName())) {
|
||||
code.add("() -> $T.$L, value -> $T.$L = value", source, name, source, name);
|
||||
} else switch (tm.getKind()) {
|
||||
case BOOLEAN -> code.add("() -> $T.$L, value -> $T.$L = value", source, name, source, name);
|
||||
case BYTE -> code.add("$L, $L, () -> (int) $T.$L, value -> $T.$L = (byte) (int) value", e.min(), e.max(), source, name, source, name);
|
||||
case SHORT -> code.add("$L, $L, () -> (int) $T.$L, value -> $T.$L = (short) (int) value", e.min(), e.max(), source, name, source, name);
|
||||
case CHAR -> code.add("$L, $L, () -> (int) $T.$L, value -> $T.$L = (char) (int) value", e.min(), e.max(), source, name, source, name);
|
||||
case INT, LONG, FLOAT, DOUBLE -> code.add("$L, $L, () -> $T.$L, value -> $T.$L = value", e.min(), e.max(), source, name, source, name);
|
||||
default -> {
|
||||
code.add("$L, $L, ", e.min(), e.max());
|
||||
if (tm instanceof DeclaredType dt && !dt.getTypeArguments().isEmpty()) {
|
||||
code.add("$T.ofClass(new $T<$T>() {}.getType())", Type.class, TypeToken.class, tm);
|
||||
} else code.add("$T.class", tm);
|
||||
code.add(", $L, () -> $T.$L, value -> $T.$L = value", e.width(), source, name, source, name);
|
||||
}
|
||||
}
|
||||
code.add(")");
|
||||
}
|
||||
}
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(source.getEnclosedElements())) {
|
||||
if (method.getModifiers().containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC))) {
|
||||
String name = method.getSimpleName().toString();
|
||||
Preset p = method.getAnnotation(Preset.class);
|
||||
if (p != null) code.add("\n.addPreset($S, $T::$L)", name, source, name);
|
||||
Verifier v = method.getAnnotation(Verifier.class);
|
||||
if (v != null) code.add("\n.addVerifier($T::$L)", source, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TypeMirror unbox(TypeMirror type) {
|
||||
try {
|
||||
return types.unboxedType(type);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
io.gitlab.jfronny.libjf.config.plugin.ConfigProcessor
|
|
@ -0,0 +1,38 @@
|
|||
package io.gitlab.jfronny.libjf.config.plugin.test;
|
||||
|
||||
import io.gitlab.jfronny.libjf.config.api.v1.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@JfConfig
|
||||
public class PluginTest {
|
||||
@Entry public static String yes;
|
||||
@Entry(min = 12, max = 15) public static double ae;
|
||||
@Entry public static byte aByte = 12;
|
||||
@Entry public static short aShort = 15;
|
||||
@Entry public static char ch = 12;
|
||||
@Entry public static int anInt = 64;
|
||||
@Entry public static long lonk = 16;
|
||||
@Entry public static float f = 15.3f;
|
||||
|
||||
@Category
|
||||
public class ExampleCategory {
|
||||
@Entry public static boolean innerValue;
|
||||
@Entry public static SomeEnum eeAA;
|
||||
@Entry public static Map<String, String> sheh;
|
||||
}
|
||||
|
||||
@Verifier
|
||||
public static void verify() {
|
||||
|
||||
}
|
||||
|
||||
@Preset
|
||||
public static void applyMeme() {
|
||||
|
||||
}
|
||||
|
||||
enum SomeEnum {
|
||||
Some, Entries
|
||||
}
|
||||
}
|
|
@ -93,7 +93,7 @@ public class ConfigInjectClassTransformer extends ClassVisitor {
|
|||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
ensureCategory();
|
||||
if (isConfig()) {
|
||||
if ("<clinit>".equals(name)) {
|
||||
if (CLINIT.equals(name)) {
|
||||
initFound = true;
|
||||
return new ClInitInjectVisitor(
|
||||
super.visitMethod(access, name, descriptor, signature, exceptions),
|
||||
|
|
|
@ -22,3 +22,4 @@ include("libjf-unsafe-v0")
|
|||
include("libjf-web-v0")
|
||||
|
||||
include("libjf-config-compiler-plugin")
|
||||
include("libjf-config-compiler-plugin-v2")
|
||||
|
|
Loading…
Reference in New Issue