From 193db6bf7b5cc767ff88527fc20ac52e6bb7d16c Mon Sep 17 00:00:00 2001 From: zaaarf Date: Sat, 25 Mar 2023 17:17:14 +0100 Subject: [PATCH] feat: initial untested draft of version 0.5.0 compatible with lillero 0.4 --- .../lll/exceptions/NotAProxyException.java | 6 +- .../exceptions/TargetNotFoundException.java | 5 +- .../ftbsc/lll/processor/LilleroProcessor.java | 245 +++++++++--------- .../ftbsc/lll/processor/annotations/Find.java | 49 ++-- .../lll/processor/annotations/Injector.java | 14 +- ...pleInjectors.java => MultipleTargets.java} | 8 +- .../lll/processor/annotations/Patch.java | 28 +- .../lll/processor/annotations/Target.java | 16 +- .../ftbsc/lll/processor/tools/ASTUtils.java | 175 +++++++------ .../lll/processor/tools/JavaPoetUtils.java | 118 +++++++-- .../{ => containers}/ArrayContainer.java | 2 +- .../tools/containers/ClassContainer.java | 47 ++++ 12 files changed, 430 insertions(+), 283 deletions(-) rename src/main/java/ftbsc/lll/processor/annotations/{MultipleInjectors.java => MultipleTargets.java} (71%) rename src/main/java/ftbsc/lll/processor/tools/{ => containers}/ArrayContainer.java (95%) create mode 100644 src/main/java/ftbsc/lll/processor/tools/containers/ClassContainer.java diff --git a/src/main/java/ftbsc/lll/exceptions/NotAProxyException.java b/src/main/java/ftbsc/lll/exceptions/NotAProxyException.java index 4c2eeaa..1ce3c0f 100644 --- a/src/main/java/ftbsc/lll/exceptions/NotAProxyException.java +++ b/src/main/java/ftbsc/lll/exceptions/NotAProxyException.java @@ -1,8 +1,8 @@ package ftbsc.lll.exceptions; import ftbsc.lll.processor.annotations.Find; -import ftbsc.lll.proxies.FieldProxy; -import ftbsc.lll.proxies.MethodProxy; +import ftbsc.lll.proxies.impl.FieldProxy; +import ftbsc.lll.proxies.impl.MethodProxy; /** * Thrown when a method is annotated with {@link Find} but does not @@ -16,6 +16,6 @@ public class NotAProxyException extends RuntimeException { * @param method the name of the method wrongly annotated */ public NotAProxyException(String parent, String method) { - super(String.format("Annotated method %s::%s does not return a proxy!", parent, method)); + super(String.format("Annotated field %s::%s does not return a proxy!", parent, method)); } } diff --git a/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java b/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java index c82e0fc..45819bf 100644 --- a/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java +++ b/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java @@ -7,9 +7,10 @@ public class TargetNotFoundException extends RuntimeException { /** * Constructs a new target not found exception for the specified method stub. + * @param type the type of element being sought (class, method, etc.) * @param stub the stub's name (and descriptor possibly) */ - public TargetNotFoundException(String stub) { - super(String.format("Could not find member corresponding to stub: %s.", stub)); + public TargetNotFoundException(String type, String stub) { + super(String.format("Could not find target %s %s.", type, stub)); } } diff --git a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java index 02d7e27..952647d 100644 --- a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java +++ b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java @@ -8,9 +8,12 @@ import ftbsc.lll.processor.annotations.Find; import ftbsc.lll.processor.annotations.Injector; import ftbsc.lll.processor.annotations.Patch; import ftbsc.lll.processor.annotations.Target; +import ftbsc.lll.processor.tools.containers.ClassContainer; import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; -import ftbsc.lll.proxies.FieldProxy; -import ftbsc.lll.proxies.MethodProxy; +import ftbsc.lll.proxies.ProxyType; +import ftbsc.lll.proxies.impl.FieldProxy; +import ftbsc.lll.proxies.impl.MethodProxy; +import ftbsc.lll.proxies.impl.TypeProxy; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; @@ -37,7 +40,7 @@ import static ftbsc.lll.processor.tools.JavaPoetUtils.*; */ @SupportedAnnotationTypes("ftbsc.lll.processor.annotations.Patch") @SupportedSourceVersion(SourceVersion.RELEASE_8) -@SupportedOptions("mappingsFile") +@SupportedOptions({"mappingsFile", "badPracticeWarnings"}) public class LilleroProcessor extends AbstractProcessor { /** * A {@link Set} of {@link String}s that will contain the fully qualified names @@ -51,6 +54,12 @@ public class LilleroProcessor extends AbstractProcessor { */ private ObfuscationMapper mapper; + /** + * Whether the processor should issue warnings when compiling code adopting + * bad practices. + */ + public static boolean badPracticeWarnings = true; + /** * Initializes the processor with the processing environment by * setting the {@code processingEnv} field to the value of the @@ -87,6 +96,17 @@ public class LilleroProcessor extends AbstractProcessor { this.mapper = new ObfuscationMapper(new BufferedReader(new InputStreamReader(targetStream, StandardCharsets.UTF_8)).lines()); } + String warns = processingEnv.getOptions().get("badPracticeWarnings"); + if(warns == null) + badPracticeWarnings = true; + else { + try { // 0 = false, any other integer = true + int i = Integer.parseInt(warns); + badPracticeWarnings = i != 0; + } catch(NumberFormatException ignored) { + badPracticeWarnings = Boolean.parseBoolean(warns); + } + } } /** @@ -110,7 +130,7 @@ public class LilleroProcessor extends AbstractProcessor { .filter(this::isValidInjector) .collect(Collectors.toSet()); if(!validInjectors.isEmpty()) { - validInjectors.forEach(this::generateInjectors); + validInjectors.forEach(this::generateClasses); if (!this.generatedInjectors.isEmpty()) { generateServiceProvider(); return true; @@ -141,7 +161,7 @@ public class LilleroProcessor extends AbstractProcessor { && processingEnv.getTypeUtils().isSameType(params.get(1), methodNodeType); })) return true; else { - processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, //TODO orphan targets String.format("Missing valid @Injector method in @Patch class %s, skipping.", elem)); return false; } @@ -152,17 +172,16 @@ public class LilleroProcessor extends AbstractProcessor { * Basically implements the {@link IInjector} interface for you. * @param cl the {@link TypeElement} for the given class */ - private void generateInjectors(TypeElement cl) { + private void generateClasses(TypeElement cl) { //find class information Patch patchAnn = cl.getAnnotation(Patch.class); - String targetClassFQN = - findClassName( - getClassFullyQualifiedName( - patchAnn, - Patch::value, - getInnerName(patchAnn, Patch::innerClass, Patch::anonymousClassCounter) - ), this.mapper - ).replace('/', '.'); + ClassContainer targetClass = new ClassContainer( + getClassFullyQualifiedName( + patchAnn, + Patch::value, + patchAnn.className() + ), this.processingEnv, this.mapper + ); //find package information Element packageElement = cl.getEnclosingElement(); @@ -170,91 +189,118 @@ public class LilleroProcessor extends AbstractProcessor { packageElement = packageElement.getEnclosingElement(); String packageName = packageElement.toString(); - //find injector(s) and target(s) - List injectors = findAnnotatedMethods(cl, Injector.class); + //find annotated elements + List targets = findAnnotatedElement(cl, Target.class); + List injectors = findAnnotatedElement(cl, Injector.class); + List finders = findAnnotatedElement(cl, Find.class); - List targets = findAnnotatedMethods(cl, Target.class); + //initialize the constructor builder + MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder(); + + //take care of TypeProxies and FieldProxies first + for(VariableElement proxyVar : finders) { + ProxyType type = getProxyType(proxyVar); + if(type == ProxyType.METHOD) //methods will be handled later + continue; + //case-specific handling + if(type == ProxyType.TYPE) { + //find and validate + ClassContainer clazz = findClassOrFallback(targetClass, proxyVar.getAnnotation(Find.class), this.processingEnv, this.mapper); + //types can be generated with a single instruction + constructorBuilder.addStatement( + "super.$L = $T.from($S, 0, $L)", + proxyVar.getSimpleName().toString(), + TypeProxy.class, + clazz.fqnObf, //use obf name, at runtime it will be obfuscated + mapModifiers(clazz.elem.getModifiers()) + ); + } else if(type == ProxyType.FIELD) + appendMemberFinderDefinition(targetClass, proxyVar, null, constructorBuilder, this.processingEnv, this.mapper); + finders.remove(proxyVar); //remove finders that have already been processed + } //declare it once for efficiency - List targetNames = - targets.stream() + List injectorNames = + injectors.stream() .map(ExecutableElement::getSimpleName) .map(Object::toString) .collect(Collectors.toList()); //this will contain the classes to generate: the key is the class name - Map toGenerate = new HashMap<>(); + HashMap toGenerate = new HashMap<>(); - for(ExecutableElement inj : injectors) { - Injector[] minjAnn = inj.getAnnotationsByType(Injector.class); + for(ExecutableElement tg : targets) { + Target[] mtgAnn = tg.getAnnotationsByType(Target.class); int iterationNumber = 1; - for(Injector injectorAnn : minjAnn) { //java is dumb - List injectionCandidates = targets; + for(Target targetAnn : mtgAnn) { + List injectorCandidates = injectors; + List finderCandidates = finders; - if(!injectorAnn.targetName().equals("") && targetNames.contains(injectorAnn.targetName())) { - //case 1: it has a name, try to match it - injectionCandidates = - injectionCandidates + if(!targetAnn.of().equals("") && injectorNames.contains(targetAnn.of())) { + //case 1: find target by name + injectorCandidates = + injectorCandidates .stream() - .filter(i -> i.getSimpleName().contentEquals(injectorAnn.targetName())) + .filter(i -> i.getSimpleName().contentEquals(targetAnn.of())) .collect(Collectors.toList()); - } else if(targets.size() == 1) { - //case 2: there is only one target - injectionCandidates = new ArrayList<>(); - injectionCandidates.add(targets.get(0)); + finderCandidates = + finderCandidates + .stream() + .filter(i -> i.getSimpleName().contentEquals(targetAnn.of())) + .collect(Collectors.toList()); + } else if(injectors.size() == 1) { + //case 2: there is only one injector + finderCandidates = new ArrayList<>(); //no candidates + injectorCandidates = new ArrayList<>(); + injectorCandidates.add(targets.get(0)); } else { //case 3: try to match by injectTargetName - String inferredName = inj.getSimpleName() - .toString() - .replaceFirst("inject", ""); - injectionCandidates = - injectionCandidates + finderCandidates = new ArrayList<>(); //no candidates + String inferredName = "inject" + tg.getSimpleName(); + injectorCandidates = + injectorCandidates .stream() .filter(t -> t.getSimpleName().toString().equalsIgnoreCase(inferredName)) .collect(Collectors.toList()); } - ExecutableElement injectionTarget = null; - - if(injectionCandidates.size() == 1) - injectionTarget = injectionCandidates.get(0); + //throw exception if user is a moron and defined a finder and an injector with the same name + if(finderCandidates.size() != 0 && injectorCandidates.size() != 0) + throw new AmbiguousDefinitionException( + String.format("Target specified user %s, but name was used by both a finder and injector.", targetAnn.of()) + ); + else if(finderCandidates.size() == 0 && injectorCandidates.size() != 1) + throw new AmbiguousDefinitionException( + String.format("Found multiple candidate injectors for target %s::%s!", cl.getSimpleName(), tg.getSimpleName()) + ); + else if(injectorCandidates.size() == 0 && finderCandidates.size() != 1) + throw new AmbiguousDefinitionException( + String.format("Found multiple candidate finders for target %s::%s!", cl.getSimpleName(), tg.getSimpleName()) + ); else { - List params = classArrayFromAnnotation(injectorAnn, Injector::params, processingEnv.getElementUtils()); - - if(params.size() != 0) { - StringBuilder descr = new StringBuilder("("); - for(TypeMirror p : params) - descr.append(descriptorFromType(TypeName.get(p))); - descr.append(")"); - injectionCandidates = - injectionCandidates - .stream() - .filter(t -> //we care about arguments but not really about return type - descr.toString() - .split("\\)")[0] - .equalsIgnoreCase(descriptorFromExecutableElement(t).split("\\)")[0]) - ).collect(Collectors.toList()); + if(injectorCandidates.size() == 1) { + //matched an injector! + toGenerate.put( + String.format("%sInjector%d", cl.getSimpleName(), iterationNumber), + new InjectorInfo(injectorCandidates.get(0), tg) + ); + iterationNumber++; //increment is only used by injectors + } else { + //matched a finder! + VariableElement finder = finders.get(0); + Find f = finder.getAnnotation(Find.class); + appendMemberFinderDefinition(targetClass, finder, tg, constructorBuilder, this.processingEnv, this.mapper); + finders.remove(finder); //unlike injectors, finders can't apply to multiple targets } - - if(injectionCandidates.size() == 1) - injectionTarget = injectionCandidates.get(0); } - //if we haven't found it yet, it's an ambiguity - if(injectionTarget == null) - throw new AmbiguousDefinitionException(String.format("Unclear target for injector %s::%s!", cl.getSimpleName(), inj.getSimpleName())); - else toGenerate.put( - String.format("%sInjector%d", cl.getSimpleName(), iterationNumber), - new InjectorInfo(inj, injectionTarget) - ); - iterationNumber++; } } //iterate over the map and generate the classes for(String injName : toGenerate.keySet()) { String targetMethodDescriptor = descriptorFromExecutableElement(toGenerate.get(injName).target); - String targetMethodName = findMemberName(targetClassFQN, toGenerate.get(injName).target.getSimpleName().toString(), targetMethodDescriptor, this.mapper); + String targetMethodName = findMemberName(targetClass.fqnObf, toGenerate.get(injName).target.getSimpleName().toString(), targetMethodDescriptor, this.mapper); MethodSpec stubOverride = MethodSpec.overriding(toGenerate.get(injName).targetStub) .addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called") @@ -279,12 +325,12 @@ public class LilleroProcessor extends AbstractProcessor { .addModifiers(Modifier.PUBLIC) .superclass(cl.asType()) .addSuperinterface(ClassName.get(IInjector.class)) + .addMethod(constructorBuilder.build()) .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString())) - .addMethod(buildStringReturnMethod("reason", patchAnn.reason())) - .addMethod(buildStringReturnMethod("targetClass", targetClassFQN)) + .addMethod(buildStringReturnMethod("reason", toGenerate.get(injName).reason)) + .addMethod(buildStringReturnMethod("targetClass", targetClass.fqn)) .addMethod(buildStringReturnMethod("methodName", targetMethodName)) .addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor)) - .addMethods(generateRequestedProxies(cl, this.mapper)) .addMethod(stubOverride) .addMethod(inject) .build(); @@ -305,53 +351,6 @@ public class LilleroProcessor extends AbstractProcessor { } } - /** - * Finds any method annotated with {@link Find} within the given class, generates - * 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, ObfuscationMapper mapper) { - List generated = new ArrayList<>(); - findAnnotatedMethods(cl, Find.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 -> { - boolean isMethod = isMethodProxyStub(m); - Element target = findMemberFromStub(m, processingEnv); - - MethodSpec.Builder b = MethodSpec.overriding(m); - - String targetParentFQN = findClassName(((TypeElement) target.getEnclosingElement()).getQualifiedName().toString(), mapper); - String methodDescriptor = isMethod ? descriptorFromExecutableElement((ExecutableElement) target) : null; - - b.addStatement("$T bd = $T.builder($S)", - isMethod ? MethodProxy.Builder.class : FieldProxy.Builder.class, - isMethod ? MethodProxy.class : FieldProxy.class, - findMemberName(targetParentFQN, target.getSimpleName().toString(), methodDescriptor, mapper) - ); - - b.addStatement("bd.setParent($S)", targetParentFQN); - - for(Modifier mod : target.getModifiers()) - b.addStatement("bd.addModifier($L)", mapModifier(mod)); - - if(isMethod) { - ExecutableElement targetMethod = (ExecutableElement) target; - for(VariableElement p : targetMethod.getParameters()) - addTypeToProxyGenerator(b, "bd", "addParameter", p.asType()); - addTypeToProxyGenerator(b, "bd", "setReturnType", targetMethod.getReturnType()); - } else addTypeToProxyGenerator(b, "bd", "setType", target.asType()); - - b.addStatement("return bd.build()"); - - generated.add(b.build()); - }); - return generated; - } - /** * Generates the Service Provider file for the generated injectors. */ @@ -384,6 +383,11 @@ public class LilleroProcessor extends AbstractProcessor { */ public final ExecutableElement targetStub; + /** + * The reason for the injection. + */ + public final String reason; + /** * The {@link ExecutableElement} corresponding to the target method. */ @@ -397,7 +401,8 @@ public class LilleroProcessor extends AbstractProcessor { public InjectorInfo(ExecutableElement injector, ExecutableElement targetStub) { this.injector = injector; this.targetStub = targetStub; - this.target = (ExecutableElement) findMemberFromStub(targetStub, processingEnv); + this.reason = injector.getAnnotation(Injector.class).reason(); + this.target = findMethodFromStub(targetStub, processingEnv); } } } \ No newline at end of file diff --git a/src/main/java/ftbsc/lll/processor/annotations/Find.java b/src/main/java/ftbsc/lll/processor/annotations/Find.java index 5f31325..384fb60 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Find.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Find.java @@ -1,7 +1,8 @@ package ftbsc.lll.processor.annotations; -import ftbsc.lll.proxies.FieldProxy; -import ftbsc.lll.proxies.MethodProxy; +import ftbsc.lll.proxies.impl.FieldProxy; +import ftbsc.lll.proxies.impl.MethodProxy; +import ftbsc.lll.proxies.impl.TypeProxy; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -14,43 +15,39 @@ import java.lang.annotation.RetentionPolicy; * @since 0.4.0 */ @Retention(RetentionPolicy.CLASS) -@java.lang.annotation.Target(ElementType.METHOD) +@java.lang.annotation.Target(ElementType.FIELD) public @interface Find { /** * @return the {@link Class} object containing the target, or the * {@link Object} class if not specified (the {@link Class} from - * {@link Patch#value()} is instead used) + * {@link Patch#value()} is instead used). + * @since 0.5.0 */ - Class parent() default Object.class; + Class value() default Object.class; /** + * For a {@link TypeProxy}, this can be either the fully-qualified name + * to be used in place of {@link #value()} or an inner class name to append + * after a $ symbol to the already acquired fully-qualified name. + * For others, this is refers to the parent class. * @return the name of the inner class that contains the target, * defaults to empty string (not an inner class) - * @since 0.4.0 + * @since 0.5.0 */ - String parentInnerClass() default ""; + String className() default ""; /** - * @return the anonymous class counter (1 for the first, 2 for - * the second, 3 for the third...) for the class that contains - * the target, defaults to 0 (not an anonymous class) - * @since 0.4.0 - */ - int parentAnonymousClassCounter() default 0; - - /** - * The name of the class member to find. If omitted, the name of the - * annotated method will be used. + * For a {@link FieldProxy}, this is the name of the field to find. If omitted, + * it will fall back on the name of the annotated field. + * For a {@link MethodProxy} it indicates an attempt to match by name only, with + * this name. This will issue a warning unless warnings are disabled. It will fail + * and throw an exception if multiple methods with that name are found in the + * relevant class. It is generally recommended that you use a @link Target} stub + * for methods, as this can lead to unpredictable behaviour at runtime. + * It will have no effect on a {@link TypeProxy}. * @return the name of the target, will default to the empty string - * (the name of the annotated method will instead be used) + * (the name of the annotated method will instead be used). + * @since 0.5.0 */ String name() default ""; - - /** - * Only use if the target is a method. - * @return a list of the parameters of the method, will default to empty - * array (in that case, an attempt will be made to match a method without - * args first) - */ - Class[] params() default {}; } diff --git a/src/main/java/ftbsc/lll/processor/annotations/Injector.java b/src/main/java/ftbsc/lll/processor/annotations/Injector.java index 4b74961..8cebd24 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Injector.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Injector.java @@ -16,19 +16,11 @@ import java.lang.annotation.RetentionPolicy; * @see Target */ @Retention(RetentionPolicy.CLASS) -@Repeatable(MultipleInjectors.class) @java.lang.annotation.Target(ElementType.METHOD) public @interface Injector { /** - * @return the name of the stub annotated with {@link Target} this is referring to. - * @since 0.3.0 + * @return the patching reason, for logging, defaults to "No reason specified." + * @since 0.5.0 */ - String targetName() default ""; - - /** - * @return the parameters of the stub annotated with {@link Target} this is referring - * to (used to discern in case of method stubs by the same name) - * @since 0.3.0 - */ - Class[] params() default {}; + String reason() default "No reason specified."; } diff --git a/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java b/src/main/java/ftbsc/lll/processor/annotations/MultipleTargets.java similarity index 71% rename from src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java rename to src/main/java/ftbsc/lll/processor/annotations/MultipleTargets.java index 8b4b3f8..aa37530 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java +++ b/src/main/java/ftbsc/lll/processor/annotations/MultipleTargets.java @@ -6,14 +6,14 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Used to support {@link Injector} as a {@link Repeatable} annotation. - * @since 0.3.0 + * Used to support {@link Target} as a {@link Repeatable} annotation. + * @since 0.5.0 */ @Retention(RetentionPolicy.CLASS) @java.lang.annotation.Target(ElementType.METHOD) -public @interface MultipleInjectors { +public @interface MultipleTargets { /** * @return the {@link Injector} annotations, as an array */ - Injector[] value(); + Target[] value(); } diff --git a/src/main/java/ftbsc/lll/processor/annotations/Patch.java b/src/main/java/ftbsc/lll/processor/annotations/Patch.java index 0dcc377..bac3f6a 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Patch.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Patch.java @@ -1,5 +1,7 @@ package ftbsc.lll.processor.annotations; +import ftbsc.lll.proxies.impl.TypeProxy; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -15,27 +17,17 @@ import java.lang.annotation.RetentionPolicy; @java.lang.annotation.Target(ElementType.TYPE) public @interface Patch { /** - * @return the Minecraft {@link Class} to target for patching + * @return the {@link Class} to target for patching */ - Class value(); + Class value() default Object.class; /** - * @return the patching reason, for logging, defaults to "No reason specified." - */ - String reason() default "No reason specified."; - - /** - * @return the name of the inner class that should be targeted, + * This can be either the fully-qualified name to be used in place of {@link #value()} + * or an inner class name to append after a $ symbol to the already acquired + * fully-qualified name. + * @return the name of the inner class that contains the target, * defaults to empty string (not an inner class) - * @since 0.4.0 + * @since 0.5.0 */ - String innerClass() default ""; - - /** - * @return the anonymous class counter (1 for the first, 2 for - * the second, 3 for the third...) for the class that should be - * targeted, defaults to 0 (not an anonymous class) - * @since 0.4.0 - */ - int anonymousClassCounter() default 0; + String className() default ""; } diff --git a/src/main/java/ftbsc/lll/processor/annotations/Target.java b/src/main/java/ftbsc/lll/processor/annotations/Target.java index 15967cb..ed5cc0f 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Target.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Target.java @@ -1,22 +1,32 @@ package ftbsc.lll.processor.annotations; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; 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 annotated with {@link Patch} + * The method annotated with this, called "stub" within the documentation, should have the + * same name and parameters as the method it's supposed to represent. + * It will 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 */ @Retention(RetentionPolicy.CLASS) +@Repeatable(MultipleTargets.class) @java.lang.annotation.Target(ElementType.METHOD) public @interface Target { + /** + * Indicates which of the methods annotated with {@link Find} or {@link Injector} + * is targeting this stub. + * @return the name of the element this is supposed to apply to + * @since 0.5.0 + */ + String of() default ""; + /** * When set to false, tells the processor to first try to match a single method by name, * and to only check parameters if further clarification is needed. diff --git a/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java index 578d49f..1576a23 100644 --- a/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java +++ b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java @@ -1,6 +1,5 @@ package ftbsc.lll.processor.tools; -import com.squareup.javapoet.*; import ftbsc.lll.exceptions.AmbiguousDefinitionException; import ftbsc.lll.exceptions.MappingNotFoundException; import ftbsc.lll.exceptions.NotAProxyException; @@ -8,9 +7,9 @@ import ftbsc.lll.exceptions.TargetNotFoundException; import ftbsc.lll.processor.annotations.Find; import ftbsc.lll.processor.annotations.Patch; import ftbsc.lll.processor.annotations.Target; +import ftbsc.lll.processor.tools.containers.ClassContainer; import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; -import ftbsc.lll.proxies.FieldProxy; -import ftbsc.lll.proxies.MethodProxy; +import ftbsc.lll.proxies.ProxyType; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.*; @@ -19,12 +18,12 @@ import javax.lang.model.util.Elements; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import static ftbsc.lll.processor.tools.JavaPoetUtils.descriptorFromExecutableElement; -import static ftbsc.lll.processor.tools.JavaPoetUtils.methodDescriptorFromParams; /** * Collection of AST-related static utils that didn't really fit into the main class. @@ -32,17 +31,19 @@ import static ftbsc.lll.processor.tools.JavaPoetUtils.methodDescriptorFromParams 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. - * @param cl the {@link ExecutableElement} for the class containing the desired method + * an {@link Element} from it. + * @param parent the parent {@link Element} to the desired element * @param ann the {@link Class} corresponding to the desired annotation - * @return a {@link List} of {@link MethodSpec}s annotated with the given annotation + * @param the type of {@link Element} to use + * @return a {@link List} of {@link Element}s annotated with the given annotation * @since 0.2.0 */ - public static List findAnnotatedMethods(TypeElement cl, Class ann) { - return cl.getEnclosedElements() + @SuppressWarnings("unchecked") + public static List findAnnotatedElement(Element parent, Class ann) { + return parent.getEnclosedElements() .stream() .filter(e -> e.getAnnotationsByType(ann).length != 0) - .map(e -> (ExecutableElement) e) + .map(e -> (T) e) .collect(Collectors.toList()); } @@ -83,48 +84,43 @@ public class ASTUtils { } } + /** + * Takes in a {@link Collection} of AST {@link Modifier}s and + * returns them mapped to their reflective integer equivalent. + * @param modifiers the {@link Modifier}s + * @return an integer value representing them + * @since 0.5.0 + */ + public static int mapModifiers(Collection modifiers) { + int i = 0; + for(Modifier m : modifiers) + i |= mapModifier(m); + return i; + } + /** * Safely extracts a {@link Class} from an annotation and gets its fully qualified name. * @param ann the annotation containing the class * @param parentFunction the annotation function returning the class - * @param innerName a string containing the inner class name or anonymous class number, may be null + * @param name a string containing the FQN, the inner class name or nothing * @param the type of the annotation carrying the information * @return the fully qualified name of the given class * @since 0.3.0 */ - public static String getClassFullyQualifiedName(T ann, Function> parentFunction, String innerName) { + public static String getClassFullyQualifiedName(T ann, Function> parentFunction, String name) { + if(name.contains(".")) + return name; String fqn; try { fqn = parentFunction.apply(ann).getCanonicalName(); } catch(MirroredTypeException e) { fqn = e.getTypeMirror().toString(); } - if(innerName != null) - fqn = String.format("%s$%s", fqn, innerName); + if(!name.equals("")) + fqn = String.format("%s$%s", fqn, name); return fqn; } - /** - * Extracts the inner class name as a String from the annotation. - * @param ann the annotation containing the class - * @param innerClassFunction the annotation function returning the inner class name - * @param anonymousCounterFunction the annotation function returning the anonymous class counter - * @param the type of the annotation carrying the information - * @return the name of the inner class, or null if the target isn't an inner class - * @since 0.4.0 - */ - public static String getInnerName(T ann, Function innerClassFunction, Function anonymousCounterFunction) { - String inner = null; - if(!innerClassFunction.apply(ann).equals("")) - inner = innerClassFunction.apply(ann); - if(anonymousCounterFunction.apply(ann) != 0) { - if(inner != null) - throw new AmbiguousDefinitionException(String.format("Unclear inner class, is it %s or %d?", inner, anonymousCounterFunction.apply(ann))); - else inner = anonymousCounterFunction.apply(ann).toString(); - } - return inner; - } - /** * Safely extracts a {@link Class} array from an annotation. * @param ann the annotation containing the class @@ -165,30 +161,40 @@ public class ASTUtils { /** * Finds the class name and maps it to the correct format. - * @param patchAnn the {@link Patch} annotation containing target class info + * @param fallback the (unobfuscated) FQN to fall back on * @param finderAnn an annotation containing metadata about the target, may be null * @return the fully qualified class name * @since 0.3.0 */ - private static String findClassName(Patch patchAnn, Find finderAnn) { + public static String findClassNameFromAnnotations(String fallback, Find finderAnn) { String fullyQualifiedName; if(finderAnn != null) { fullyQualifiedName = getClassFullyQualifiedName( finderAnn, - Find::parent, - getInnerName(finderAnn, Find::parentInnerClass, Find::parentAnonymousClassCounter) + Find::value, + finderAnn.className() ); if(!fullyQualifiedName.equals("java.lang.Object")) return findClassName(fullyQualifiedName, null); } - fullyQualifiedName = + return findClassName(fallback, null); + } + + /** + * Finds the class name and maps it to the correct format. + * @param patchAnn the {@link Patch} annotation containing target class info + * @param finderAnn an annotation containing metadata about the target, may be null + * @return the fully qualified class name + * @since 0.3.0 + */ + public static String findClassNameFromAnnotations(Patch patchAnn, Find finderAnn) { + return findClassNameFromAnnotations( getClassFullyQualifiedName( patchAnn, Patch::value, - getInnerName(patchAnn, Patch::innerClass, Patch::anonymousClassCounter) - ); - return findClassName(fullyQualifiedName, null); + patchAnn.className() + ), null); } /** @@ -222,10 +228,10 @@ public class ASTUtils { * @throws TargetNotFoundException if it finds no valid candidate * @since 0.3.0 */ - private static Element findMember(String parentFQN, String name, String descr, boolean strict, boolean field, ProcessingEnvironment env) { + public static Element findMember(String parentFQN, String name, String descr, boolean strict, boolean field, ProcessingEnvironment env) { TypeElement parent = env.getElementUtils().getTypeElement(parentFQN); if(parent == null) - throw new AmbiguousDefinitionException(String.format("Could not find parent class %s!", parentFQN)); + throw new AmbiguousDefinitionException(String.format("Could not find parent class %s for member %s!", parentFQN, descr == null ? name : name + descr)); //try to find by name List candidates = parent.getEnclosedElements() .stream() @@ -233,12 +239,12 @@ public class ASTUtils { .filter(e -> e.getSimpleName().contentEquals(name)) .collect(Collectors.toList()); if(candidates.size() == 0) - throw new TargetNotFoundException(String.format("%s %s", name, descr)); + throw new TargetNotFoundException(field ? "field" : "method", String.format("%s %s", name, descr)); if(candidates.size() == 1 && (!strict || field)) return candidates.get(0); - if(descr == null) { + if(field || descr == null) { throw new AmbiguousDefinitionException( - String.format("Found %d methods named %s in class %s!", candidates.size(), name, parentFQN) + String.format("Found %d members named %s in class %s!", candidates.size(), name, parentFQN) ); } else { candidates = candidates.stream() @@ -248,7 +254,7 @@ public class ASTUtils { : c -> descr.split("\\)")[0].equalsIgnoreCase(descriptorFromExecutableElement(c).split("\\)")[0]) ).collect(Collectors.toList()); if(candidates.size() == 0) - throw new TargetNotFoundException(String.format("%s %s", name, descr)); + throw new TargetNotFoundException("method", String.format("%s %s", name, descr)); if(candidates.size() > 1) throw new AmbiguousDefinitionException( String.format("Found %d methods named %s in class %s!", candidates.size(), name, parentFQN) @@ -258,54 +264,73 @@ public class ASTUtils { } /** - * Finds the real class member (field or method) corresponding to a stub annotated with - * {@link Target} or {@link Find}. + * Finds the real class method corresponding to a stub annotated with {@link Target}. * @param stub the {@link ExecutableElement} for the stub * @param env the {@link ProcessingEnvironment} to perform the operation in - * @return the {@link Element} corresponding to the method or field + * @return the {@link ExecutableElement} corresponding to the method * @throws AmbiguousDefinitionException if it finds more than one candidate * @throws TargetNotFoundException if it finds no valid candidate * @since 0.3.0 */ - public static Element findMemberFromStub(ExecutableElement stub, ProcessingEnvironment env) { + public static ExecutableElement findMethodFromStub(ExecutableElement stub, ProcessingEnvironment env) { //the parent always has a @Patch annotation Patch patchAnn = stub.getEnclosingElement().getAnnotation(Patch.class); //there should ever only be one of these two Target targetAnn = stub.getAnnotation(Target.class); //if this is null strict mode is always disabled Find findAnn = stub.getAnnotation(Find.class); //this may be null, it means no fallback info - String parentFQN = findClassName(patchAnn, findAnn); - String methodDescriptor = - findAnn != null - ? methodDescriptorFromParams(findAnn, Find::params, env.getElementUtils()) - : descriptorFromExecutableElement(stub); - String memberName = - findAnn != null && !findAnn.name().equals("") - ? findAnn.name() - : stub.getSimpleName().toString(); - return findMember( + String parentFQN = findClassNameFromAnnotations(patchAnn, findAnn); + String methodName = stub.getSimpleName().toString(); + if(findAnn != null && !findAnn.name().equals("")) + throw new AmbiguousDefinitionException(String.format("Specified name %s in @Find annotation for method stub %s!", findAnn.name(), methodName)); + String methodDescriptor = descriptorFromExecutableElement(stub); + return (ExecutableElement) findMember( parentFQN, - memberName, + methodName, methodDescriptor, targetAnn != null && targetAnn.strict(), - targetAnn == null && !isMethodProxyStub(stub), //only evaluate if target is null + false, //only evaluate if target is null env ); } /** - * Utility method for finding out what type of proxy a method is. + * Utility method for finding out what type of proxy a field is. * It will fail if the return type is not a known type of proxy. - * @param m the annotated {@link ExecutableElement} - * @return whether it returns a {@link MethodProxy} or a {@link FieldProxy} + * @param v the annotated {@link VariableElement} + * @return the {@link ProxyType} for the element * @throws NotAProxyException if it's neither * @since 0.4.0 */ - public static boolean isMethodProxyStub(ExecutableElement m) { - String returnTypeFQN = m.getReturnType().toString(); - if(returnTypeFQN.equals("ftbsc.lll.proxies.FieldProxy")) - return false; - else if(returnTypeFQN.equals("ftbsc.lll.proxies.MethodProxy")) - return true; - else throw new NotAProxyException(m.getEnclosingElement().getSimpleName().toString(), m.getSimpleName().toString()); + public static ProxyType getProxyType(VariableElement v) { + String returnTypeFQN = v.asType().toString(); + switch(returnTypeFQN) { + case "ftbsc.lll.proxies.impl.FieldProxy": + return ProxyType.FIELD; + case "ftbsc.lll.proxies.impl.MethodProxy": + return ProxyType.METHOD; + case "ftbsc.lll.proxies.impl.TypeProxy": + return ProxyType.TYPE; + case "ftbsc.lll.proxies.impl.PackageProxy": + return ProxyType.PACKAGE; + default: + throw new NotAProxyException(v.getEnclosingElement().getSimpleName().toString(), v.getSimpleName().toString()); + } + } + + /** + * Finds and builds a {@link ClassContainer} based on information contained + * within a {@link Find} annotation, else returns a fallback. + * @param fallback the {@link ClassContainer} it falls back on + * @param f the {@link Find} annotation to get info from + * @param env the {@link ProcessingEnvironment} to perform the operation in + * @param mapper the {@link ObfuscationMapper} to use, may be null + * @return the built {@link ClassContainer} or the fallback if not enough information was present + * @since 0.5.0 + */ + public static ClassContainer findClassOrFallback(ClassContainer fallback, Find f, ProcessingEnvironment env, ObfuscationMapper mapper) { + String fqn = getClassFullyQualifiedName(f, Find::value, f.className()); + return fqn.equals("java.lang.Object") + ? fallback + : new ClassContainer(fqn, env, mapper); } } diff --git a/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java b/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java index 0591929..32a94bb 100644 --- a/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java +++ b/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java @@ -1,20 +1,31 @@ package ftbsc.lll.processor.tools; import com.squareup.javapoet.*; +import ftbsc.lll.processor.LilleroProcessor; +import ftbsc.lll.processor.annotations.Find; +import ftbsc.lll.processor.tools.containers.ArrayContainer; +import ftbsc.lll.processor.tools.containers.ClassContainer; +import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; +import ftbsc.lll.proxies.ProxyType; import ftbsc.lll.tools.DescriptorBuilder; -import ftbsc.lll.proxies.MethodProxy; -import ftbsc.lll.proxies.FieldProxy; +import ftbsc.lll.proxies.impl.MethodProxy; +import ftbsc.lll.proxies.impl.FieldProxy; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; +import javax.tools.Diagnostic; import java.lang.annotation.Annotation; import java.util.List; import java.util.function.Function; -import static ftbsc.lll.processor.tools.ASTUtils.classArrayFromAnnotation; +import static ftbsc.lll.processor.tools.ASTUtils.*; +import static ftbsc.lll.processor.tools.ASTUtils.mapModifiers; /** * Collection of static utils that rely on JavaPoet to function. @@ -96,23 +107,6 @@ public class JavaPoetUtils { return methodSignature.toString(); } - /** - * Builds a (partial, not including the return type) method descriptor from its parameters - * @param ann the annotation containing the class - * @param fun the annotation function returning the class - * @param elementUtils the {@link Elements} containing utils for the current processing environment - * @param the type of the annotation carrying the information - * @return the method descriptor - */ - public static String methodDescriptorFromParams(T ann, Function[]> fun, Elements elementUtils) { - List mirrors = classArrayFromAnnotation(ann, fun, elementUtils); - StringBuilder sb = new StringBuilder("("); - for(TypeMirror t : mirrors) - sb.append(descriptorFromType(t)); - sb.append(")"); - return sb.toString(); - } - /** * Adds to the given {@link MethodSpec.Builder} the given line of code, * containing a call to a method of a {@link MethodProxy.Builder} or a @@ -139,4 +133,88 @@ public class JavaPoetUtils { ); } } + + /** + * Appends to a given {@link MethodSpec.Builder} definitions for a proxy. + * @param fallback the {@link ClassContainer} to fall back on + * @param var the {@link VariableElement} representing the proxy + * @param stub the stub {@link ExecutableElement} if present or relevant, null otherwise + * @param con the {@link MethodSpec.Builder} to append to + * @param env the {@link ProcessingEnvironment} to perform the operation in + * @param mapper the {@link ObfuscationMapper} to use, may be null + * @since 0.5.0 + */ + public static void appendMemberFinderDefinition( + ClassContainer fallback, VariableElement var, ExecutableElement stub, MethodSpec.Builder con, ProcessingEnvironment env, ObfuscationMapper mapper) { + ProxyType type = getProxyType(var); + if(type != ProxyType.METHOD && type != ProxyType.FIELD) + return; //this method is irrelevant to everyoen else + + //we need this stuff + Find f = var.getAnnotation(Find.class); + ClassContainer parent = findClassOrFallback(fallback, f, env, mapper); + final boolean isMethod = type == ProxyType.METHOD; + final String builderName = var.getSimpleName().toString() + "Builder"; + + String name, nameObf; + Element target; + + if(isMethod) { + ExecutableElement executableTarget; + if(f.name().equals("")) //find and validate from stub + executableTarget = findMethodFromStub(stub, env); + else { //find and validate by name alone + if(LilleroProcessor.badPracticeWarnings) //warn user that he is doing bad stuff + env.getMessager().printMessage(Diagnostic.Kind.WARNING, + String.format("Matching method %s by name, this is bad practice and may lead to unexpected behaviour. Use @Target stubs instead!", f.name())); + executableTarget = (ExecutableElement) findMember(parent.fqn, f.name(), null, false, false, env); + } + name = executableTarget.getSimpleName().toString(); + nameObf = findMemberName(parent.fqnObf, name, descriptorFromExecutableElement(executableTarget), mapper); + target = executableTarget; + } else { + //find and validate target + name = f.name().equals("") ? var.getSimpleName().toString() : f.name(); + target = findMember(parent.fqn, name, null, false, true, env); + nameObf = findMemberName(parent.fqnObf, name, null, mapper); + } + + //initialize builder + con.addStatement("$T $L = $T.builder($S)", + isMethod ? MethodProxy.Builder.class : FieldProxy.Builder.class, + builderName, //variable name is always unique by definition + isMethod ? MethodProxy.class : FieldProxy.class, + nameObf + ); + + //set parent + con.addStatement( + "$L.setParent($S, $L)", + builderName, + parent.fqnObf, + mapModifiers(parent.elem.getModifiers()) + ); + + //set modifiers + con.addStatement( + "$L.setModifiers($L)", + builderName, + mapModifiers(target.getModifiers()) + ); + + if(isMethod) { //set parameters and return type + ExecutableElement executableTarget = (ExecutableElement) target; + for(VariableElement p : executableTarget.getParameters()) + addTypeToProxyGenerator(con, builderName, "addParameter", p.asType()); + addTypeToProxyGenerator(con, builderName, "setReturnType", executableTarget.getReturnType()); + } else //set type + addTypeToProxyGenerator(con,builderName, "setType", target.asType()); + + //build and set + con.addStatement( + "super.$L = $L.build()", + var.getSimpleName().toString(), + builderName + ); + } } diff --git a/src/main/java/ftbsc/lll/processor/tools/ArrayContainer.java b/src/main/java/ftbsc/lll/processor/tools/containers/ArrayContainer.java similarity index 95% rename from src/main/java/ftbsc/lll/processor/tools/ArrayContainer.java rename to src/main/java/ftbsc/lll/processor/tools/containers/ArrayContainer.java index c32a621..7e7da25 100644 --- a/src/main/java/ftbsc/lll/processor/tools/ArrayContainer.java +++ b/src/main/java/ftbsc/lll/processor/tools/containers/ArrayContainer.java @@ -1,4 +1,4 @@ -package ftbsc.lll.processor.tools; +package ftbsc.lll.processor.tools.containers; import javax.lang.model.type.ArrayType; import javax.lang.model.type.TypeKind; diff --git a/src/main/java/ftbsc/lll/processor/tools/containers/ClassContainer.java b/src/main/java/ftbsc/lll/processor/tools/containers/ClassContainer.java new file mode 100644 index 0000000..bde527d --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/tools/containers/ClassContainer.java @@ -0,0 +1,47 @@ +package ftbsc.lll.processor.tools.containers; + +import ftbsc.lll.exceptions.TargetNotFoundException; +import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; + +import static ftbsc.lll.processor.tools.ASTUtils.findClassName; + +/** + * Container for information about a class. + * Used internally for efficiency reasons. + * @since 0.5.0 + */ +public class ClassContainer { + /** + * The fully-qualified name of the class. + */ + public final String fqn; + + /** + * The obfuscated fully-qualified name of the class. + * If the mapper passed is null, then this will be identical to {@link #fqn} + */ + public final String fqnObf; + + /** + * The {@link Element} corresponding to the class. + */ + public final Element elem; + + /** + * Public constructor. + * @param fqn the fully-qualified name of the target class + * @param env the {@link ProcessingEnvironment} to be used to locate the class + * @param mapper the {@link ObfuscationMapper} to be used, may be null + */ + public ClassContainer(String fqn, ProcessingEnvironment env, ObfuscationMapper mapper) { + this.fqn = fqn; + this.fqnObf = findClassName(fqn, mapper); + Element elem = env.getElementUtils().getTypeElement(fqn); //at compile time we have an unobfuscated environment + if(elem == null) + throw new TargetNotFoundException("class", fqn); + else this.elem = elem; + } +} \ No newline at end of file