diff --git a/gradle.properties b/gradle.properties index 62539aa..33da943 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/libjf-config-compiler-plugin-v2/build.gradle.kts b/libjf-config-compiler-plugin-v2/build.gradle.kts new file mode 100644 index 0000000..b9fd08c --- /dev/null +++ b/libjf-config-compiler-plugin-v2/build.gradle.kts @@ -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("compileTestJava") { + options.compilerArgs.add("-AmodId=example-mod") +} \ No newline at end of file diff --git a/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/Cl.java b/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/Cl.java new file mode 100644 index 0000000..df011b9 --- /dev/null +++ b/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/Cl.java @@ -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"); +} diff --git a/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/ConfigClass.java b/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/ConfigClass.java new file mode 100644 index 0000000..9737994 --- /dev/null +++ b/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/ConfigClass.java @@ -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); + } +} diff --git a/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/ConfigProcessor.java b/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/ConfigProcessor.java new file mode 100644 index 0000000..463c622 --- /dev/null +++ b/libjf-config-compiler-plugin-v2/src/main/java/io/gitlab/jfronny/libjf/config/plugin/ConfigProcessor.java @@ -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 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 annotations, RoundEnvironment roundEnvironment) { + Set 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.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, 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 klazz : known.keySet().stream().toList()) { + List 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 V find(Map, V> map, List key) { + for (var entry : map.entrySet()) if (entry.getKey().equals(key)) return entry.getValue(); + return null; + } + + private void process(ConfigClass toProcess, Set 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; + } + } +} diff --git a/libjf-config-compiler-plugin-v2/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/libjf-config-compiler-plugin-v2/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..2f54cdc --- /dev/null +++ b/libjf-config-compiler-plugin-v2/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.gitlab.jfronny.libjf.config.plugin.ConfigProcessor \ No newline at end of file diff --git a/libjf-config-compiler-plugin-v2/src/test/java/io/gitlab/jfronny/libjf/config/plugin/test/PluginTest.java b/libjf-config-compiler-plugin-v2/src/test/java/io/gitlab/jfronny/libjf/config/plugin/test/PluginTest.java new file mode 100644 index 0000000..abfa073 --- /dev/null +++ b/libjf-config-compiler-plugin-v2/src/test/java/io/gitlab/jfronny/libjf/config/plugin/test/PluginTest.java @@ -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 sheh; + } + + @Verifier + public static void verify() { + + } + + @Preset + public static void applyMeme() { + + } + + enum SomeEnum { + Some, Entries + } +} diff --git a/libjf-config-compiler-plugin/src/main/java/io/gitlab/jfronny/libjf/config/plugin/asm/ConfigInjectClassTransformer.java b/libjf-config-compiler-plugin/src/main/java/io/gitlab/jfronny/libjf/config/plugin/asm/ConfigInjectClassTransformer.java index e11de61..35e1688 100644 --- a/libjf-config-compiler-plugin/src/main/java/io/gitlab/jfronny/libjf/config/plugin/asm/ConfigInjectClassTransformer.java +++ b/libjf-config-compiler-plugin/src/main/java/io/gitlab/jfronny/libjf/config/plugin/asm/ConfigInjectClassTransformer.java @@ -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 ("".equals(name)) { + if (CLINIT.equals(name)) { initFound = true; return new ClInitInjectVisitor( super.visitMethod(access, name, descriptor, signature, exceptions), diff --git a/settings.gradle.kts b/settings.gradle.kts index 877bd40..df5e175 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,3 +22,4 @@ include("libjf-unsafe-v0") include("libjf-web-v0") include("libjf-config-compiler-plugin") +include("libjf-config-compiler-plugin-v2")