diff options
author | Linnea Gräf <nea@nea.moe> | 2024-10-18 14:19:55 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-10-18 14:19:55 +0200 |
commit | 34a3477488e2e15a5a0e7cd2cd358faf8f926f71 (patch) | |
tree | 8a54c6f418448ce91696d03e1aeb4e94ffe8375d | |
parent | f4fa81ebd56882bf25261427f3ca6672e2782e0b (diff) | |
download | skyhanni-34a3477488e2e15a5a0e7cd2cd358faf8f926f71.tar.gz skyhanni-34a3477488e2e15a5a0e7cd2cd358faf8f926f71.tar.bz2 skyhanni-34a3477488e2e15a5a0e7cd2cd358faf8f926f71.zip |
Support permuting type arguments of GenericSkyhanniEventgeneric-permutable-resolution
4 files changed, 120 insertions, 8 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt b/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt index ce8194ebc..3aee266b2 100644 --- a/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt +++ b/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt @@ -1,11 +1,15 @@ package at.hannibal2.skyhanni.api.event import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.utils.ReflectionUtils import java.lang.invoke.LambdaMetafactory import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.TypeVariable +import java.lang.reflect.WildcardType import java.util.function.Consumer class EventListeners private constructor(val name: String, private val isGeneric: Boolean) { @@ -14,15 +18,19 @@ class EventListeners private constructor(val name: String, private val isGeneric constructor(event: Class<*>) : this( (event.name.split(".").lastOrNull() ?: event.name).replace("$", "."), - GenericSkyHanniEvent::class.java.isAssignableFrom(event) + GenericSkyHanniEvent::class.java.isAssignableFrom(event), ) fun addListener(method: Method, instance: Any, options: HandleEvent) { + require(method.parameterCount == 1) val generic: Class<*>? = if (isGeneric) { - method.genericParameterTypes - .firstNotNullOfOrNull { it as? ParameterizedType } - ?.let { it.actualTypeArguments.firstOrNull() as? Class<*> } - ?: throw IllegalArgumentException("Generic event handler must have a generic type") + ReflectionUtils.resolveUpperBoundSuperClassGenericParameter( + method.genericParameterTypes[0], + GenericSkyHanniEvent::class.java.typeParameters[0], + ) ?: error( + "Generic event handler type parameter is not present in " + + "event class hierarchy for type ${method.genericParameterTypes[0]}", + ) } else { null } @@ -32,7 +40,7 @@ class EventListeners private constructor(val name: String, private val isGeneric prefix = "(", postfix = ")", separator = ", ", - transform = Class<*>::getTypeName + transform = Class<*>::getTypeName, ) }" listeners.add(Listener(name, createEventConsumer(name, instance, method), options, generic)) @@ -52,7 +60,7 @@ class EventListeners private constructor(val name: String, private val isGeneric MethodType.methodType(Consumer::class.java, instance::class.java), MethodType.methodType(Nothing::class.javaPrimitiveType, Object::class.java), handle, - MethodType.methodType(Nothing::class.javaPrimitiveType, method.parameterTypes[0]) + MethodType.methodType(Nothing::class.javaPrimitiveType, method.parameterTypes[0]), ).target.bindTo(instance).invokeExact() as Consumer<Any> } catch (e: Throwable) { throw IllegalArgumentException("Method $name is not a valid consumer", e) diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt index 7f5b42e3c..59726be51 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt @@ -856,7 +856,7 @@ object DamageIndicatorManager { } @HandleEvent - fun onEntityJoin(event: EntityEnterWorldEvent<Entity>) { + fun onEntityJoin(event: EntityEnterWorldEvent<*>) { mobFinder?.handleNewEntity(event.entity) } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/ReflectionUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/ReflectionUtils.kt index f714ebc97..cc6251a17 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/ReflectionUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/ReflectionUtils.kt @@ -3,6 +3,10 @@ package at.hannibal2.skyhanni.utils import java.lang.reflect.Constructor import java.lang.reflect.Field import java.lang.reflect.Modifier +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.TypeVariable +import java.lang.reflect.WildcardType import kotlin.properties.ReadWriteProperty import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty @@ -65,4 +69,65 @@ object ReflectionUtils { } fun Class<*>.getDeclaredFieldOrNull(name: String): Field? = declaredFields.firstOrNull { it.name == name } + + + /** + * Resolve all super class generic type parameters to their respective bound types in the class inheriting them. + * Note that this is only done once, so a class declaration like + * ```kotlin + * class Parent<ParentT> + * class Child<OtherT> : Parent<OtherT> + * class GrandChild : Child<String> + * ``` + * would result in `mapOf(OtherT to String, OtherT to ParentT)`. Variables bound to variables need to be manually unraveled. + * Note also that wild cards like + * ```kotlin + * class WildChild : Parent<out String> + * ``` + * are left untouched: `mapOf(ParentT to WildCardType(arrayOf(String), arrayOf()))` + */ + fun findSuperClassTypeParameters( + type: Type?, + universe: MutableMap<TypeVariable<*>, Type>, // TODO: this could go with a (owner, name) tuple key instead + ) { + when (type) { + is ParameterizedType -> { + val rawType = type.rawType as Class<*> // TODO check + rawType.typeParameters.zip(type.actualTypeArguments).associateTo(universe) { it } + findSuperClassTypeParameters(rawType.genericSuperclass, universe) + } + + is Class<*> -> { + findSuperClassTypeParameters(type.genericSuperclass, universe) + } + + is TypeVariable<*> -> { + findSuperClassTypeParameters(universe[type] ?: return, universe) + } + } + } + + /** + * Resolve the upper bound of a type variable from a child classes type parameters using [findSuperClassTypeParameters]. + * + * This method performs the mentioned resolving of type parameters and wild card resolutions. + * Note that the returned class may not actually be allowed by all bounds along the chain, so might be a super class of + * what you would expect. + */ + fun resolveUpperBoundSuperClassGenericParameter(type: Type, variable: TypeVariable<*>): Class<*>? { + val universe = mutableMapOf<TypeVariable<*>, Type>() + findSuperClassTypeParameters(type, universe) + var p: Type = variable + while (true) { + if (p is TypeVariable<*>) { + p = universe[p] ?: return null + } else if (p is WildcardType) { + p = p.upperBounds[0] + } else if (p is Class<*>) { + return p + } else { + return null + } + } + } } diff --git a/src/test/java/at/hannibal2/skyhanni/test/utils/ReflectUtilsTest.kt b/src/test/java/at/hannibal2/skyhanni/test/utils/ReflectUtilsTest.kt new file mode 100644 index 000000000..106f3aeb0 --- /dev/null +++ b/src/test/java/at/hannibal2/skyhanni/test/utils/ReflectUtilsTest.kt @@ -0,0 +1,39 @@ +package at.hannibal2.skyhanni.test.utils + +import at.hannibal2.skyhanni.utils.ReflectionUtils +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.lang.reflect.ParameterizedType +import java.lang.reflect.TypeVariable + +class ReflectUtilsTest { + class SomeClass + open class Parent<A, B> + open class Child<A, B> : Parent<B, A>() + open class GrandChild<T> : Child<String, T>() + + abstract class Holder<X> + + inline fun <reified T> resolve(typeParam: TypeVariable<*>): Class<*>? { + return ReflectionUtils.resolveUpperBoundSuperClassGenericParameter( + (object : Holder<T>() {} + .javaClass.genericSuperclass as ParameterizedType) + .actualTypeArguments[0], + typeParam, + ) + } + + @Test + fun testResolveUpperBoundSuperClassGenericParameter() { + val firstParent = Parent::class.java.typeParameters[0] + val secondParent = Parent::class.java.typeParameters[1] + Assertions.assertEquals(String::class.java, resolve<Parent<String, Int>>(firstParent)) + Assertions.assertEquals(Integer::class.java, resolve<Parent<String, Int>>(secondParent)) + Assertions.assertEquals(Integer::class.java, resolve<Child<String, Int>>(firstParent)) + Assertions.assertEquals(String::class.java, resolve<Child<String, Int>>(secondParent)) + Assertions.assertEquals(SomeClass::class.java, resolve<GrandChild<SomeClass>>(firstParent)) + Assertions.assertEquals(String::class.java, resolve<GrandChild<SomeClass>>(secondParent)) + Assertions.assertEquals(SomeClass::class.java, resolve<GrandChild<out SomeClass>>(firstParent)) + Assertions.assertEquals(Any::class.java, resolve<GrandChild<*>>(firstParent)) + } +} |