From 6ff87be213eb46e32b25c6d2ff3be8d1f1e830ed Mon Sep 17 00:00:00 2001 From: zaaarf Date: Wed, 1 Mar 2023 21:29:19 +0100 Subject: feat: added @FindField and @FindMethod (untested) --- src/main/java/ftbsc/lll/processor/ASTUtils.java | 54 +++++++++-- .../java/ftbsc/lll/processor/LilleroProcessor.java | 107 ++++++++++++++++++--- .../ftbsc/lll/processor/annotations/FindField.java | 21 ++++ .../lll/processor/annotations/FindMethod.java | 23 +++++ .../ftbsc/lll/processor/annotations/Injector.java | 8 +- .../ftbsc/lll/processor/annotations/Target.java | 4 +- 6 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 src/main/java/ftbsc/lll/processor/annotations/FindField.java create mode 100644 src/main/java/ftbsc/lll/processor/annotations/FindMethod.java diff --git a/src/main/java/ftbsc/lll/processor/ASTUtils.java b/src/main/java/ftbsc/lll/processor/ASTUtils.java index 3c6854f..38e50e2 100644 --- a/src/main/java/ftbsc/lll/processor/ASTUtils.java +++ b/src/main/java/ftbsc/lll/processor/ASTUtils.java @@ -7,9 +7,12 @@ import com.squareup.javapoet.TypeName; import ftbsc.lll.tools.DescriptorBuilder; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import java.lang.annotation.Annotation; +import java.util.List; +import java.util.stream.Collectors; /** * Collection of static utils that didn't really fit into the main class. @@ -18,19 +21,17 @@ public class ASTUtils { /** * Finds, among the methods of a class cl, the one annotated with ann, and tries to build * a {@link ExecutableElement} from it. - * In case of multiple occurrences, only the first one is returned. - * No check existance check is performed within the method. * @param cl the {@link ExecutableElement} for the class containing the desired method * @param ann the {@link Class} corresponding to the desired annotation - * @return the {@link MethodSpec} representing the desired method + * @return a {@link List} of {@link MethodSpec}s annotated with the given annotation + * @since 0.2.0 */ - @SuppressWarnings("OptionalGetWithoutIsPresent") - public static ExecutableElement findAnnotatedMethod(TypeElement cl, Class ann) { - return (ExecutableElement) cl.getEnclosedElements() + public static List findAnnotatedMethods(TypeElement cl, Class ann) { + return cl.getEnclosedElements() .stream() .filter(e -> e.getAnnotation(ann) != null) - .findFirst() - .get(); //will never be null so can ignore warning + .map(e -> (ExecutableElement) e) + .collect(Collectors.toList()); } /** @@ -85,4 +86,41 @@ public class ASTUtils { methodSignature.append(descriptorFromType(m.getReturnType())); return methodSignature.toString(); } + + /** + * Maps a {@link javax.lang.model.element.Modifier} to its reflective + * {@link java.lang.reflect.Modifier} equivalent. + * @param m the {@link Modifier} to map + * @return an integer representing the modifier + * @see java.lang.reflect.Modifier + * @since 0.2.0 + */ + public static int mapModifier(Modifier m) { + switch(m) { + case PUBLIC: + return java.lang.reflect.Modifier.PUBLIC; + case PROTECTED: + return java.lang.reflect.Modifier.PROTECTED; + case PRIVATE: + return java.lang.reflect.Modifier.PRIVATE; + case ABSTRACT: + return java.lang.reflect.Modifier.ABSTRACT; + case STATIC: + return java.lang.reflect.Modifier.STATIC; + case FINAL: + return java.lang.reflect.Modifier.FINAL; + case TRANSIENT: + return java.lang.reflect.Modifier.TRANSIENT; + case VOLATILE: + return java.lang.reflect.Modifier.VOLATILE; + case SYNCHRONIZED: + return java.lang.reflect.Modifier.SYNCHRONIZED; + case NATIVE: + return java.lang.reflect.Modifier.NATIVE; + case STRICTFP: + return java.lang.reflect.Modifier.STRICT; + default: + return 0; + } + } } diff --git a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java index ed46998..d4a6ae7 100644 --- a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java +++ b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java @@ -2,10 +2,9 @@ package ftbsc.lll.processor; import com.squareup.javapoet.*; import ftbsc.lll.IInjector; -import ftbsc.lll.processor.annotations.Injector; -import ftbsc.lll.processor.annotations.Patch; -import ftbsc.lll.processor.annotations.Target; -import ftbsc.lll.tools.DescriptorBuilder; +import ftbsc.lll.processor.annotations.*; +import ftbsc.lll.proxies.FieldProxy; +import ftbsc.lll.proxies.MethodProxy; import ftbsc.lll.tools.SrgMapper; import javax.annotation.processing.*; @@ -19,16 +18,17 @@ import javax.tools.FileObject; import javax.tools.JavaFileObject; import javax.tools.StandardLocation; import java.io.*; -import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import static ftbsc.lll.processor.ASTUtils.descriptorFromMethodSpec; -import static ftbsc.lll.processor.ASTUtils.findAnnotatedMethod; +import static ftbsc.lll.processor.ASTUtils.*; /** * The actual annotation processor behind the magic. @@ -78,7 +78,7 @@ public class LilleroProcessor extends AbstractProcessor { /** * This checks whether a given class contains the requirements to be parsed into a Lillero injector. * It must have at least one method annotated with {@link Target}, and one method annotated with {@link Injector} - * that must be public, static and take in a ClassNode and MethodNode from ObjectWeb's ASM library. + * that must take in a ClassNode and MethodNode from ObjectWeb's ASM library. * @param elem the element to check. * @return whether it can be converted into a valid {@link IInjector}. */ @@ -90,8 +90,6 @@ public class LilleroProcessor extends AbstractProcessor { List params = ((ExecutableType) e.asType()).getParameterTypes(); return e.getAnnotation(Injector.class) != null && e.getAnnotation(Target.class) == null - && e.getModifiers().contains(Modifier.PUBLIC) - && e.getModifiers().contains(Modifier.STATIC) && params.size() == 2 && processingEnv.getTypeUtils().isSameType(params.get(0), classNodeType) && processingEnv.getTypeUtils().isSameType(params.get(1), methodNodeType); @@ -129,14 +127,14 @@ public class LilleroProcessor extends AbstractProcessor { } //pretty sure class names de facto never change but better safe than sorry String targetClassSrgName = mapper.getMcpClass(targetClassCanonicalName.replace('.', '/')); - ExecutableElement targetMethod = findAnnotatedMethod(cl, Target.class); + ExecutableElement targetMethod = findAnnotatedMethods(cl, Target.class).get(0); //there should only be one String targetMethodDescriptor = descriptorFromMethodSpec(targetMethod); String targetMethodSrgName = mapper.getSrgMember( targetClassCanonicalName.replace('.', '/'), targetMethod.getSimpleName() + " " + targetMethodDescriptor ); - ExecutableElement injectorMethod = findAnnotatedMethod(cl, Injector.class); + ExecutableElement injectorMethod = findAnnotatedMethods(cl, Injector.class).get(0); //there should only be one Element packageElement = cl.getEnclosingElement(); while (packageElement.getKind() != ElementKind.PACKAGE) @@ -149,6 +147,7 @@ public class LilleroProcessor extends AbstractProcessor { MethodSpec inject = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .returns(void.class) + .addAnnotation(Override.class) .addParameter(ParameterSpec.builder( TypeName.get(processingEnv .getElementUtils() @@ -157,17 +156,24 @@ public class LilleroProcessor extends AbstractProcessor { TypeName.get(processingEnv .getElementUtils() .getTypeElement("org.objectweb.asm.tree.MethodNode").asType()), "main").build()) - .addStatement("$T." + injectorMethod.getSimpleName() + "(clazz, main)", TypeName.get(cl.asType())) + .addStatement("super." + injectorMethod.getSimpleName() + "(clazz, main)", TypeName.get(cl.asType())) .build(); + List injectorModifiers = new ArrayList<>(); + injectorModifiers.add(Modifier.PUBLIC); + if(cl.getModifiers().contains(Modifier.ABSTRACT)) + injectorModifiers.add(Modifier.ABSTRACT); //so we dont actually have to instantiate the stubs + TypeSpec injectorClass = TypeSpec.classBuilder(injectorSimpleClassName) - .addModifiers(Modifier.PUBLIC) + .addModifiers(injectorModifiers.toArray(new Modifier[0])) + .superclass(cl.asType()) .addSuperinterface(ClassName.get(IInjector.class)) .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString())) .addMethod(buildStringReturnMethod("reason", ann.reason())) .addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.'))) .addMethod(buildStringReturnMethod("methodName", targetMethodSrgName)) .addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor)) + .addMethods(generateRequestedProxies(cl, mapper)) .addMethod(inject) .build(); @@ -194,11 +200,84 @@ public class LilleroProcessor extends AbstractProcessor { private static MethodSpec buildStringReturnMethod(String name, String returnString) { return MethodSpec.methodBuilder(name) .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) .returns(String.class) .addStatement("return $S", returnString) .build(); } + /** + * Finds any method annotated with {@link FindMethod} or {@link FindField} within the given + * class, and builds the {@link MethodSpec} necessary for building it. + * @param cl the class to search + * @return a {@link List} of method specs + * @since 0.2.0 + */ + private List generateRequestedProxies(TypeElement cl, SrgMapper mapper) { + List generated = new ArrayList<>(); + findAnnotatedMethods(cl, FindMethod.class) + .stream() + .filter(m -> !m.getModifiers().contains(Modifier.STATIC)) //skip static stuff as we can't override it + .filter(m -> !m.getModifiers().contains(Modifier.FINAL)) //in case someone is trying to be funny + .forEach(m -> { + FindMethod ann = m.getAnnotation(FindMethod.class); + String targetMethodName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name(); + try { + MethodSpec.Builder b = MethodSpec.overriding(m); + Method targetMethod = ann.parent().getMethod( + targetMethodName, + ann.params() + ); + b.addStatement("$T bd = $T.builder($S)", + MethodProxy.Builder.class, + MethodProxy.class, + targetMethodName + ); + b.addStatement("bd.setParent($S)", targetMethod.getDeclaringClass().getCanonicalName()); + b.addStatement("bd.setModifier($L)", targetMethod.getModifiers()); + for(Class p : targetMethod.getParameterTypes()) + b.addStatement("bd.addParameter($T.class)", p); + b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType()); + b.addStatement("return bd.build()"); + generated.add(b.build()); + } catch(NoSuchMethodException e) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Method not found: " + targetMethodName + ); + } + }); + findAnnotatedMethods(cl, FindField.class) + .stream() + .filter(m -> !m.getModifiers().contains(Modifier.STATIC)) + .filter(m -> !m.getModifiers().contains(Modifier.FINAL)) + .forEach(m -> { + FindField ann = m.getAnnotation(FindField.class); + String targetFieldName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name(); + try { + MethodSpec.Builder b = MethodSpec.overriding(m); + Field targetField = ann.parent().getField(targetFieldName); + b.addStatement("$T bd = $T.builder($S)", + FieldProxy.Builder.class, + FieldProxy.class, + targetFieldName + ); + b.addStatement("bd.setParent($S)", targetField.getDeclaringClass().getCanonicalName()); + b.addStatement("bd.setModifier($L)", targetField.getModifiers()); + b.addStatement("bd.setType($T.class)", targetField.getType()); + b.addStatement("return bd.build()"); + generated.add(b.build()); + } catch(NoSuchFieldException e) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Field not found: " + targetFieldName + " in class " + ann.parent().getCanonicalName() + ); + } + }); + return generated; + } + + /** * Generates the Service Provider file for the generated injectors. */ diff --git a/src/main/java/ftbsc/lll/processor/annotations/FindField.java b/src/main/java/ftbsc/lll/processor/annotations/FindField.java new file mode 100644 index 0000000..9b5a824 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/annotations/FindField.java @@ -0,0 +1,21 @@ +package ftbsc.lll.processor.annotations; + +import ftbsc.lll.proxies.FieldProxy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Overrides the marked method in the Injector, having the + * implementation return a built {@link FieldProxy}. + * @implNote if name is omitted, name of the annotated method + * is used. + * @since 0.2.0 + */ +@Retention(RetentionPolicy.CLASS) +@java.lang.annotation.Target(ElementType.METHOD) +public @interface FindField { + Class parent(); + String name() default ""; +} diff --git a/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java new file mode 100644 index 0000000..bf93442 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java @@ -0,0 +1,23 @@ +package ftbsc.lll.processor.annotations; + +import ftbsc.lll.proxies.MethodProxy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Overrides the marked method in the Injector, having the + * implementation return a built {@link MethodProxy} with + * the specified parameters. + * @implNote if name is omitted, the name of the annotated + * method is used. + * @since 0.2.0 + */ +@Retention(RetentionPolicy.CLASS) +@java.lang.annotation.Target(ElementType.METHOD) +public @interface FindMethod { + Class parent(); + String name() default ""; + Class[] params(); +} diff --git a/src/main/java/ftbsc/lll/processor/annotations/Injector.java b/src/main/java/ftbsc/lll/processor/annotations/Injector.java index 1e2d8dd..f5e22aa 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Injector.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Injector.java @@ -6,10 +6,10 @@ import java.lang.annotation.RetentionPolicy; /** * Marks a method as the injector method for purposes of generation. - * The method itself should be {@code public static}, and take in a ClassNode and MethodNode - * (from the ObjectWeb ASM library) as parameters. It will be discarded otherwise. - * It will also be discarded unless the containing class is not annotated with {@link Patch} - * and no other method within the class is annotated with {@link Target}. + * The method itself should take in a ClassNode and MethodNode (from the ObjectWeb ASM library) + * as parameters. It will be discarded otherwise. + * It will also be discarded unless the containing class is annotated with {@link Patch} + * and another method within the class is annotated with {@link Target}. * @see Patch * @see Target */ diff --git a/src/main/java/ftbsc/lll/processor/annotations/Target.java b/src/main/java/ftbsc/lll/processor/annotations/Target.java index 3a1d209..885bd9c 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Target.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Target.java @@ -8,8 +8,8 @@ import java.lang.annotation.RetentionPolicy; * Marks a method as the target method. * The method itself should have the same name, return type and parameters as the desired * Minecraft method. - * It will also be discarded unless the containing class is not annotated with {@link Patch} - * and no other method within the class is annotated with {@link Injector}. + * It will also be discarded unless the containing class is annotated with {@link Patch} + * and another method within the class is annotated with {@link Injector}. * @see Patch * @see Injector */ -- cgit v1.2.3-56-ga3b1