From d1a6c021fcbef245e34d9ae5fd98c724fb2dda99 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Fri, 24 Oct 2025 22:35:18 +0200 Subject: [PATCH] chore: improve readability of method retrieval --- .../bukkit/schematic/StateWrapperSpigot.java | 25 +++--- .../core/util/ReflectionHelper.java | 76 ++++++++++++++++++ .../core/util/ReflectionHelperTest.java | 79 +++++++++++++++++++ 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java create mode 100644 Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 4e6f390d6..000d707d5 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -18,6 +18,7 @@ */ package com.plotsquared.bukkit.schematic; +import com.plotsquared.core.util.ReflectionHelper; import com.plotsquared.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.bukkit.WorldEditPlugin; @@ -236,13 +237,11 @@ final class StateWrapperSpigot implements StateWrapper { private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class craftBlockEntityStateClass) throws NoSuchMethodException, IllegalAccessException { - for (final Method method : craftBlockEntityStateClass.getMethods()) { - if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 && - method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) { - return LOOKUP.unreflect(method); - } - } - throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName()); + return LOOKUP.unreflect(ReflectionHelper.findMethod( + craftBlockEntityStateClass, + MethodType.methodType(Boolean.TYPE, Boolean.TYPE, Boolean.TYPE), + Modifier.PUBLIC + ).orElseThrow(() -> new NoSuchMethodException("Couldn't lookup CraftBlockEntityState#update(boolean, boolean) boolean"))); } private static MethodHandle findCraftBlockEntityStateSnapshotMethodHandle(Class craftBlockEntityStateClass) throws @@ -254,13 +253,11 @@ final class StateWrapperSpigot implements StateWrapper { private static MethodHandle findSignBlockEntitySetTextMethodHandle(Class signBlockEntity, Class signText) throws NoSuchMethodException, IllegalAccessException { - for (final Method method : signBlockEntity.getMethods()) { - if (method.getReturnType() == Boolean.TYPE && method.getParameterCount() == 2 - && method.getParameterTypes()[0] == signText && method.getParameterTypes()[1] == Boolean.TYPE) { - return LOOKUP.unreflect(method); - } - } - throw new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"); + return LOOKUP.unreflect(ReflectionHelper.findMethod( + signBlockEntity, + MethodType.methodType(Boolean.TYPE, signText, Boolean.TYPE), + Modifier.PUBLIC + ).orElseThrow(() -> new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"))); } } diff --git a/Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java b/Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java new file mode 100644 index 000000000..c24ad06ea --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java @@ -0,0 +1,76 @@ +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.util; + +import org.jetbrains.annotations.ApiStatus; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.util.Optional; + +@ApiStatus.Internal +public final class ReflectionHelper { + + /** + * Find a (declared) method with an unknown or potentially obfuscated name by its signature and optional modifiers. + *
+ * The method - if private - is not made accessible. Either call {@link Method#setAccessible(boolean)} or + * use a {@link java.lang.invoke.MethodHandles.Lookup#privateLookupIn(Class, MethodHandles.Lookup) private lookup}. + * + * @param holder The class providing the method. + * @param signature The signature of the method, identified by parameter types and the return type. + * @param modifiers All possible modifiers of the method that should be validated. + * @return The method, if one has been found. Otherwise, an empty Optional. + * @throws RuntimeException if multiple matching methods have been found. + * @see java.lang.reflect.Modifier + */ + public static Optional findMethod(Class holder, MethodType signature, int... modifiers) { + Method found = null; + outer: + for (final Method method : holder.getDeclaredMethods()) { + if (method.getParameterCount() != signature.parameterCount()) { + continue; + } + if (!signature.returnType().isAssignableFrom(method.getReturnType())) { + continue; + } + + for (final int modifier : modifiers) { + if ((method.getModifiers() & modifier) == 0) { + continue outer; + } + } + + Class[] parameterTypes = signature.parameterArray(); + for (int i = 0; i < parameterTypes.length; i++) { + // validate expected parameter is either the same type or subtype of actual parameter + if (!parameterTypes[i].isAssignableFrom(method.getParameterTypes()[i])) { + continue outer; + } + } + if (found != null) { + throw new RuntimeException("Found ambiguous method by selector: " + method + " vs " + found); + } + found = method; + } + return Optional.ofNullable(found); + } + +} diff --git a/Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java b/Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java new file mode 100644 index 000000000..4be21e490 --- /dev/null +++ b/Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java @@ -0,0 +1,79 @@ +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.util; + +import org.junit.jupiter.api.Test; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Modifier; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ReflectionHelperTest { + + @Test + void findMethod() throws NoSuchMethodException { + assertThrows( + RuntimeException.class, () -> + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType(String.class)) + ); + assertEquals( + MethodTesterClass.class.getMethod("methodThree"), + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType(String.class), Modifier.PUBLIC) + .orElse(null) + ); + assertEquals( + MethodTesterClass.class.getDeclaredMethod("methodFour", String.class, Collection.class), + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType( + String.class, String.class, Collection.class + )).orElse(null) + ); + // check that helper allows super classes of parameters when searching + assertEquals( + MethodTesterClass.class.getDeclaredMethod("methodFour", String.class, Collection.class), + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType( + String.class, String.class, Object.class + )).orElse(null) + ); + } + + @SuppressWarnings("unused") + private static class MethodTesterClass { + + private static String methodOne() { + return ""; + } + + private static String methodTwo() { + return ""; + } + + public static String methodThree() { + return ""; + } + + protected static String methodFour(String param, Collection paramList) { + return ""; + } + + } + +}