feat: better inner classes api, removed deprecation warnings since that stuff is there to stay

This commit is contained in:
zaaarf 2024-06-04 20:03:51 +02:00
parent f8bf9996c9
commit 4d330a731b
No known key found for this signature in database
GPG key ID: 102E445F4C3F829B
10 changed files with 166 additions and 125 deletions

View file

@ -1,17 +1,22 @@
# Lillero-processor # Lillero-processor
Lillero-processor is an annotation processor made to simplify development of [Lillero](https://github.com/zaaarf/lillero) patches, minimising the amount of boilerplate code needed. Lillero-processor is an annotation processor made to simplify development of [Lillero](https://github.com/zaaarf/lillero) patches, minimising the
amount of boilerplate code needed.
An important note: to make things as easy as it is, the processor assumes that you have the code of the target available in your development environment. As of 0.6.0, it will not work otherwise. Please note that to work its magic, the processor needs the code of the target of your patches available in the compile
time environment. If your build system does not allow that, you may want to use the library manually instead.
## Usage ## Usage
First things first, add the processor to your `build.gradle`: First things first, add the processor to your build system. The example shows Gradle, but any build system supporting
Maven repositories should work:
```groovy ```groovy
dependencies { dependencies {
implementation 'ftbsc.lll:processor:<whatever the latest version is>' implementation 'ftbsc.lll:processor:<whatever the latest version is>'
annotationProcessor 'ftbsc.lll:processor:<whatever the latest version is>' annotationProcessor 'ftbsc.lll:processor:<whatever the latest version is>'
} }
``` ```
Once it's done, you will be able to use Lillero without insane amounts of boilerplate. The processor works by generating new classes overriding the patches you write. The examples are abstract because stubs look better, but it should work even with concrete classes - in fact, modifiers are generally disregarded. Once it's done, you will be able to use Lillero without insane amounts of boilerplate. The processor works by generating
new classes overriding the patches you write. The examples are abstract because stubs look better, but it should work
even with concrete classes - in fact, modifiers are generally disregarded.
The examples are about Minecraft, but you can use this with any Lillero-based project. The examples are about Minecraft, but you can use this with any Lillero-based project.
@ -49,16 +54,27 @@ public abstract class SamplePatch {
} }
``` ```
The annotation `@Patch` specifies which class should be patched. `@Target` must be used on a stub with the same descriptor (return type and parameter types) and name as the target method. Its parameter `of` specifies who is referring to it. It follows that multiple patches may be made to different methods within a same class, as long as the injector name is always specified correctly. The `@Target` annotation is repeatable, and may therefore be used to have multiple injections on the same method. The annotation `@Patch` specifies which class should be patched. `@Target` must be used on a stub with the same
descriptor (return type and parameter types) and name as the target method. Its parameter `of` specifies who is
referring to it. It follows that multiple patches may be made to different methods within a same class, as long as the
injector name is always specified correctly. The `@Target` annotation is repeatable, and may therefore be used to have
multiple injections on the same method.
If for any reason you don't want to check the full signature, but rather attempt a lookup by name only, simply add `strict = false` to your `@Target`. This is not recommended, as you may not always have the guarantee that you are the only one tampering with runtime code. If for any reason you don't want to check the full signature, but rather attempt a lookup by name only, simply add
`strict = false` to your `@Target`. This is not recommended, as you may not always have the guarantee that you are the
only one tampering with runtime code.
You may find yourself not wanting to use the actual name of the target method in the stub. Maybe you have a name conflict, or maybe you are just trying to patch a constructor (`<init>`) or a static constructor (`<clinit>`), whose names you cannot type. Simply add `methodName = "name"` to your `@Target` annotation, and the specified name will overrule the stub's name. You may find yourself not wanting to use the actual name of the target method in the stub. Maybe you have a name
conflict, or maybe you are just trying to patch a constructor (`<init>`) or a static constructor (`<clinit>`), whose
names you cannot type. Simply add `methodName = "name"` to your `@Target` annotation, and the specified name will
overrule the stub's name.
It's interesting to note that the `ClassNode` in your injector method can be omitted if you don't use it (that is, most of them). Note that you can omit the `ClassNode` parameter in your injector method if you don't use it (which is most cases).
### Finders ### Finders
While patching, you may find yourself needing to refer to other methods and fields, both within your code and within the target. This can be simplified considerably through the `@Find` annotation. The behaviour of `@Find` differs considerably depending on what kind of element it is looking for. Let's see the three cases. While patching, you may find yourself needing to refer to other methods and fields, both within your code and within the
target. This can be simplified considerably through the `@Find` annotation. The behaviour of `@Find` differs
considerably depending on what kind of element it is looking for. Let's see the three cases.
```java ```java
@Find(SomeClass.class) @Find(SomeClass.class)
@ -72,7 +88,8 @@ This is the simplest case. This finder will match any field named `fieldName` wi
TypeProxy typeProxy; TypeProxy typeProxy;
``` ```
A `TypeProxy` is used to represent a class type. The `name` parameter, if given, will be ignored, and so will be the actual field name. The resulting `TypeProxy` will be a representation of `SomeClass`. A `TypeProxy` is used to represent a class type. The `name` parameter, if given, will be ignored, and so will be the
actual field name. The resulting `TypeProxy` will be a representation of `SomeClass`.
```java ```java
@Find(SomeClass.class) @Find(SomeClass.class)
@ -82,11 +99,16 @@ MethodProxy methodProxy;
abstract void someMethod(int i, int j); abstract void someMethod(int i, int j);
``` ```
MethodProxies need a stub to correctly match their target. Matching by name is also supported - either by setting the `strict` flag of the `@Target` annotation or by setting the `name` parameter in the `@Find` annotation - but is not recommended. The class specified within `@Find`, much like with fields, will be considered the parent class of the method you are looking for. MethodProxies need a stub to correctly match their target. Matching by name is also supported - either by setting the
`strict` flag of the `@Target` annotation or by setting the `name` parameter in the `@Find` annotation - but is not
recommended. The class specified within `@Find`, much like with fields, will be considered the parent class of the
method you are looking for.
Whenever the class is unspecified in a finder (except in TypeProxy's case, which is an error) it will be assumed to be the class containing the `@Find` annotation - that is, the patch class. Whenever the class is unspecified in a finder (except in TypeProxy's case, which is an error) it will be assumed to be
the class containing the `@Find` annotation - that is, the patch class.
Lillero provides three classes to use these in your injectors: `FieldProxyInsnNode`, `MethodProxyInsnNode` and `TypeProxyInsnNode`. Each wraps the equivalent [ObjectWeb ASM](https://asm.ow2.io/) `InsnNode`. For instance: Lillero provides three classes to use these in your injectors: `FieldProxyInsnNode`, `MethodProxyInsnNode` and
`TypeProxyInsnNode`. Each wraps the equivalent [ObjectWeb ASM](https://asm.ow2.io/) `InsnNode`. For instance:
```java ```java
@Find(SomeClass.class) @Find(SomeClass.class)
@ -103,23 +125,33 @@ public void inject(ClassNode clazz, MethodNode main) {
Obviously, it's up to you to use the correct opcode. The processor can't read your mind (yet). Obviously, it's up to you to use the correct opcode. The processor can't read your mind (yet).
### Private Inner Classes ### Private Inner Classes
You may find yourself needing to interact with a private inner class - which you can't reference explicitly by `Name.class`. The processor has your back, once again. Every annotation which tries to match a class (i.e. `@Patch` and `@Find`) also provides a `innerName` parameter. This allows you to specify the "unaccessible part" of the name, to be appended with a `$` in front of what is extracted from the Class object. In the unfortunate case of multiple nesting with private classes, just place any extra `$` yourself (i.e. `SampleClass$PrivateInnerFirst$InnerSecond` should be reached with a `@Patch(value = SampleClass.class, innerName = "PrivateInnerFirst$InnerSecond"`). You may find yourself needing to interact with a private inner class - which you can't reference explicitly by
`Name.class`. The processor has your back, once again. Every annotation which tries to match a class (i.e. `@Patch` and
`@Find`) also provides a `inner` parameter. This allows you to specify the "unaccessible part" of the name, to be
appended with a `$` in front of what is extracted from the Class object. In the unfortunate case of multiple nesting
with private classes, just place any extra `$` yourself (i.e. `SampleClass$PrivateInnerFirst$InnerSecond` should be
reached with a `@Patch(value = SampleClass.class, inner = {"PrivateInnerFirst", "InnerSecond"}`).
#### Anonymous classes #### Anonymous classes
Anonymous classes are trickier, because [they are apparently unavailable](https://stackoverflow.com/questions/75849759/how-to-find-an-anonymous-class-in-the-annotation-processing-environment) in the normal annotation processing environment. That means that, unlike with other classes, the processor cannot make sure that they exist, and it cannot easily extract information about their fields and methods. Anonymous classes are trickier, because [they are apparently unavailable](https://stackoverflow.com/questions/75849759/how-to-find-an-anonymous-class-in-the-annotation-processing-environment) in the normal annotation processing
environment. That means that, unlike with other classes, the processor cannot make sure that they exist, and it cannot
easily extract information about their fields and methods.
Anonymous classes are numbered by the compiler in the order it meets them, starting from 1. The following rules apply to patching an anonymous class with the processor, as of version 0.6.0: Anonymous classes are numbered by the compiler in the order it meets them, starting from 1. The following rules apply to
* Use the compiler-assigned number as `innerName` parameter, next to the parent class. patching an anonymous class with the processor, as of version 0.6.0:
* Use the compiler-assigned number as `inner` parameter, next to the parent class.
* Write any method stub normally. * Write any method stub normally.
* Finders for anonymous class fields may be made, but their type has to be specified explicitly, unlike all others, by using the `type()` and `typeInner()` parameters. * Finders for anonymous class fields may be made, but their type has to be specified explicitly, unlike all others, by
- Local variables of the containing method may sometimes be accessible by an anonymous class. Make sure to use the `name` parameter of the finder appending the `val$` prefix, such as `val$actualName`. using the `type()` and `typeInner()` parameters.
- Local variables of the containing method may sometimes be accessible by an anonymous class. Make sure to use the
The extra `@Find` parameters (`type()` and `typeInner()`) are meant to be temporary, hacky workarounds until a better way is found. Expect them to change and be removed without notice. `name` parameter of the finder appending the `val$` prefix, such as `val$actualName`.
Most if not all of this (although I have not tested it) should apply to local classes as well. Most if not all of this (although I have not tested it) should apply to local classes as well.
### Hybrid setups ### Hybrid setups
Sometimes, you may want to manually write IInjectors yourself in a project which also uses the processor. In these cases, you don't want to create the service provider (the `META-INF/services/ftbsc.lll.IInjector` file) yourself, to avoid conflicts. Simply add the annotation `@RegisterBareInjector` on top of the IInjector class(es) you wrote. Sometimes, you may want to manually write IInjectors yourself in a project which also uses the processor. In these
cases, you don't want to create the service provider (the `META-INF/services/ftbsc.lll.IInjector` file) yourself, to
void conflicts. Simply add the annotation `@RegisterBareInjector` on top of the IInjector class(es) you wrote.
### Obfuscation support ### Obfuscation support
You may pass a mappings file to the processor by adding this to your `build.gradle`: You may pass a mappings file to the processor by adding this to your `build.gradle`:
@ -130,14 +162,19 @@ compileJava { //mappings for lillero-processor
} }
``` ```
This feature is in a very early stage, and currently only works with TSRG files (Minecraft MCP to SRG mappings). More formats will be added in the future - possibly as a separate library the processor will rely on. This feature is in a very early stage, and currently only works with TSRG files (Minecraft MCP to SRG mappings). More
formats will be added in the future - possibly as a separate library the processor will rely on.
### Other processor arguments ### Other processor arguments
In the same way you pass mappings, you may pass `false` or `0` to the boolean arguments `badPracticeWarnings` and `anonymousClassWarning`, to disable, respectively, warnings about bad practices in usage, and reminders of the unsafety of anonymous classes. In the same way you pass mappings, you may pass `false` or `0` to the boolean arguments `badPracticeWarnings` and
`anonymousClassWarning`, to disable, respectively, warnings about bad practices in usage, and reminders of the unsafety
of anonymous classes.
## Conclusions and Extras ## Conclusions and Extras
Since reaching version 0.5.0, the processor will hopefully be mostly stable. It has changed much in the past versions, but I am confident that we now found a solution capable of handling most, if not all, cases. Since reaching version 0.5.0, the processor will hopefully be mostly stable. It has changed much in the past versions,
but I am confident that we now found a solution capable of handling most, if not all, cases.
Though most of the original code is gone, you can still read my dev diary about developing its first version [here](https://fantabos.co/posts/zaaarf/to-kill-a-boilerplate/) if you are curious about the initial ideas behind it. Though most of the original code is gone, you can still read my dev diary about developing its first version
[here](https://fantabos.co/posts/zaaarf/to-kill-a-boilerplate/) if you are curious about the initial ideas behind it.
In conclusion, let me just say: happy patching! In conclusion, let me just say: happy patching!

View file

@ -1,8 +1,7 @@
package ftbsc.lll.exceptions; package ftbsc.lll.exceptions;
/** /**
* Thrown when the processor finds multiple methods matching the * Thrown when the processor finds multiple methods matching the given criteria.
* given criteria.
*/ */
public class AmbiguousDefinitionException extends RuntimeException { public class AmbiguousDefinitionException extends RuntimeException {
@ -13,13 +12,4 @@ public class AmbiguousDefinitionException extends RuntimeException {
public AmbiguousDefinitionException(String message) { public AmbiguousDefinitionException(String message) {
super(message); super(message);
} }
/**
* Constructs a new ambiguous definition exception with the specified detail message and cause.
* @param message the detail message
* @param cause the cause, may be null (indicating nonexistent or unknown cause)
*/
public AmbiguousDefinitionException(String message, Throwable cause) {
super(message, cause);
}
} }

View file

@ -0,0 +1,15 @@
package ftbsc.lll.exceptions;
/**
* Thrown when the user provides manually an invalid class name.
*/
public class InvalidClassNameException extends RuntimeException {
/**
* Constructs a new exception for the specified name.
* @param name the name in question
*/
public InvalidClassNameException(String name) {
super(String.format("Provided class name %s is not valid!", name));
}
}

View file

@ -1,22 +0,0 @@
package ftbsc.lll.exceptions;
/**
* Thrown when a resource passed as an argument is not found.
*/
public class InvalidResourceException extends RuntimeException {
/**
* Empty constructor, used when the provided resource exists but is empty.
*/
public InvalidResourceException() {
super("The specified resource was empty!");
}
/**
* Named constructor, used when the specified resource doesn't exist.
* @param name the resource name
*/
public InvalidResourceException(String name) {
super(String.format("Specified resource %s was not found!", name));
}
}

View file

@ -151,7 +151,7 @@ public class LilleroProcessor extends AbstractProcessor {
//find class information //find class information
Patch patchAnn = cl.getAnnotation(Patch.class); Patch patchAnn = cl.getAnnotation(Patch.class);
ClassContainer targetClass = ClassContainer.from( ClassContainer targetClass = ClassContainer.from(
patchAnn, Patch::value, patchAnn.innerName(), this.getProcessorOptions() patchAnn, Patch::value, patchAnn.inner(), this.getProcessorOptions()
); );
//find package information //find package information
Element packageElement = cl.getEnclosingElement(); Element packageElement = cl.getEnclosingElement();

View file

@ -26,15 +26,13 @@ public @interface Find {
Class<?> value() default Object.class; Class<?> value() default Object.class;
/** /**
* This is the inner class name to append after a $ symbol to the already acquired * For a {@link TypeProxy}, this refers to the target itself rather than its parent.
* fully-qualified name. If it's a number instead of a valid name, the class will be
* treated as an anonymous class, and will therefore be automatically unverified.
* For a {@link TypeProxy}, this refers to the class itself rather than the parent.
* @return the name of the inner class that contains the target, defaults to empty * @return the name of the inner class that contains the target, defaults to empty
* string (not an inner class) * string (not an inner class)
* @see Patch#inner() for details
* @since 0.5.0 * @since 0.5.0
*/ */
String innerName() default ""; String[] inner() default {};
/** /**
* For a {@link FieldProxy}, this is the name of the field to find. If omitted, * For a {@link FieldProxy}, this is the name of the field to find. If omitted,
@ -42,7 +40,7 @@ public @interface Find {
* For a {@link MethodProxy} it indicates an attempt to match by name only, with * 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 * 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 * 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 * relevant class. It is generally recommended that you use a {@link Target} stub
* for methods, as this can lead to unpredictable behaviour at runtime. * for methods, as this can lead to unpredictable behaviour at runtime.
* It will have no effect on a {@link TypeProxy}. * It will have no effect on a {@link TypeProxy}.
* @return the name of the target, will default to the empty string (the name of * @return the name of the target, will default to the empty string (the name of
@ -52,20 +50,16 @@ public @interface Find {
String name() default ""; String name() default "";
/** /**
* This overrules the type of a field. Only to be used in the case (such as fields of * This overrules a field type. Only to be used in the case (such as in fields of
* anonymous classes) of fields whose parents cannot be reached at processing time. * anonymous classes) of fields whose parents cannot be reached at processing time.
* @return a {@link Class} representing the type. * @return a {@link Class} representing the type.
* @deprecated This is only meant as a temporary solution until a better handling
* is implemented; only use this if strictly necessary as it may be
* removed or changed even across revisions.
*/ */
@Deprecated
Class<?> type() default Object.class; Class<?> type() default Object.class;
/** /**
* This is to be used in cases where private inner classes are used as parameters.
* @return the inner class name to be used with {@link #type()} * @return the inner class name to be used with {@link #type()}
* @deprecated See {@link #type()}'s deprecation notice for more info. * @see Patch#inner() for details
*/ */
@Deprecated String[] typeInner() default {};
String typeInner() default "";
} }

View file

@ -20,12 +20,13 @@ public @interface Patch {
Class<?> value(); Class<?> value();
/** /**
* This is the inner class name to append after a $ symbol to the already acquired * This contains the inner class name(s) to append, separated by a $ symbol, to the already
* fully-qualified name. If it's a number instead of a valid name, the class will be * acquired fully-qualified name.
* treated as an anonymous class, and will therefore be automatically unverified. * If a number is provided instead of a valid name, the class will be treated as an
* @return the name of the inner class that contains the target, defaults to empty * anonymous class, and will therefore be skipped in verification.
* string (not an inner class) * @return the name or path of the inner class that contain the target, defaults to array containing
* a single empty string (not an inner class)
* @since 0.5.0 * @since 0.5.0
*/ */
String innerName() default ""; String[] inner() default {};
} }

View file

@ -32,13 +32,13 @@ public class ClassContainer {
public final TypeElement elem; public final TypeElement elem;
/** /**
* Private constructor, called from {@link #from(Annotation, Function, String, ProcessorOptions)}. * Private constructor, called from {@link #from(Annotation, Function, String[], ProcessorOptions)}.
* @param fqn the fully-qualified name of the target class * @param fqn the fully-qualified name of the target class
* @param innerNames an array of Strings containing the path to the inner class, may be null * @param innerNames an array of Strings containing the path to the inner class, may be null
* @param options the {@link ProcessorOptions} to be used * @param options the {@link ProcessorOptions} to be used
*/ */
private ClassContainer(String fqn, String[] innerNames, ProcessorOptions options) { private ClassContainer(String fqn, String[] innerNames, ProcessorOptions options) {
//find and validate // find and validate
TypeElement elem = options.env.getElementUtils().getTypeElement(fqn); TypeElement elem = options.env.getElementUtils().getTypeElement(fqn);
if(elem == null) if(elem == null)
@ -49,23 +49,11 @@ public class ClassContainer {
); );
if(innerNames != null) { if(innerNames != null) {
boolean skip = false;
for(String inner : innerNames) { for(String inner : innerNames) {
if(inner == null) continue; if(inner != null) fqnBuilder.append('$').append(inner);
fqnBuilder.append("$").append(inner); if(skip) continue;
try { if(shouldValidate(inner)) {
int anonClassCounter = Integer.parseInt(inner);
//anonymous classes cannot be validated!
if(options.anonymousClassWarning)
options.env.getMessager().printMessage(
Diagnostic.Kind.WARNING,
String.format(
"Anonymous classes cannot be verified by the processor. The existence of %s$%s is not guaranteed!",
fqnBuilder, anonClassCounter
)
);
elem = null;
break;
} catch(NumberFormatException exc) {
elem = elem elem = elem
.getEnclosedElements() .getEnclosedElements()
.stream() .stream()
@ -74,11 +62,24 @@ public class ClassContainer {
.filter(e -> e.getSimpleName().contentEquals(inner)) .filter(e -> e.getSimpleName().contentEquals(inner))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
} else {
options.env.getMessager().printMessage(
Diagnostic.Kind.WARNING,
String.format(
"Anonymous class %s$%s and its children cannot be verified by the processor!",
fqnBuilder,
inner
)
);
elem = null;
skip = true;
continue;
} }
if(elem == null) if(elem == null)
throw new TargetNotFoundException("class", inner); throw new TargetNotFoundException("class", inner);
} }
} }
this.data = getClassData(fqnBuilder.toString(), options.mapper); this.data = getClassData(fqnBuilder.toString(), options.mapper);
this.elem = elem; this.elem = elem;
} }
@ -87,17 +88,22 @@ public class ClassContainer {
* Safely extracts a {@link Class} from an annotation and gets its fully qualified name. * Safely extracts a {@link Class} from an annotation and gets its fully qualified name.
* @param ann the annotation containing the class * @param ann the annotation containing the class
* @param classFunction the annotation function returning the class * @param classFunction the annotation function returning the class
* @param innerName a string containing the inner class name or nothing * @param innerNames a string containing the inner class name or nothing
* @param options the {@link ProcessorOptions} to be used * @param options the {@link ProcessorOptions} to be used
* @param <T> the type of the annotation carrying the information * @param <T> the type of the annotation carrying the information
* @return the fully qualified name of the given class * @return the fully qualified name of the given class
* @since 0.5.0 * @since 0.5.0
*/ */
public static <T extends Annotation> ClassContainer from(T ann, Function<T, Class<?>> classFunction, String innerName, ProcessorOptions options) { public static <T extends Annotation> ClassContainer from(
String fqn; T ann,
String[] inner; Function<T, Class<?>> classFunction,
fqn = getTypeFromAnnotation(ann, classFunction, options.env).toString(); String[] innerNames,
inner = innerName.equals("") ? null : innerName.split("//$"); ProcessorOptions options
) {
String fqn = getTypeFromAnnotation(ann, classFunction, options.env).toString();
String[] inner = innerNames != null && innerNames.length != 0
? String.join("$", innerNames).split("\\$")
: null;
return new ClassContainer(fqn, inner, options); return new ClassContainer(fqn, inner, options);
} }
@ -123,8 +129,8 @@ public class ClassContainer {
* @since 0.5.0 * @since 0.5.0
*/ */
public static ClassContainer findOrFallback(ClassContainer fallback, Patch p, Find f, ProcessorOptions options) { public static ClassContainer findOrFallback(ClassContainer fallback, Patch p, Find f, ProcessorOptions options) {
if(f == null) return ClassContainer.from(p, Patch::value, p.innerName(), options); if(f == null) return ClassContainer.from(p, Patch::value, p.inner(), options);
ClassContainer cl = ClassContainer.from(f, Find::value, f.innerName(), options); ClassContainer cl = ClassContainer.from(f, Find::value, f.inner(), options);
return cl.data.name.equals("java/lang/Object") ? fallback : cl; return cl.data.name.equals("java/lang/Object") ? fallback : cl;
} }
} }

View file

@ -58,7 +58,7 @@ public class FieldContainer {
*/ */
private FieldContainer(ClassContainer parent, String name, String descriptor, ProcessorOptions options) { private FieldContainer(ClassContainer parent, String name, String descriptor, ProcessorOptions options) {
this.parent = parent; this.parent = parent;
if(parent.elem == null) { //unverified if(parent.elem == null) { // unverified
if(descriptor == null) if(descriptor == null)
throw new AmbiguousDefinitionException("Cannot use name-based lookups for fields of unverifiable classes!"); throw new AmbiguousDefinitionException("Cannot use name-based lookups for fields of unverifiable classes!");
this.elem = null; this.elem = null;
@ -81,9 +81,9 @@ public class FieldContainer {
* @since 0.5.0 * @since 0.5.0
*/ */
public static FieldContainer from(VariableElement finder, ProcessorOptions options) { public static FieldContainer from(VariableElement finder, ProcessorOptions options) {
//the parent always has a @Patch annotation // the parent always has a @Patch annotation
Patch patchAnn = finder.getEnclosingElement().getAnnotation(Patch.class); Patch patchAnn = finder.getEnclosingElement().getAnnotation(Patch.class);
//the finder always has a @Find annotation // the finder always has a @Find annotation
Find f = finder.getAnnotation(Find.class); Find f = finder.getAnnotation(Find.class);
ClassContainer parent = ClassContainer.findOrFallback( ClassContainer parent = ClassContainer.findOrFallback(
@ -96,12 +96,11 @@ public class FieldContainer {
if(fieldType.toString().equals("java.lang.Object")) { if(fieldType.toString().equals("java.lang.Object")) {
descriptor = null; descriptor = null;
} else { } else {
if(fieldType.getKind() != TypeKind.VOID && !fieldType.getKind().isPrimitive()) if(fieldType.getKind() != TypeKind.VOID && !fieldType.getKind().isPrimitive()) {
descriptor = //jank af but this is temporary anyway descriptor = String.format("L%s;", ClassContainer.from(
"L" + ClassContainer.from(
f, Find::type, f.typeInner(), options f, Find::type, f.typeInner(), options
).data.nameMapped + ";"; ).data.nameMapped);
else descriptor = descriptorFromType(fieldType, options.env); } else descriptor = descriptorFromType(fieldType, options.env);
} }
return new FieldContainer(parent, name, descriptor, options); return new FieldContainer(parent, name, descriptor, options);

View file

@ -1,9 +1,6 @@
package ftbsc.lll.processor.utils; package ftbsc.lll.processor.utils;
import ftbsc.lll.exceptions.AmbiguousDefinitionException; import ftbsc.lll.exceptions.*;
import ftbsc.lll.exceptions.MappingNotFoundException;
import ftbsc.lll.exceptions.NotAProxyException;
import ftbsc.lll.exceptions.TargetNotFoundException;
import ftbsc.lll.mapper.utils.Mapper; import ftbsc.lll.mapper.utils.Mapper;
import ftbsc.lll.mapper.data.ClassData; import ftbsc.lll.mapper.data.ClassData;
import ftbsc.lll.mapper.data.FieldData; import ftbsc.lll.mapper.data.FieldData;
@ -13,12 +10,14 @@ import ftbsc.lll.processor.containers.ClassContainer;
import ftbsc.lll.proxies.ProxyType; import ftbsc.lll.proxies.ProxyType;
import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*; import javax.lang.model.element.*;
import javax.lang.model.type.*; import javax.lang.model.type.*;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -148,7 +147,7 @@ public class ASTUtils {
t = env.getTypeUtils().erasure(t); //type erasure t = env.getTypeUtils().erasure(t); //type erasure
StringBuilder desc = new StringBuilder(); StringBuilder desc = new StringBuilder();
//add array brackets // add array brackets
while(t.getKind() == TypeKind.ARRAY) { while(t.getKind() == TypeKind.ARRAY) {
desc.append("["); desc.append("[");
t = ((ArrayType) t).getComponentType(); t = ((ArrayType) t).getComponentType();
@ -261,7 +260,7 @@ public class ASTUtils {
*/ */
public static FieldData getFieldData(String parent, String name, Mapper mapper) { public static FieldData getFieldData(String parent, String name, Mapper mapper) {
try { try {
name = name.replace('.', '/'); //just in case name = name.replace('.', '/'); // just in case
if(mapper != null) if(mapper != null)
return mapper.getFieldData(parent, name); return mapper.getFieldData(parent, name);
} catch(MappingNotFoundException ignored) {} } catch(MappingNotFoundException ignored) {}
@ -286,7 +285,7 @@ public class ASTUtils {
boolean strict, boolean field, ProcessingEnvironment env) { boolean strict, boolean field, ProcessingEnvironment env) {
if(parent.elem == null) if(parent.elem == null)
throw new TargetNotFoundException("parent class", parent.data.name); throw new TargetNotFoundException("parent class", parent.data.name);
//try to find by name // try to find by name
List<Element> candidates = parent.elem.getEnclosedElements() List<Element> candidates = parent.elem.getEnclosedElements()
.stream() .stream()
.filter(e -> (field && e instanceof VariableElement) || e instanceof ExecutableElement) .filter(e -> (field && e instanceof VariableElement) || e instanceof ExecutableElement)
@ -304,8 +303,8 @@ public class ASTUtils {
"Found %d members named %s in class %s!", candidates.size(), name, parent.data.name)); "Found %d members named %s in class %s!", candidates.size(), name, parent.data.name));
} else { } else {
if(field) { if(field) {
//fields can verify the signature for extra safety // fields can verify the signature for extra safety
//but there can only be 1 field with a given name // but there can only be 1 field with a given name
if(!descriptorFromType(candidates.get(0).asType(), env).equals(descr)) if(!descriptorFromType(candidates.get(0).asType(), env).equals(descr))
throw new TargetNotFoundException("field", String.format( throw new TargetNotFoundException("field", String.format(
"%s with descriptor %s", name, descr), parent.data.name); "%s with descriptor %s", name, descr), parent.data.name);
@ -348,7 +347,7 @@ public class ASTUtils {
continue; continue;
if (env.getElementUtils().overrides(method, (ExecutableElement) elem, context)) { if (env.getElementUtils().overrides(method, (ExecutableElement) elem, context)) {
method = (ExecutableElement) elem; method = (ExecutableElement) elem;
break; //found break; // found
} }
} }
@ -406,4 +405,26 @@ public class ASTUtils {
throw new NotAProxyException(v.getEnclosingElement().getSimpleName().toString(), v.getSimpleName().toString()); throw new NotAProxyException(v.getEnclosingElement().getSimpleName().toString(), v.getSimpleName().toString());
} }
} }
/**
* A pattern for efficiently recognising numeric strings.
* @since 0.7.0
*/
private final static Pattern NUMERIC = Pattern.compile("^[0-9]+$");
/**
* Checks whether a certain class name is valid, and whether the processor is able to validate
* its existence.
* @param name the name to validate
* @return true if it's a valid class name, false if it's an anonymous class identifier
* @throws InvalidClassNameException if an invalid name was provided
* @since 0.7.0
*/
public static boolean shouldValidate(String name) throws InvalidClassNameException {
if(SourceVersion.isIdentifier(name) && !SourceVersion.isKeyword(name)) {
return true; // if it's a valid name, proceed
} else if(NUMERIC.matcher(name).matches()) {
return false;
} else throw new InvalidClassNameException(name);
}
} }