Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend: Allow Parameterless Handling of SkyHanniEvent(s) #3064

Open
wants to merge 5 commits into
base: beta
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .live-plugins/event/plugin.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes

val skyhanniEvent = "at.hannibal2.skyhanni.api.event.SkyHanniEvent"
val handleEvent = "HandleEvent"
val eventType = "eventType"

registerInspection(HandleEventInspectionKotlin())

Expand All @@ -28,16 +29,33 @@ class HandleEventInspectionKotlin : AbstractKotlinInspection() {
val visitor = object : KtVisitorVoid() {
override fun visitNamedFunction(function: KtNamedFunction) {
val hasEventAnnotation = function.annotationEntries.any { it.shortName!!.asString() == handleEvent }

// Check if the function's parameter is a SkyHanniEvent or its subtype
val isEvent = function.valueParameters.firstOrNull()?.type()?.supertypes()
?.any { it.fqName?.asString() == skyhanniEvent } ?: false

// Find the annotation entry
val annotationEntry = function.annotationEntries
.find { it.shortName!!.asString() == handleEvent }

// Check if the annotation specifies the eventType explicitly or as a positional parameter
val hasEventType = annotationEntry?.valueArguments
?.any { argument ->
// Check if it is a named parameter for `eventType`
argument.getArgumentName()?.asName?.asString() == "eventType" ||
// Check if it is a positional argument (first argument)
(annotationEntry.valueArguments.indexOf(argument) == 0 &&
argument.getArgumentExpression()?.text != null)
} ?: false

// Validate function annotation and parameters
if (isEvent && !hasEventAnnotation && function.valueParameters.size == 1 && function.isPublic) {
holder.registerProblem(
function,
"Event handler function should be annotated with @HandleEvent",
HandleEventQuickFix()
)
} else if (!isEvent && hasEventAnnotation) {
} else if (!isEvent && !hasEventType && hasEventAnnotation) {
holder.registerProblem(
function,
"Function should not be annotated with @HandleEvent if it does not take a SkyHanniEvent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ class ModuleProcessor(private val codeGenerator: CodeGenerator, private val logg
}

if (function.annotations.any { it.shortName.asString() == "HandleEvent" }) {
val firstParameter = function.parameters.firstOrNull()?.type?.resolve()!!
if (!skyHanniEvent!!.isAssignableFrom(firstParameter)) {
val firstParameter = function.parameters.firstOrNull()?.type?.resolve()
val handleEventAnnotation = function.annotations.find { it.shortName.asString() == "HandleEvent" }
val eventType = handleEventAnnotation?.arguments?.find { it.name?.asString() == "eventType" }?.value
val isFirstParameterProblem = firstParameter == null && eventType == null
val notAssignable = firstParameter != null && !skyHanniEvent!!.isAssignableFrom(firstParameter)

if (isFirstParameterProblem || notAssignable) {
warnings.add("Function in $className must have an event assignable from $skyHanniEvent because it is annotated with @HandleEvent")
}
}
Expand Down
72 changes: 53 additions & 19 deletions src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,63 @@ class EventListeners private constructor(val name: String, private val isGeneric
)

fun addListener(method: Method, instance: Any, options: HandleEvent) {
require(method.parameterCount == 1)
val generic: Class<*>? = if (isGeneric) {
val name = buildListenerName(method)
val eventConsumer = createEventConsumer(method, instance, options)
val generic = if (isGeneric) resolveGenericType(method) else null

listeners.add(Listener(name, eventConsumer, options, generic))
}

private fun buildListenerName(method: Method): String {
val paramTypesString = method.parameterTypes.joinTo(
StringBuilder(),
prefix = "(",
postfix = ")",
separator = ", ",
transform = Class<*>::getTypeName
).toString()

return "${method.declaringClass.name}.${method.name}$paramTypesString"
}

private fun createEventConsumer(method: Method, instance: Any, options: HandleEvent): (Any) -> Unit {
return when (method.parameterCount) {
0 -> createZeroParameterConsumer(method, instance, options)
1 -> createSingleParameterConsumer(method, instance)
else -> throw IllegalArgumentException(
"Method ${method.name} must have either 0 or 1 parameters."
)
}
}

private fun createZeroParameterConsumer(method: Method, instance: Any, options: HandleEvent): (Any) -> Unit {
require(options.eventType != SkyHanniEvent::class) {
"Method ${method.name} has no parameters but no eventType was provided in the annotation."
}
val eventType = options.eventType.java
require(SkyHanniEvent::class.java.isAssignableFrom(eventType)) {
"eventType in @HandleEvent must extend SkyHanniEvent. Provided: $eventType"
}
return { _: Any -> method.invoke(instance) }
}

private fun createSingleParameterConsumer(method: Method, instance: Any): (Any) -> Unit {
require(SkyHanniEvent::class.java.isAssignableFrom(method.parameterTypes[0])) {
"Method ${method.name} parameter must be a subclass of SkyHanniEvent."
}
return { event -> method.invoke(instance, event) }
}

private fun resolveGenericType(method: Method): Class<*> =
method.genericParameterTypes.getOrNull(0)?.let { genericType ->
ReflectionUtils.resolveUpperBoundSuperClassGenericParameter(
method.genericParameterTypes[0],
GenericSkyHanniEvent::class.java.typeParameters[0],
genericType,
GenericSkyHanniEvent::class.java.typeParameters[0]
) ?: error(
"Generic event handler type parameter is not present in " +
"event class hierarchy for type ${method.genericParameterTypes[0]}",
"event class hierarchy for type $genericType"
)
} else {
null
}
val name = "${method.declaringClass.name}.${method.name}${
method.parameterTypes.joinTo(
StringBuilder(),
prefix = "(",
postfix = ")",
separator = ", ",
transform = Class<*>::getTypeName,
)
}"
listeners.add(Listener(name, createEventConsumer(name, instance, method), options, generic))
}
} ?: error("Method ${method.name} does not have a generic parameter type.")

/**
* Creates a consumer using LambdaMetafactory, this is the most efficient way to reflectively call
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package at.hannibal2.skyhanni.api.event

import at.hannibal2.skyhanni.data.IslandType
import kotlin.reflect.KClass

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class HandleEvent(
/**
* For cases where the event properties are themselves not needed, and solely a listener for an event fire suffices.
*/
val eventType: KClass<out SkyHanniEvent> = SkyHanniEvent::class,

/**
* If the event should only be received while on SkyBlock.
*/
Expand Down Expand Up @@ -32,7 +38,6 @@ annotation class HandleEvent(
*/
val receiveCancelled: Boolean = false,
) {

companion object {
const val HIGHEST = -2
const val HIGH = -1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ object SkyHanniEvents {

@Suppress("UNCHECKED_CAST")
private fun registerMethod(method: Method, instance: Any) {
if (method.parameterCount != 1) return
val options = method.getAnnotation(HandleEvent::class.java) ?: return
val event = method.parameterTypes[0]
if (!SkyHanniEvent::class.java.isAssignableFrom(event)) return
listeners.getOrPut(event as Class<SkyHanniEvent>) { EventListeners(event) }
val eventType = method.parameterTypes.getOrNull(0) ?: options.eventType.java
if (!SkyHanniEvent::class.java.isAssignableFrom(eventType)) return
listeners.getOrPut(eventType as Class<SkyHanniEvent>) { EventListeners(eventType) }
.addListener(method, instance, options)
}

Expand Down
Loading