package io.gitlab.jfronny.libjf.config.plugin.asm; import io.gitlab.jfronny.libjf.config.api.v1.Category; import io.gitlab.jfronny.libjf.config.api.v1.JfConfig; import io.gitlab.jfronny.libjf.config.plugin.BuildMetadata; import io.gitlab.jfronny.libjf.config.plugin.util.ClInitInjectVisitor; import io.gitlab.jfronny.libjf.config.plugin.util.GeneratorAdapter2; import io.gitlab.jfronny.libjf.config.plugin.asm.value.DiscoveredValue; import org.gradle.api.GradleException; import org.objectweb.asm.*; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; import java.util.*; import static io.gitlab.jfronny.libjf.config.plugin.asm.value.KnownTypes.*; import static org.objectweb.asm.Opcodes.*; import static org.objectweb.asm.Type.*; public class ConfigInjectClassTransformer extends ClassVisitor { // Field names public static final String PREFIX = "libjf$config$"; public static final String INIT_METHOD = PREFIX + "clinit"; public static final String REGISTER_METHOD = PREFIX + "register"; public static final String BUILDER_ROOT = PREFIX + "root"; public static final String CLINIT = ""; // Transformation metadata public final String modId; public Type current; private final Set knownConfigClasses; // Transformer state private final List categories = new LinkedList<>(); private final List referencedConfigs = new LinkedList<>(); private final List presets = new LinkedList<>(); private final List verifiers = new LinkedList<>(); private final List values = new LinkedList<>(); private boolean initFound = false; private TransformerMode mode; public boolean isCategory() { return mode == TransformerMode.CONFIG_CATEGORY || isConfig(); } public void ensureCategory() { if (!isCategory()) throw new NotAConfigClassException(); } public boolean isConfig() { return mode == TransformerMode.CONFIG_ROOT; } public boolean isOther() { return mode == TransformerMode.OTHER; } public ConfigInjectClassTransformer(String modId, ClassVisitor cw, Set knownConfigClasses) { super(ASM9, cw); this.modId = modId; this.knownConfigClasses = knownConfigClasses; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.current = Type.getType('L' + name + ';'); mode = TransformerMode.OTHER; } @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { if (mode == null) throw new IllegalStateException("Attempted to visit annotation before class"); if (descriptor.equals(Type.getDescriptor(Category.class)) && isOther()) { mode = TransformerMode.CONFIG_CATEGORY; return new AnnotationMetaGatheringVisitor(super.visitAnnotation(descriptor, visible), referencedConfigs); } else if (descriptor.equals(Type.getDescriptor(JfConfig.class)) && isOther()) { mode = TransformerMode.CONFIG_ROOT; knownConfigClasses.add(current); return new AnnotationMetaGatheringVisitor(super.visitAnnotation(descriptor, visible), referencedConfigs); } else { return super.visitAnnotation(descriptor, visible); } } @Override public void visitNestMember(String nestMember) { ensureCategory(); categories.add(nestMember); super.visitNestMember(nestMember); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { ensureCategory(); if (isConfig()) { if (CLINIT.equals(name)) { initFound = true; return new ClInitInjectVisitor( super.visitMethod(access, name, descriptor, signature, exceptions), current.getInternalName(), INIT_METHOD, "()V" ); } } if (name.startsWith(PREFIX)) { throw new GradleException("This class declares methods generated by this plugin manually. Do not transform classes twice!"); } if ((access & ACC_STATIC) == ACC_STATIC) { // Possibly add verifier or preset return new MethodMetaGatheringVisitor( super.visitMethod(access, name, descriptor, signature, exceptions), name, presets, verifiers); } return super.visitMethod(access, name, descriptor, signature, exceptions); } @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { ensureCategory(); if ((access & ACC_STATIC) == ACC_STATIC) { return new FieldMetaGatheringVisitor(super.visitField(access, name, descriptor, signature, value), name, values, Type.getType(descriptor)); } return super.visitField(access, name, descriptor, signature, value); } @Override public void visitEnd() { ensureCategory(); if (isConfig()) { if (!initFound) { // Generate if missing GeneratorAdapter2 m = method(ACC_PRIVATE | ACC_STATIC, CLINIT, "()V", null, null); m.invokeStatic(current, new Method(INIT_METHOD, "()V")); m.returnValue(); m.endMethod(); } // Generate main method { GeneratorAdapter2 m = method(ACC_PRIVATE | ACC_STATIC, INIT_METHOD, "()V", null, null); m.comment("Migrate files from old paths"); m.invokeIStatic(CONFIG_HOLDER_TYPE, new Method("getInstance", CONFIG_HOLDER_TYPE, new Type[0])); m.push(modId); m.invokeInterface(CONFIG_HOLDER_TYPE, new Method("migrateFiles", "(Ljava/lang/String;)V")); m.comment("Invoke DSL, continue in " + BUILDER_ROOT); m.push(modId); m.invokeIStatic(DSL_TYPE, new Method("create", DSL_DEFAULTED_TYPE, new Type[]{STRING_TYPE})); m.λ("apply", getMethodDescriptor(CONFIG_BUILDER_FUNCTION_TYPE), getMethodType(CONFIG_BUILDER_TYPE, CONFIG_BUILDER_TYPE), current.getInternalName(), BUILDER_ROOT); m.invokeInterface(DSL_DEFAULTED_TYPE, new Method("config", CONFIG_INSTANCE_TYPE, new Type[]{CONFIG_BUILDER_FUNCTION_TYPE})); m.pop(); m.returnValue(); m.endMethod(); } // Generate register method for impl // This method does nothing but is needed to properly implement the interface // This also ensures the static initializer is called { GeneratorAdapter2 m = method(ACC_PUBLIC | ACC_STATIC, REGISTER_METHOD, getMethodDescriptor(VOID_TYPE, DSL_DEFAULTED_TYPE), null, null); m.comment("This placeholder method is referenced in the FMJ to ensure the static initializer is called"); m.returnValue(); m.endMethod(); } } { Type builderType = isConfig() ? CONFIG_BUILDER_TYPE : CATEGORY_BUILDER_TYPE; GeneratorAdapter2 m = method(ACC_PRIVATE | ACC_STATIC, BUILDER_ROOT, getMethodDescriptor(builderType, builderType), null, null); m.loadArg(0); for (String name : referencedConfigs) { m.comment("Reference the config of \"" + name + "\""); m.push(name); dslInvoke(m, "referenceConfig", CATEGORY_BUILDER_TYPE, STRING_TYPE); } for (String name : categories) { m.comment("Register the category defined in " + name); m.push(camelCase(name.substring(current.getInternalName().length()))); m.λ("apply", getMethodDescriptor(CATEGORY_BUILDER_FUNCTION_TYPE), getMethodType(CATEGORY_BUILDER_TYPE, CATEGORY_BUILDER_TYPE), name, BUILDER_ROOT); dslInvoke(m, "category", CATEGORY_BUILDER_TYPE, STRING_TYPE, CATEGORY_BUILDER_FUNCTION_TYPE); } for (String verifier : verifiers) { m.comment("Register the verifier \"" + verifier + "\""); m.runnable(current.getInternalName(), verifier); dslInvoke(m, "addVerifier", CATEGORY_BUILDER_TYPE, RUNNABLE_TYPE); } for (String preset : presets) { m.comment("Register the preset \"" + preset + "\""); m.push(preset); m.runnable(current.getInternalName(), preset); dslInvoke(m, "addPreset", CATEGORY_BUILDER_TYPE, STRING_TYPE, RUNNABLE_TYPE); } for (DiscoveredValue value : values) { m.comment("Register the value \"" + value + "\""); value.generateRegistration(m, this); } m.returnValue(); m.endMethod(); for (DiscoveredValue value : values) { value.generateλ(this); } } super.visitEnd(); } public void dslInvoke(GeneratorAdapter m, String name, Type returnType, Type... arguments) { Type builderType = isConfig() ? CONFIG_BUILDER_TYPE : CATEGORY_BUILDER_TYPE; m.invokeInterface(builderType, new Method(name, returnType, arguments)); if (isConfig()) m.checkCast(CONFIG_BUILDER_TYPE); } private String camelCase(String s) { return Character.toLowerCase(s.charAt(0)) + s.substring(1); } public GeneratorAdapter2 method(int access, String name, String descriptor, String signature, String[] exceptions) { if (BuildMetadata.IS_RELEASE && !name.equals(CLINIT)) access |= ACC_SYNTHETIC; return new GeneratorAdapter2(super.visitMethod(access, name, descriptor, signature, exceptions), access, name, descriptor); } }