feat: added @FindField and @FindMethod (untested)

This commit is contained in:
zaaarf 2023-03-01 21:29:19 +01:00
parent 60bd356b5d
commit 6ff87be213
No known key found for this signature in database
GPG key ID: 82240E075E31FA4C
6 changed files with 189 additions and 28 deletions

View file

@ -7,9 +7,12 @@ import com.squareup.javapoet.TypeName;
import ftbsc.lll.tools.DescriptorBuilder; import ftbsc.lll.tools.DescriptorBuilder;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeMirror;
import java.lang.annotation.Annotation; 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. * 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 * Finds, among the methods of a class cl, the one annotated with ann, and tries to build
* a {@link ExecutableElement} from it. * 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 cl the {@link ExecutableElement} for the class containing the desired method
* @param ann the {@link Class} corresponding to the desired annotation * @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 List<ExecutableElement> findAnnotatedMethods(TypeElement cl, Class<? extends Annotation> ann) {
public static ExecutableElement findAnnotatedMethod(TypeElement cl, Class<? extends Annotation> ann) { return cl.getEnclosedElements()
return (ExecutableElement) cl.getEnclosedElements()
.stream() .stream()
.filter(e -> e.getAnnotation(ann) != null) .filter(e -> e.getAnnotation(ann) != null)
.findFirst() .map(e -> (ExecutableElement) e)
.get(); //will never be null so can ignore warning .collect(Collectors.toList());
} }
/** /**
@ -85,4 +86,41 @@ public class ASTUtils {
methodSignature.append(descriptorFromType(m.getReturnType())); methodSignature.append(descriptorFromType(m.getReturnType()));
return methodSignature.toString(); 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;
}
}
} }

View file

@ -2,10 +2,9 @@ package ftbsc.lll.processor;
import com.squareup.javapoet.*; import com.squareup.javapoet.*;
import ftbsc.lll.IInjector; import ftbsc.lll.IInjector;
import ftbsc.lll.processor.annotations.Injector; import ftbsc.lll.processor.annotations.*;
import ftbsc.lll.processor.annotations.Patch; import ftbsc.lll.proxies.FieldProxy;
import ftbsc.lll.processor.annotations.Target; import ftbsc.lll.proxies.MethodProxy;
import ftbsc.lll.tools.DescriptorBuilder;
import ftbsc.lll.tools.SrgMapper; import ftbsc.lll.tools.SrgMapper;
import javax.annotation.processing.*; import javax.annotation.processing.*;
@ -19,16 +18,17 @@ import javax.tools.FileObject;
import javax.tools.JavaFileObject; import javax.tools.JavaFileObject;
import javax.tools.StandardLocation; import javax.tools.StandardLocation;
import java.io.*; import java.io.*;
import java.lang.annotation.Annotation; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static ftbsc.lll.processor.ASTUtils.descriptorFromMethodSpec; import static ftbsc.lll.processor.ASTUtils.*;
import static ftbsc.lll.processor.ASTUtils.findAnnotatedMethod;
/** /**
* The actual annotation processor behind the magic. * 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. * 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} * 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. * @param elem the element to check.
* @return whether it can be converted into a valid {@link IInjector}. * @return whether it can be converted into a valid {@link IInjector}.
*/ */
@ -90,8 +90,6 @@ public class LilleroProcessor extends AbstractProcessor {
List<? extends TypeMirror> params = ((ExecutableType) e.asType()).getParameterTypes(); List<? extends TypeMirror> params = ((ExecutableType) e.asType()).getParameterTypes();
return e.getAnnotation(Injector.class) != null return e.getAnnotation(Injector.class) != null
&& e.getAnnotation(Target.class) == null && e.getAnnotation(Target.class) == null
&& e.getModifiers().contains(Modifier.PUBLIC)
&& e.getModifiers().contains(Modifier.STATIC)
&& params.size() == 2 && params.size() == 2
&& processingEnv.getTypeUtils().isSameType(params.get(0), classNodeType) && processingEnv.getTypeUtils().isSameType(params.get(0), classNodeType)
&& processingEnv.getTypeUtils().isSameType(params.get(1), methodNodeType); && 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 } //pretty sure class names de facto never change but better safe than sorry
String targetClassSrgName = mapper.getMcpClass(targetClassCanonicalName.replace('.', '/')); 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 targetMethodDescriptor = descriptorFromMethodSpec(targetMethod);
String targetMethodSrgName = mapper.getSrgMember( String targetMethodSrgName = mapper.getSrgMember(
targetClassCanonicalName.replace('.', '/'), targetClassCanonicalName.replace('.', '/'),
targetMethod.getSimpleName() + " " + targetMethodDescriptor 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(); Element packageElement = cl.getEnclosingElement();
while (packageElement.getKind() != ElementKind.PACKAGE) while (packageElement.getKind() != ElementKind.PACKAGE)
@ -149,6 +147,7 @@ public class LilleroProcessor extends AbstractProcessor {
MethodSpec inject = MethodSpec.methodBuilder("inject") MethodSpec inject = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
.returns(void.class) .returns(void.class)
.addAnnotation(Override.class)
.addParameter(ParameterSpec.builder( .addParameter(ParameterSpec.builder(
TypeName.get(processingEnv TypeName.get(processingEnv
.getElementUtils() .getElementUtils()
@ -157,17 +156,24 @@ public class LilleroProcessor extends AbstractProcessor {
TypeName.get(processingEnv TypeName.get(processingEnv
.getElementUtils() .getElementUtils()
.getTypeElement("org.objectweb.asm.tree.MethodNode").asType()), "main").build()) .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(); .build();
List<Modifier> 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) TypeSpec injectorClass = TypeSpec.classBuilder(injectorSimpleClassName)
.addModifiers(Modifier.PUBLIC) .addModifiers(injectorModifiers.toArray(new Modifier[0]))
.superclass(cl.asType())
.addSuperinterface(ClassName.get(IInjector.class)) .addSuperinterface(ClassName.get(IInjector.class))
.addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString())) .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString()))
.addMethod(buildStringReturnMethod("reason", ann.reason())) .addMethod(buildStringReturnMethod("reason", ann.reason()))
.addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.'))) .addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.')))
.addMethod(buildStringReturnMethod("methodName", targetMethodSrgName)) .addMethod(buildStringReturnMethod("methodName", targetMethodSrgName))
.addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor)) .addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor))
.addMethods(generateRequestedProxies(cl, mapper))
.addMethod(inject) .addMethod(inject)
.build(); .build();
@ -194,11 +200,84 @@ public class LilleroProcessor extends AbstractProcessor {
private static MethodSpec buildStringReturnMethod(String name, String returnString) { private static MethodSpec buildStringReturnMethod(String name, String returnString) {
return MethodSpec.methodBuilder(name) return MethodSpec.methodBuilder(name)
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(String.class) .returns(String.class)
.addStatement("return $S", returnString) .addStatement("return $S", returnString)
.build(); .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<MethodSpec> generateRequestedProxies(TypeElement cl, SrgMapper mapper) {
List<MethodSpec> 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. * Generates the Service Provider file for the generated injectors.
*/ */

View file

@ -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 "";
}

View file

@ -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();
}

View file

@ -6,10 +6,10 @@ import java.lang.annotation.RetentionPolicy;
/** /**
* Marks a method as the injector method for purposes of generation. * 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 * The method itself should take in a ClassNode and MethodNode (from the ObjectWeb ASM library)
* (from the ObjectWeb ASM library) as parameters. It will be discarded otherwise. * as parameters. It will be discarded otherwise.
* It will also be discarded unless the containing class is not annotated with {@link Patch} * It will also be discarded unless the containing class is annotated with {@link Patch}
* and no other method within the class is annotated with {@link Target}. * and another method within the class is annotated with {@link Target}.
* @see Patch * @see Patch
* @see Target * @see Target
*/ */

View file

@ -8,8 +8,8 @@ import java.lang.annotation.RetentionPolicy;
* Marks a method as the target method. * Marks a method as the target method.
* The method itself should have the same name, return type and parameters as the desired * The method itself should have the same name, return type and parameters as the desired
* Minecraft method. * Minecraft method.
* It will also be discarded unless the containing class is not annotated with {@link Patch} * It will also be discarded unless the containing class is annotated with {@link Patch}
* and no other method within the class is annotated with {@link Injector}. * and another method within the class is annotated with {@link Injector}.
* @see Patch * @see Patch
* @see Injector * @see Injector
*/ */