From ee8f5f999305c0d1bff2515639296a070726a8b9 Mon Sep 17 00:00:00 2001 From: Crypto Morin Date: Wed, 1 Jan 2025 21:34:08 -0800 Subject: [PATCH] v13.0.0 * Added XProxifier for XReflection IV * **[XReflection]** Using the suffix support for JetBrains `@Language` annotation, we no longer need to add semicolons or `{}` at the end of string APIs. Other than the readability improvement, this results in a slight performance improvement as well, since fewer characters are needed for parsing. * Improved Unit Tests. * Added `@Contract` annotations to most APIs. * **[XTag]** Fixed `INVENTORY_NOT_DISPLAYABLE` for wheat. --- TODO.md | 42 +- pom.xml | 2 +- .../com/cryptomorin/xseries/XItemStack.java | 58 +- .../java/com/cryptomorin/xseries/XPotion.java | 4 +- .../java/com/cryptomorin/xseries/XTag.java | 6 +- .../xseries/profiles/builder/XSkull.java | 12 +- .../xseries/reflection/XReflection.java | 28 +- .../xseries/reflection/asm/ASMAnalyzer.java | 2 + .../reflection/asm/ASMClassLoader.java | 18 +- .../xseries/reflection/asm/ASMVersion.java | 1 + .../reflection/asm/ArrayInsnGenerator.java | 13 +- .../xseries/reflection/asm/XReflectASM.java | 107 +++- .../constraint/ReflectiveConstraint.java | 8 +- .../reflection/jvm/FieldMemberHandle.java | 6 +- .../reflection/jvm/classes/ClassHandle.java | 32 +- .../reflection/proxy/OverloadedMethod.java | 2 +- .../reflection/proxy/ReflectiveProxy.java | 57 +- .../proxy/ReflectiveProxyObject.java | 38 +- .../annotations/{Class.java => Proxify.java} | 2 +- .../proxy/generator/XProxifier.java | 494 ++++++++++++++++++ .../proxy/processors/ProxyMethodInfo.java | 7 + .../ReflectiveAnnotationProcessor.java | 16 +- .../cryptomorin/xseries/test/Constants.java | 15 +- .../xseries/test/XSeriesTests.java | 13 +- .../ReflectionBenchmarkTargetMethodProxy.java | 4 +- .../reflection/asm/ASMGeneratedSample.java | 12 + .../xseries/test/reflection/asm/ASMTests.java | 9 +- .../test/reflection/proxy/ProxyTestClass.java | 27 +- .../reflection/proxy/ProxyTestProcessor.java | 37 ++ .../reflection/proxy/ProxyTestProxified.java | 17 +- .../test/reflection/proxy/ProxyTests.java | 36 +- .../test/reflection/proxy/TestAnnotation.java | 44 ++ .../reflection/proxy/TestAnnotation2.java | 34 ++ .../reflection/proxy/TestAnnotationList.java | 34 ++ .../xseries/test/writer/DifferenceHelper.java | 4 +- src/test/resources/server.properties | 4 +- 36 files changed, 1122 insertions(+), 123 deletions(-) rename src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/{Class.java => Proxify.java} (98%) create mode 100644 src/main/java/com/cryptomorin/xseries/reflection/proxy/generator/XProxifier.java create mode 100644 src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProcessor.java create mode 100644 src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation.java create mode 100644 src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation2.java create mode 100644 src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotationList.java diff --git a/TODO.md b/TODO.md index 41d8cf06..61a27eba 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,8 @@ This file simply highlighs things that need to be done in future releases or oth solution is yet to be found. It's also a simple list of planned decisions for the future of the project that other developers can see and perhaps give suggestions about. Anyone is welcome to complete any of the listed issues. +* **[XMaterial]** We have to convert this into a XModule/XRegistry class soon. Wait and see what Spigot/Pape does first. + * **[Unit Tests]** Improve Maven's unit testing (Read #149 issue for more info.) * **[XReflectASM/ReflectiveProxy]** Add more benchmarking for XReflection Stage III & IV with different method @@ -22,6 +24,39 @@ developers can see and perhaps give suggestions about. Anyone is welcome to comp of using the annotations. This is useful for situations where annotations will seem bulky or the data is more complex and must be calculated during runtime. +* **[ReflectiveProxyObject]** Add a way to ignore certain methods if they're not supported in a specific + version instead of throwing an error (related to the issue below) + +* **[ReflectiveProxyObject]** Add some sort of `boolean isSupported(String method, MethodType type)` to + **ReflectiveProxyObject**. So we can test whether a method is supported in the current version or not. + If we could make the method signature more simple, that'd be nice as well. For example, we could take the + string approach and do `boolean isSupported(String signature)` to be used + like `isSupported("private method(int a, String b, ...)")` that could work. A more strict approach using the + already existing interface would've been nicer. For example, in JavaScript we could reference the method + itself and add properties to it. I guess an ideal approach would be how you link methods in javadocs. + +* **[ReflectiveProxyObject]** Add a simple enum class scanner that uses `@Proxify` and `@ReflectName` annotations to + parse an enum class and map the correct values to field. How this value gets saved should be handled by + an interface class like `interface A { void mapValue(Object) }` which our enum implements. Then we could + also add a parent interface for **ReflectiveProxyObject** that enums also share, allowing us to use its + `instance()` method. Which also means that we can use our enum class in other proxy methods. + +* **[ReflectiveProxyObject]** Add interface inheritance support. (This might already be a thing, we just need to test) + +* **[ReflectiveProxyObject]** Add support for inner classes. + +* **[ReflectiveProxyObject]** Create a Maven and Gradle plugin for real class support that remaps fields, constructors + and static member accesses to proxy access. + +* **[XProxifier]** Implement configurable settings. (Read the TODO list inside that class for more info.) + +* **[XProxifier]** Turn this into an IntelliJ plugin that allows proxifying any compiled file. Will probably + need ASM to read the class file due to class loader issues? Not sure, needs to be tested. + +* **[XProxifier]** Add a way to automatically generate mappings for different versions of a proxified class. + We might define download links to mapping files, download the needed mapping, parse it and generate a class + container for it? + * **[XTag]** Inline all fields. Using a `static {}` block is unnecessary and makes things really hard to track. Currently, this is not possible using IntelliJ's `Refactor -> Inline Field` feature, because you'll get a `No initializer present for the field` error. Not sure if this is a bug or some sort of tricky feature. @@ -33,6 +68,7 @@ developers can see and perhaps give suggestions about. Anyone is welcome to comp including the current version and methods to enable/disable certain features that are currently handled by system properties? One thing we could do is to add an option to enable debug mode, certain exceptions that are normally suppressed are printed. + * **[General]** Don't forget to remove pre-12.0.0 deprecated codes in MC v1.22 * **[General]** Add a guide for Maven and Gradle users on how to properly exclude XSeries XReflection's III and IV proxy @@ -44,7 +80,5 @@ developers can see and perhaps give suggestions about. Anyone is welcome to comp have to spend a lot of time to design one. * **[Documentation]** While the javadocs are pretty comprehensive for most classes, they're mostly flooded with small - and - technical details that most developers don't have to be concerned about. We should make a guide on the wiki with - screenshots - and a general overview of all the features which makes it much easier for developers to get started. \ No newline at end of file + and technical details that most developers don't have to be concerned about. We should make a guide on the wiki with + screenshots and a general overview of all the features which makes it much easier for developers to get started. \ No newline at end of file diff --git a/pom.xml b/pom.xml index b529940f..67a0eea1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.cryptomorin XSeries - 12.1.0 + 13.0.0 XSeries A set of utilities for Minecraft plugins diff --git a/src/main/java/com/cryptomorin/xseries/XItemStack.java b/src/main/java/com/cryptomorin/xseries/XItemStack.java index 858361b7..a46a7d26 100644 --- a/src/main/java/com/cryptomorin/xseries/XItemStack.java +++ b/src/main/java/com/cryptomorin/xseries/XItemStack.java @@ -42,10 +42,7 @@ import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.TropicalFish; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemFlag; -import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.*; import org.bukkit.inventory.meta.*; import org.bukkit.inventory.meta.trim.ArmorTrim; import org.bukkit.inventory.meta.trim.TrimMaterial; @@ -55,8 +52,10 @@ import org.bukkit.material.SpawnEgg; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionType; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; import java.util.*; import java.util.function.BiPredicate; @@ -77,7 +76,7 @@ * ItemStack * * @author Crypto Morin - * @version 7.5.1 + * @version 7.5.2 * @see XMaterial * @see XPotion * @see XSkull @@ -1236,6 +1235,7 @@ public static Color parseColor(@Nullable String str) { * @since 2.0.1 */ @NotNull + @Contract(mutates = "param1") public static List giveOrDrop(@NotNull Player player, @Nullable ItemStack... items) { return giveOrDrop(player, false, items); } @@ -1250,6 +1250,7 @@ public static List giveOrDrop(@NotNull Player player, @Nullable ItemS * @since 2.0.1 */ @NotNull + @Contract(mutates = "param1") public static List giveOrDrop(@NotNull Player player, boolean split, @Nullable ItemStack... items) { if (items == null || items.length == 0) return new ArrayList<>(); List leftOvers = addItems(player.getInventory(), split, items); @@ -1260,6 +1261,7 @@ public static List giveOrDrop(@NotNull Player player, boolean split, return leftOvers; } + @Contract(mutates = "param1") public static List addItems(@NotNull Inventory inventory, boolean split, @NotNull ItemStack... items) { return addItems(inventory, split, null, items); } @@ -1277,7 +1279,9 @@ public static List addItems(@NotNull Inventory inventory, boolean spl * @return items that didn't fit in the inventory. * @since 4.0.0 */ + @NotNull + @Contract(mutates = "param1") public static List addItems(@NotNull Inventory inventory, boolean split, @Nullable Predicate modifiableSlots, @NotNull ItemStack... items) { Objects.requireNonNull(inventory, "Cannot add items to null inventory"); @@ -1344,6 +1348,9 @@ public static List addItems(@NotNull Inventory inventory, boolean spl return leftOvers; } + @NotNull + @Contract(pure = true) + @Range(from = -1, to = Integer.MAX_VALUE) public static int firstPartial(@NotNull Inventory inventory, @Nullable ItemStack item, int beginIndex) { return firstPartial(inventory, item, beginIndex, null); } @@ -1361,6 +1368,9 @@ public static int firstPartial(@NotNull Inventory inventory, @Nullable ItemStack * @throws IndexOutOfBoundsException if the beginning index is less than 0 or greater than the inventory storage size. * @since 4.0.0 */ + @NotNull + @Contract(pure = true) + @Range(from = -1, to = Integer.MAX_VALUE) public static int firstPartial(@NotNull Inventory inventory, @Nullable ItemStack item, int beginIndex, @Nullable Predicate modifiableSlots) { if (item != null) { ItemStack[] items = getStorageContents(inventory); @@ -1378,6 +1388,8 @@ public static int firstPartial(@NotNull Inventory inventory, @Nullable ItemStack return -1; } + @NotNull + @Contract(pure = true) public static List stack(@NotNull Collection items) { return stack(items, ItemStack::isSimilar); } @@ -1396,6 +1408,7 @@ public static List stack(@NotNull Collection items) { * @since 4.0.0 */ @NotNull + @Contract(pure = true) public static List stack(@NotNull Collection items, @NotNull BiPredicate similarity) { Objects.requireNonNull(items, "Cannot stack null items"); Objects.requireNonNull(similarity, "Similarity check cannot be null"); @@ -1418,6 +1431,8 @@ public static List stack(@NotNull Collection items, @NotNu return stacked; } + @Contract(pure = true) + @Range(from = -1, to = Integer.MAX_VALUE) public static int firstEmpty(@NotNull Inventory inventory, int beginIndex) { return firstEmpty(inventory, beginIndex, null); } @@ -1434,6 +1449,8 @@ public static int firstEmpty(@NotNull Inventory inventory, int beginIndex) { * @throws IndexOutOfBoundsException if the beginning index is less than 0 or greater than the inventory storage size. * @since 4.0.0 */ + @Contract(pure = true) + @Range(from = -1, to = Integer.MAX_VALUE) public static int firstEmpty(@NotNull Inventory inventory, int beginIndex, @Nullable Predicate modifiableSlots) { ItemStack[] items = getStorageContents(inventory); int invSize = items.length; @@ -1458,6 +1475,8 @@ public static int firstEmpty(@NotNull Inventory inventory, int beginIndex, @Null * @see #firstPartial(Inventory, ItemStack, int) * @since 4.2.0 */ + @Contract(pure = true) + @Range(from = -1, to = Integer.MAX_VALUE) public static int firstPartialOrEmpty(@NotNull Inventory inventory, @Nullable ItemStack item, int beginIndex) { if (item != null) { ItemStack[] items = getStorageContents(inventory); @@ -1474,6 +1493,10 @@ public static int firstPartialOrEmpty(@NotNull Inventory inventory, @Nullable It return -1; } + /** + * Cross-version compatible version of {@link Inventory#getStorageContents()}. + */ + @Contract(pure = true) public static ItemStack[] getStorageContents(Inventory inventory) { // Mojang divides player inventory like this: // public final ItemStack[] items = new ItemStack[36]; @@ -1489,6 +1512,31 @@ public static ItemStack[] getStorageContents(Inventory inventory) { } } + /** + * @see #isEmpty(ItemStack) + * @since 7.5.2 + */ + @Contract(pure = true) + public static boolean notEmpty(@Nullable ItemStack item) { + return !isEmpty(item); + } + + /** + * Checks if this item is {@code null} or {@link Material#AIR}. + * The latter can only happen in the following situations: + *
    + *
  • {@link PlayerInventory#getItemInMainHand()}
  • + *
  • {@link PlayerInventory#getItemInOffHand()}
  • + *
+ * + * @see #notEmpty(ItemStack) + * @since 7.5.2 + */ + @Contract(pure = true) + public static boolean isEmpty(@Nullable ItemStack item) { + return item == null || item.getType() == Material.AIR; + } + public static class MaterialCondition extends RuntimeException { protected XMaterial solution; diff --git a/src/main/java/com/cryptomorin/xseries/XPotion.java b/src/main/java/com/cryptomorin/xseries/XPotion.java index 2a12e201..44beb5ab 100644 --- a/src/main/java/com/cryptomorin/xseries/XPotion.java +++ b/src/main/java/com/cryptomorin/xseries/XPotion.java @@ -333,8 +333,8 @@ private static List split(@NotNull String str, @SuppressWarnings("SamePa * Format: Potion, Duration (in seconds), Amplifier (level) [%chance] *
      *     WEAKNESS, 30, 1
-     *     SLOWNESS 200 10
-     *     1, 10000, 100 %50
+     *     SLOWNESS, 200, 10
+     *     1, 10000, 100, %50
      * 
* The last argument can also include a chance (written in percent) which if not met, returns null. * diff --git a/src/main/java/com/cryptomorin/xseries/XTag.java b/src/main/java/com/cryptomorin/xseries/XTag.java index 299b0cdb..6aa84d50 100644 --- a/src/main/java/com/cryptomorin/xseries/XTag.java +++ b/src/main/java/com/cryptomorin/xseries/XTag.java @@ -2436,8 +2436,10 @@ public final class XTag> { ) .inheritFrom( AIR, CAVE_VINES, FILLED_CAULDRONS, FIRE, FLUID, PORTALS, - WALL_SIGNS, WALL_HANGING_SIGNS, WALL_TORCHES, ALIVE_CORAL_WALL_FANS, DEAD_CORAL_WALL_FANS, WALL_HEADS, - CANDLE_CAKES, WALL_BANNERS, FLOWER_POTS.without(XMaterial.FLOWER_POT), CROPS.without(XMaterial.WHEAT) + WALL_SIGNS, WALL_HANGING_SIGNS, WALL_TORCHES, ALIVE_CORAL_WALL_FANS, + DEAD_CORAL_WALL_FANS, WALL_HEADS, CANDLE_CAKES, WALL_BANNERS, + FLOWER_POTS.without(XMaterial.FLOWER_POT), + CROPS.without(XMaterial.WHEAT_SEEDS, XMaterial.WHEAT) ).build(); } diff --git a/src/main/java/com/cryptomorin/xseries/profiles/builder/XSkull.java b/src/main/java/com/cryptomorin/xseries/profiles/builder/XSkull.java index 62a020b8..608174d5 100644 --- a/src/main/java/com/cryptomorin/xseries/profiles/builder/XSkull.java +++ b/src/main/java/com/cryptomorin/xseries/profiles/builder/XSkull.java @@ -85,7 +85,7 @@ public final class XSkull { * @return A {@link ProfileInstruction} that sets the profile for the generated {@link ItemStack}. */ @NotNull - @Contract(pure = true) + @Contract(value = "-> new", pure = true) public static ProfileInstruction createItem() { return of(XMaterial.PLAYER_HEAD.parseItem()); } @@ -97,7 +97,7 @@ public static ProfileInstruction createItem() { * @return A {@link ProfileInstruction} that sets the profile for the given {@link ItemStack}. */ @NotNull - @Contract(pure = true) + @Contract(value = "_ -> new", pure = true) public static ProfileInstruction of(@NotNull ItemStack stack) { return new ProfileInstruction<>(new ProfileContainer.ItemStackProfileContainer(stack)); } @@ -109,7 +109,7 @@ public static ProfileInstruction of(@NotNull ItemStack stack) { * @return An {@link ProfileInstruction} that sets the profile for the given {@link ItemMeta}. */ @NotNull - @Contract(pure = true) + @Contract(value = "_ -> new", pure = true) public static ProfileInstruction of(@NotNull ItemMeta meta) { return new ProfileInstruction<>(new ProfileContainer.ItemMetaProfileContainer((SkullMeta) meta)); } @@ -121,7 +121,7 @@ public static ProfileInstruction of(@NotNull ItemMeta meta) { * @return An {@link ProfileInstruction} that sets the profile for the given {@link Block}. */ @NotNull - @Contract(pure = true) + @Contract(value = "_ -> new", pure = true) public static ProfileInstruction of(@NotNull Block block) { return new ProfileInstruction<>(new ProfileContainer.BlockProfileContainer(block)); } @@ -133,7 +133,7 @@ public static ProfileInstruction of(@NotNull Block block) { * @return An {@link ProfileInstruction} that sets the profile for the given {@link BlockState}. */ @NotNull - @Contract(pure = true) + @Contract(value = "_ -> new", pure = true) public static ProfileInstruction of(@NotNull BlockState state) { return new ProfileInstruction<>(new ProfileContainer.BlockStateProfileContainer((Skull) state)); } @@ -156,7 +156,7 @@ public static ProfileInstruction of(@NotNull BlockState state) { * @return A clone of the default {@link GameProfile}. */ @NotNull - @Contract(pure = true) + @Contract(value = "-> new", pure = true) protected static Profileable getDefaultProfile() { // We copy this just in case something changes the GameProfile properties. GameProfile clone = PlayerProfiles.createGameProfile(DEFAULT_PROFILE.getId(), DEFAULT_PROFILE.getName()); diff --git a/src/main/java/com/cryptomorin/xseries/reflection/XReflection.java b/src/main/java/com/cryptomorin/xseries/reflection/XReflection.java index 3131f733..503ea413 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/XReflection.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/XReflection.java @@ -27,6 +27,7 @@ import com.cryptomorin.xseries.reflection.asm.XReflectASM; import com.cryptomorin.xseries.reflection.constraint.ReflectiveConstraint; import com.cryptomorin.xseries.reflection.jvm.MethodMemberHandle; +import com.cryptomorin.xseries.reflection.jvm.classes.ClassHandle; import com.cryptomorin.xseries.reflection.jvm.classes.DynamicClassHandle; import com.cryptomorin.xseries.reflection.jvm.classes.StaticClassHandle; import com.cryptomorin.xseries.reflection.minecraft.MinecraftClassHandle; @@ -36,10 +37,7 @@ import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxy; import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxyObject; import org.bukkit.Bukkit; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.*; import java.util.*; import java.util.concurrent.Callable; @@ -99,14 +97,14 @@ * It's accessed from {@link XReflection#proxify(Class)}. * *
  • - * Stage IV (ASM Mode): This is simply Stage III API, however instead of relying on + * Stage IV (ASMification): This is simply Stage III API, however instead of relying on * Java's proxy system, an ASM-assisted code generation occurs in a new class loader, which makes this * almost as fast as direct calls, even for inaccessible private methods due to the use of * {@link java.lang.invoke.MethodHandle#invokeExact(Object...)} with polymorphic signature. * This system is only used if at least ASM9 is detected during runtime (supports shading as well) *
  • *
  • - * Stage V (Magic ASM Mode): ??? + * Stage V (Compile-Time Remapper): ??? *
  • * * @@ -171,7 +169,7 @@ public final class XReflection { * It's simply enough for the property to be present, but you can also specify a Minecraft version * for the class to use for {@link #supports(int)} and other similar version checking methods. */ - @ApiStatus.Internal + @TestOnly public static final String DISABLE_MINECRAFT_CAPABILITIES_PROPERTY = "xseries.xreflection.disable.minecraft"; /** @@ -340,6 +338,7 @@ public static String getVersionInformation() { * @since 7.0.0 */ @Nullable + @Contract(pure = true) public static Integer getLatestPatchNumberOf(int minorVersion) { if (minorVersion <= 0) throw new IllegalArgumentException("Minor version must be positive: " + minorVersion); @@ -384,13 +383,14 @@ public static Integer getLatestPatchNumberOf(int minorVersion) { @ApiStatus.Internal @ApiStatus.Experimental + @Unmodifiable public static final Set SUPPORTED_MAPPINGS; static { if (isMinecraftDisabled() != null) { - SUPPORTED_MAPPINGS = EnumSet.noneOf(MinecraftMapping.class); + SUPPORTED_MAPPINGS = Collections.unmodifiableSet(EnumSet.noneOf(MinecraftMapping.class)); } else { - SUPPORTED_MAPPINGS = + SUPPORTED_MAPPINGS = Collections.unmodifiableSet( supply( ofMinecraft() .inPackage(MinecraftPackage.NMS, "server.level") @@ -401,7 +401,8 @@ public static Integer getLatestPatchNumberOf(int minorVersion) { .inPackage(MinecraftPackage.NMS, "server.level") .map(MinecraftMapping.MOJANG, "EntityPlayer"), () -> EnumSet.of(MinecraftMapping.SPIGOT, MinecraftMapping.OBFUSCATED) - ).get(); + ).get() + ); } } @@ -563,10 +564,12 @@ public static Class getCraftClass(@NotNull String name) { * * @param clazz the class to get the array version of. You could use for multi-dimensions arrays too. * @throws IllegalArgumentException if the class could not be found. + * @see ClassHandle#asArray() */ @NotNull @Contract(pure = true) - public static Class toArrayClass(Class clazz) { + public static Class toArrayClass(@NotNull Class clazz) { + Objects.requireNonNull(clazz, "Class is null"); try { return Class.forName("[L" + clazz.getName() + ';'); } catch (ClassNotFoundException ex) { @@ -672,6 +675,7 @@ public static , O> AggregateReflectiveSupplier T relativizeSuppressedExceptions(@NotNull T ex) { Objects.requireNonNull(ex, "Cannot relativize null exception"); final StackTraceElement[] EMPTY_STACK_TRACE_ARRAY = new StackTraceElement[0]; @@ -706,6 +710,7 @@ public static T relativizeSuppressedExceptions(@NotNull T } @SuppressWarnings("unchecked") + @Contract(value = "_ -> fail", pure = true) private static void throwException(Throwable exception) throws T { throw (T) exception; } @@ -751,6 +756,7 @@ public static RuntimeException throwCheckedException(@NotNull Throwable exceptio * Adds the stacktrace of the current thread in case an error occurs in the given Future. */ @ApiStatus.Experimental + @Contract(value = "_ -> new", mutates = "param1") public static CompletableFuture stacktrace(@NotNull CompletableFuture completableFuture) { StackTraceElement[] currentStacktrace = Thread.currentThread().getStackTrace(); return completableFuture.whenComplete((value, ex) -> { // Gets called even when it's completed. diff --git a/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMAnalyzer.java b/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMAnalyzer.java index 43b860f6..163fc671 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMAnalyzer.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMAnalyzer.java @@ -49,6 +49,8 @@ final class ASMAnalyzer { // static void printAnalyzerResult(MethodNode method, Analyzer analyzer, PrintWriter printWriter) private static final MethodHandle CheckClassAdapter_printAnalyzerResult; + private ASMAnalyzer() {} + static { MethodHandle printAnalyzerResult; MethodHandles.Lookup lookup = MethodHandles.lookup(); diff --git a/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMClassLoader.java b/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMClassLoader.java index 9bf9d763..f1ceedb6 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMClassLoader.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMClassLoader.java @@ -37,6 +37,8 @@ */ @SuppressWarnings("unused") final class ASMClassLoader extends ClassLoader { + private static final String DEFINE_CLASS = "defineClass"; + protected ASMClassLoader() {} protected Class defineClass(String name, byte[] bytes) { @@ -53,7 +55,7 @@ private static Class defineClassLombockTransplanter(String className, byte[] * * Lookup lookup = MethodHandles.lookup(); * MethodType type = MethodType.methodType(Class.class, new Class[] {String.class, byte[].class, int.class, int.class}); - * MethodHandle method = lookup.findVirtual(original.getClass(), "defineClass", type); + * MethodHandle method = lookup.findVirtual(original.getClass(), DEFINE_CLASS, type); * shadowClassLoaderClass = (Class) method.invokeWithArguments(original, "lombok.launch.ShadowClassLoader", bytes, new Integer(0), new Integer(len)}) */ Class methodHandles = Class.forName("java.lang.invoke.MethodHandles"); @@ -74,9 +76,9 @@ private static Class defineClassLombockTransplanter(String className, byte[] // at java.base/java.lang.invoke.MethodHandles$Lookup.resolveOrFail(MethodHandles.java:3747) // at java.base/java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:2767) // ... 15 more - Object method = findVirtualMethod.invoke(lookup, classLoader.getClass(), "defineClass", type); + Object method = findVirtualMethod.invoke(lookup, classLoader.getClass(), DEFINE_CLASS, type); return (Class) invokeMethod.invoke(method, new Object[] - {new Object[]{classLoader, classLoader, bytecode, 0, bytecode.length}}); + {new Object[]{classLoader, className, bytecode, 0, bytecode.length}}); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { // Ignore, old Java @@ -89,11 +91,11 @@ private static Class defineClassJLA(String generatedClassName, byte[] bytecod // DelegatingClassLoader // static Class defineClass(String name, byte[] bytes, int off, int len, ClassLoader parentClassLoader) Class ClassDefiner = Class.forName("jdk.internal.reflect.ClassDefiner"); - Method meth = ClassDefiner.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class); + Method meth = ClassDefiner.getDeclaredMethod(DEFINE_CLASS, String.class, byte[].class, int.class, int.class, ClassLoader.class); return (Class) meth.invoke(null, generatedClassName, bytecode, 0, bytecode.length, XReflectASM.class.getClassLoader()); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } } @@ -105,12 +107,12 @@ private static Class methodHandleLoadClass(byte[] bytecode) { MethodHandles.Lookup lookup = MethodHandles.lookup(); try { // Java 9+ - MethodHandle defineClass = lookup.findVirtual(MethodHandles.Lookup.class, "defineClass", + MethodHandle defineClass = lookup.findVirtual(MethodHandles.Lookup.class, DEFINE_CLASS, MethodType.methodType(Class.class, byte[].class)); return (Class) defineClass.invoke(lookup, bytecode); } catch (Throwable e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } } @@ -123,7 +125,7 @@ private static Class loadClass(String className, byte[] bytecode) { Class cls = Class.forName("java.lang.ClassLoader"); java.lang.reflect.Method method = cls.getDeclaredMethod( - "defineClass", + DEFINE_CLASS, String.class, byte[].class, int.class, int.class); // Protected method invocation. diff --git a/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMVersion.java b/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMVersion.java index b0cba081..fe7c30bc 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMVersion.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/asm/ASMVersion.java @@ -78,6 +78,7 @@ private ASMVersion() {} System.out.println("[XSeries/XReflection] Using custom ASM Java target version: " + usedJavaVersion); } } catch (SecurityException ignored) { + // If we don't have access to system properties, don't care. } USED_ASM_OPCODE_VERSION = usedAsmVersion; diff --git a/src/main/java/com/cryptomorin/xseries/reflection/asm/ArrayInsnGenerator.java b/src/main/java/com/cryptomorin/xseries/reflection/asm/ArrayInsnGenerator.java index cca0af87..e94843ef 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/asm/ArrayInsnGenerator.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/asm/ArrayInsnGenerator.java @@ -28,13 +28,13 @@ /** * A simple class that generates Java bytecode instructions for creating arrays. + * Doesn't properly work with multidimensional arrays. * It's kind of weird that ASM doesn't have a helper class for this. * * @since 14.0.0 */ final class ArrayInsnGenerator { private final GeneratorAdapter mv; - private final Type type; private final int length; private int index = 0; private final int storeInsn; @@ -51,13 +51,16 @@ public ArrayInsnGenerator(GeneratorAdapter mv, Class type, int length) { this.mv = mv; this.length = length; - this.type = Type.getType(type); - this.storeInsn = type == Object.class ? -1 : this.type.getOpcode(Opcodes.IASTORE); + Type asmType = Type.getType(type); + this.storeInsn = type == Object.class ? -1 : asmType.getOpcode(Opcodes.IASTORE); mv.push(length); - mv.newArray(this.type); + mv.newArray(asmType); } + /** + * Used for {@code Object[]} arrays. + */ private boolean isDynamicStoreInsn() { return storeInsn == -1; } @@ -75,7 +78,7 @@ public void add(Type elementType, Runnable instruction) { private void add(Runnable instruction, int storeInsn) { if (index >= length) { - throw new IllegalStateException("Array is already full at index " + index); + throw new IllegalStateException("Array is already full, at index " + index); } // store instruction: diff --git a/src/main/java/com/cryptomorin/xseries/reflection/asm/XReflectASM.java b/src/main/java/com/cryptomorin/xseries/reflection/asm/XReflectASM.java index a4b2e415..191ea610 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/asm/XReflectASM.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/asm/XReflectASM.java @@ -34,8 +34,10 @@ import com.google.common.collect.Streams; import org.intellij.lang.annotations.Pattern; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.objectweb.asm.*; import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; import java.io.IOException; import java.io.PrintWriter; @@ -284,6 +286,7 @@ public static XReflectASM proxify(Class return asm; } + @NotNull @SuppressWarnings("unchecked") public T create() { Class proxified = loadClass(); @@ -402,7 +405,7 @@ public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, Str public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { classWriter.visit( /* version */ JAVA_VERSION, - /* access */ Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, + /* access */ Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL, /* name */ generatedClassType.getInternalName(), /* signature */ null, /* supername */ SUPER_CLASS, @@ -431,6 +434,8 @@ public void visitSource(String source, String debug) { @Override public void visitEnd() { + generateGetTargetClass(); + generateIsInstance(); generateInstance(); generateBindTo(); @@ -634,14 +639,15 @@ private void generateCode() { // We also use invokeExact instead of invoke, we will do all needed conversions ourselves. // Since we're generating a bytecode ourselves, we don't need to generate a try-catch for // the MethodHandle invoke methods either, the class verifier allows that. + String invokeExact = "invokeExact"; switch (type) { case METHOD: if (handle.isInaccessible()) { adapter.invokeVirtual(Type.getType(MethodHandle.class), - new org.objectweb.asm.commons.Method("invokeExact", + new org.objectweb.asm.commons.Method(invokeExact, Type.getType(handle.info.rType.real), Streams.concat( - Stream.of(targetClassType), + isStatic ? Stream.of() : Stream.of(targetClassType), Arrays.stream(handle.info.pTypes).map(x -> Type.getType(x.real)) ).toArray(Type[]::new))); } else { @@ -655,13 +661,27 @@ private void generateCode() { } break; case FIELD: + boolean isSetter = argumentTypes.length != 0; + Type fieldDescriptor; + if (argumentTypes.length != 0) { + fieldDescriptor = adapter.getArgumentTypes()[0]; + } else { + fieldDescriptor = adapter.getReturnType(); + } + if (handle.isInaccessible()) { + List parameters = new ArrayList<>(3); + if (!isStatic) parameters.add(targetClassType); + if (isSetter) parameters.add(Type.getType(handle.info.pTypes[0].real)); + adapter.invokeVirtual(Type.getType(MethodHandle.class), - new org.objectweb.asm.commons.Method("invokeExact", Type.getType(handle.info.rType.real), - new Type[]{targetClassType})); + new org.objectweb.asm.commons.Method(invokeExact, + Type.getType(handle.info.rType.real), + parameters.toArray(new Type[0]) + )); } else { int fieldCode; - if (argumentTypes.length != 0) { + if (isSetter) { if (isStatic) fieldCode = Opcodes.PUTSTATIC; else fieldCode = Opcodes.PUTFIELD; } else { @@ -669,13 +689,18 @@ private void generateCode() { else fieldCode = Opcodes.GETFIELD; } - adapter.visitFieldInsn(fieldCode, targetClassType.getInternalName(), name, adapter.getReturnType().getDescriptor()); + adapter.visitFieldInsn( + /* opcode */ fieldCode, + /* owner */ targetClassType.getInternalName(), + /* name */ name, + /* descriptor */ fieldDescriptor.getDescriptor() + ); } break; case CONSTRUCTOR: if (handle.isInaccessible()) { adapter.invokeVirtual(Type.getType(MethodHandle.class), - new org.objectweb.asm.commons.Method("invokeExact", targetClassType, convert(handle.info.pTypes))); + new org.objectweb.asm.commons.Method(invokeExact, targetClassType, convert(handle.info.pTypes))); } else { adapter.visitMethodInsn( Opcodes.INVOKESPECIAL, @@ -709,7 +734,9 @@ private void generateCode() { } @Override - public void visitCode() {} + public void visitCode() { + // Don't retain any of the existing code, we will generate our own. + } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { @@ -869,17 +896,18 @@ private void initStaticFields() { mv.visitLabel(label6); mv.visitTypeInsn(Opcodes.NEW, "java/lang/RuntimeException"); mv.visitInsn(Opcodes.DUP); - mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + String StringBuilder = "java/lang/StringBuilder"; + mv.visitTypeInsn(Opcodes.NEW, StringBuilder); mv.visitInsn(Opcodes.DUP); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, StringBuilder, CONSTRUCTOR_NAME, "()V", false); mv.visitLdcInsn("Failed to get inaccessible members for "); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, StringBuilder, "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitLdcInsn(generatedClassType); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, StringBuilder, "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, StringBuilder, "toString", "()Ljava/lang/String;", false); mv.loadLocal(ex); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/RuntimeException", "", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/RuntimeException", CONSTRUCTOR_NAME, "(Ljava/lang/String;Ljava/lang/Throwable;)V", false); mv.visitInsn(Opcodes.ATHROW); mv.visitLabel(noExceptionThrown); @@ -896,7 +924,7 @@ private void initStaticFields() { private void generateInstance() { // If we use the target class as a return type, then we'd also have to create a synthetic method as well // classWriter.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC, "instance", "()Ljava/lang/Object;", null, null); - GeneratorAdapter mv = createMethod(Opcodes.ACC_PUBLIC, "instance", Type.getMethodDescriptor(Type.getType(Object.class))); + GeneratorAdapter mv = createMethod(Opcodes.ACC_PUBLIC, INSTANCE_FIELD, Type.getMethodDescriptor(Type.getType(Object.class))); Label label0 = new Label(); mv.visitLabel(label0); @@ -950,7 +978,7 @@ private void generateBindTo() { Label label3 = new Label(); mv.visitLabel(label3); visitThis(mv, label0, label3); - mv.visitLocalVariable("instance", targetClassType.getDescriptor(), null, label0, label3, 1); + mv.visitLocalVariable(INSTANCE_FIELD, targetClassType.getDescriptor(), null, label0, label3, 1); mv.visitMaxs(3, 2); mv.visitEnd(); @@ -1076,21 +1104,29 @@ private void generateEquals() { mv.visitEnd(); } - private GeneratorAdapter createMethod(int access, - @Pattern("(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)|()|()") String name, - String descriptor) { - GeneratorAdapter method = new GeneratorAdapter( - classWriter.visitMethod(access, name, descriptor, null, null), - access, name, descriptor - ); - method.visitCode(); - return method; + private void generateGetTargetClass() { + GeneratorAdapter mv = createMethod(Opcodes.ACC_PUBLIC, "Class getTargetClass()"); + mv.push(targetClassType); + mv.returnValue(); + + mv.visitMaxs(1, 0); + mv.visitEnd(); + } + + private void generateIsInstance() { + GeneratorAdapter mv = createMethod(Opcodes.ACC_PUBLIC, "boolean isInstance(Object)"); + mv.loadArg(0); + mv.instanceOf(targetClassType); + mv.returnValue(); + + mv.visitMaxs(1, 1); + mv.visitEnd(); } private void generateToString() { // return this.getClass().getSimpleName() + "(instance=" + this.instance + ')'; Type StringBuilder = Type.getType(java.lang.StringBuilder.class); - GeneratorAdapter mv = createMethod(Opcodes.ACC_PUBLIC, "toString", "()Ljava/lang/String;"); + GeneratorAdapter mv = createMethod(Opcodes.ACC_PUBLIC, "String toString()"); Label start = mv.newLabel(); mv.visitLabel(start); @@ -1125,10 +1161,27 @@ private void generateToString() { mv.visitEnd(); } + private GeneratorAdapter createMethod(int access, String descriptor) { + Method desc = getMethod(descriptor); + return createMethod(access, desc.getName(), desc.getDescriptor()); + } + + private GeneratorAdapter createMethod(int access, + @Pattern("(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)|()|()") String name, + String descriptor) { + GeneratorAdapter method = new GeneratorAdapter( + classWriter.visitMethod(access, name, descriptor, null, null), + access, name, descriptor + ); + method.visitCode(); + return method; + } + private void visitThis(MethodVisitor mv, Label start, Label end) { mv.visitLocalVariable("this", generatedClassType.getDescriptor(), null, start, end, 0); } + @NotNull public Class loadClass() { if (this.loaded != null) return this.loaded; diff --git a/src/main/java/com/cryptomorin/xseries/reflection/constraint/ReflectiveConstraint.java b/src/main/java/com/cryptomorin/xseries/reflection/constraint/ReflectiveConstraint.java index 782052b4..32a156b4 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/constraint/ReflectiveConstraint.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/constraint/ReflectiveConstraint.java @@ -24,6 +24,8 @@ import com.cryptomorin.xseries.reflection.ReflectiveHandle; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; /** * A set of checks performed on a {@link ReflectiveHandle} to determine whether it meets @@ -50,11 +52,13 @@ public interface ReflectiveConstraint { /** * The category name of this constraint. */ + @Contract(pure = true) String category(); /** * The name of this constraint. */ + @Contract(pure = true) String name(); /** @@ -67,7 +71,9 @@ public interface ReflectiveConstraint { * @param jvm the corresponding JVM object (not {@link java.lang.invoke.MethodHandle}) of the handle. * @return Refer to {@link Result} for details. */ - Result appliesTo(ReflectiveHandle handle, Object jvm); + @NotNull + @Contract(pure = true) + Result appliesTo(@NotNull ReflectiveHandle handle, @NotNull Object jvm); enum Result { /** diff --git a/src/main/java/com/cryptomorin/xseries/reflection/jvm/FieldMemberHandle.java b/src/main/java/com/cryptomorin/xseries/reflection/jvm/FieldMemberHandle.java index fd687aa8..2a9770db 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/jvm/FieldMemberHandle.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/jvm/FieldMemberHandle.java @@ -101,7 +101,9 @@ public FieldMemberHandle(ClassHandle clazz) { super(clazz); } - public Boolean isGetter() { + public boolean isGetter() { + if (getter == null) + throw new IllegalStateException("Not specified whether the field handle is a getter or setter"); return getter; } @@ -154,7 +156,7 @@ public FieldMemberHandle copy() { @Override public MethodHandle reflect() throws ReflectiveOperationException { - Objects.requireNonNull(getter, "Not specified whether the method is a getter or setter"); + Objects.requireNonNull(getter, "Not specified whether the field handle is a getter or setter"); Field jvm = reflectJvm(); if (getter) { diff --git a/src/main/java/com/cryptomorin/xseries/reflection/jvm/classes/ClassHandle.java b/src/main/java/com/cryptomorin/xseries/reflection/jvm/classes/ClassHandle.java index bd717f40..4882cd10 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/jvm/classes/ClassHandle.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/jvm/classes/ClassHandle.java @@ -32,7 +32,9 @@ import com.cryptomorin.xseries.reflection.parser.ReflectionParser; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; import java.util.IdentityHashMap; import java.util.Map; @@ -46,7 +48,7 @@ public abstract class ClassHandle implements ReflectiveHandle>, NamedRe protected final ReflectiveNamespace namespace; private final Map, ReflectiveConstraint> constraints = new IdentityHashMap<>(); - protected ClassHandle(ReflectiveNamespace namespace) { + protected ClassHandle(@NotNull ReflectiveNamespace namespace) { this.namespace = namespace; namespace.link(this); } @@ -56,7 +58,8 @@ protected ClassHandle(ReflectiveNamespace namespace) { */ @SuppressWarnings("unchecked") @ApiStatus.Experimental - public ClassHandle constraint(ReflectiveConstraint constraint) { + @Contract(value = "_ -> this", mutates = "this") + public ClassHandle constraint(@NotNull ReflectiveConstraint constraint) { this.constraints.put((Class) constraint.getClass(), constraint); return this; } @@ -71,14 +74,21 @@ protected > T checkConstraints(T jvm) { return jvm; } - public abstract ClassHandle asArray(int dimensions); + @NotNull + @Contract("_ -> new") + public abstract ClassHandle asArray(@Range(from = 1, to = Integer.MAX_VALUE) int dimensions); + @NotNull + @Contract("-> new") public final ClassHandle asArray() { return asArray(1); } + @Contract(pure = true) public abstract boolean isArray(); + @NotNull + @Contract("_ -> new") public DynamicClassHandle inner(@Language(value = "Java", suffix = "{}") String declaration) { return inner(namespace.classHandle(declaration)); } @@ -88,7 +98,9 @@ public DynamicClassHandle inner(@Language(value = "Java", suffix = "{}") String * @param the type of the class handle. * @return the same object as the one provided in the parameter. */ - public T inner(T handle) { + @NotNull + @Contract("_ -> param1") + public T inner(@NotNull T handle) { Objects.requireNonNull(handle, "Inner handle is null"); if (this == handle) throw new IllegalArgumentException("Same instance: " + this); handle.parent = this; @@ -101,6 +113,7 @@ public T inner(T handle) { * * @return -1 if this class cannot be found, 0 if not an array, otherwise a positive number. */ + @Range(from = -1, to = Integer.MAX_VALUE) public int getDimensionCount() { int count = -1; Class clazz = reflectOrNull(); @@ -114,46 +127,57 @@ public int getDimensionCount() { return count; } + @Contract(pure = true) public ReflectiveNamespace getNamespace() { return namespace; } + @Contract(value = "-> new", pure = true) public MethodMemberHandle method() { return new MethodMemberHandle(this); } + @Contract(value = "_ -> new", pure = true) public MethodMemberHandle method(@Language(value = "Java", suffix = ";") String declaration) { return createParser(declaration).parseMethod(method()); } + @Contract(value = "-> new", pure = true) public EnumMemberHandle enums() { return new EnumMemberHandle(this); } + @Contract(value = "-> new", pure = true) public FieldMemberHandle field() { return new FieldMemberHandle(this); } + @Contract(value = "_ -> new", pure = true) public FieldMemberHandle field(@Language(value = "Java", suffix = ";") String declaration) { return createParser(declaration).parseField(field()); } + @Contract(value = "_ -> new", pure = true) public ConstructorMemberHandle constructor(@Language(value = "Java", suffix = ";") String declaration) { return createParser(declaration).parseConstructor(constructor()); } + @Contract(value = "-> new", pure = true) public ConstructorMemberHandle constructor() { return new ConstructorMemberHandle(this); } + @Contract(value = "_ -> new", pure = true) public ConstructorMemberHandle constructor(Class... parameters) { return constructor().parameters(parameters); } + @Contract(value = "_ -> new", pure = true) public ConstructorMemberHandle constructor(ClassHandle... parameters) { return constructor().parameters(parameters); } + @Contract(value = "_ -> new", pure = true) private ReflectionParser createParser(@Language("Java") String declaration) { return new ReflectionParser(declaration).imports(this.namespace); } diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/OverloadedMethod.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/OverloadedMethod.java index f857dabd..25e3467a 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/proxy/OverloadedMethod.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/OverloadedMethod.java @@ -91,7 +91,7 @@ public void add(T method, String name) { Map descriptors = descriptorMap.computeIfAbsent(name, k -> new HashMap<>(3)); String descriptor = descritporProcessor.apply(method); if (descriptors.put(descriptor, method) != null) { - throw new IllegalArgumentException("Method named '" + name + "' with descriptor '" + descriptors + "' was already added: " + descriptorMap); + throw new IllegalArgumentException("Method named '" + name + "' with descriptor '" + descriptor + "' was already added: " + descriptors); } } diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxy.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxy.java index 2fde5c9a..174cfe9d 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxy.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxy.java @@ -27,6 +27,7 @@ import com.cryptomorin.xseries.reflection.jvm.objects.ReflectedObject; import com.cryptomorin.xseries.reflection.proxy.annotations.Constructor; import com.cryptomorin.xseries.reflection.proxy.annotations.Field; +import com.cryptomorin.xseries.reflection.proxy.annotations.Proxify; import com.cryptomorin.xseries.reflection.proxy.processors.MappedType; import com.cryptomorin.xseries.reflection.proxy.processors.ProxyMethodInfo; import com.cryptomorin.xseries.reflection.proxy.processors.ReflectiveAnnotationProcessor; @@ -35,6 +36,7 @@ import org.jetbrains.annotations.Nullable; import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -43,7 +45,7 @@ /** * The basis of this class is that you create an {@code interface} class and annotate it using - * {@link com.cryptomorin.xseries.reflection.proxy.annotations.Class Class}, {@link Field}, {@link Constructor}, etc... annotations to mark how + * {@link Proxify Class}, {@link Field}, {@link Constructor}, etc... annotations to mark how * these methods are resolved for different versions. Everything is accessed through methods, * even constructors and fields are accessed in forms of methods. *

    @@ -94,8 +96,12 @@ public static ReflectiveProxy proxify(Class for (ProxyMethodInfo overload : mapping.getValue().getOverloads()) { ReflectedObject jvm = overload.handle.jvm().unreflect(); + MethodHandle methodHandle = (MethodHandle) overload.handle.unreflect(); + + methodHandle = createDynamicProxy(null, methodHandle); + ProxifiedObject proxifiedObj = new ProxifiedObject( - (MethodHandle) overload.handle.unreflect(), + methodHandle, overload, jvm.accessFlags().contains(XAccessFlag.STATIC), jvm.type() == ReflectedObject.Type.CONSTRUCTOR, @@ -136,6 +142,22 @@ public static ReflectiveProxy proxify(Class return proxy; } + private static MethodHandle createDynamicProxy(@Nullable Object bindInstance, MethodHandle methodHandle) { + int parameterCount = methodHandle.type().parameterCount(); + int requireArgs = bindInstance != null ? 1 : 0; + + // bind the only parameter left and remove it. + if (bindInstance != null) methodHandle = methodHandle.bindTo(bindInstance); + + if (parameterCount == requireArgs) { + return methodHandle.asType(MethodType.methodType(Object.class)); + } else { + return methodHandle + .asSpreader(Object[].class, parameterCount - requireArgs) + .asType(MethodType.methodType(Object.class, Object[].class)); + } + } + private static String descriptorProcessor(ProxifiedObject obj) { // We can't use the MethodHandle here because the parameter list might contain the descriptor for the receiver object. return OverloadedMethod.getParameterDescriptor(MappedType.getRealTypes(obj.proxyMethodInfo.pTypes)); @@ -243,7 +265,15 @@ public T bindTo(@NotNull Object instance) { } else { try { // insert = MethodHandles.insertArguments(unbound.handle, 0, instance); - insert = unbound.handle.bindTo(instance); + // This is cached, no worries. + insert = (MethodHandle) unbound.proxyMethodInfo.handle.unreflect(); + + // We already checked for static and constructor members, all these handles are for instance members now. + if (insert.type().parameterCount() == 0) { + throw new IllegalStateException("Non-static, non-constructor with 0 arguments found: " + insert); + } else { + insert = createDynamicProxy(instance, insert); + } } catch (Exception e) { throw new IllegalStateException("Failed to bind " + instance + " to " + entry.getKey() + " -> " + unbound.handle + " (static=" + unbound.isStatic + ", constructor=" + unbound.isConstructor + ')', e); } @@ -281,7 +311,7 @@ private static String getMethodList(Class clazz, boolean declaredOnly) { } @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { { // ReflectiveProxyObject & Object methods int paramCount = method.getParameterCount(); String name = method.getName(); @@ -306,14 +336,15 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (instance == null) proxyClass.wait(); else instance.wait(); return null; - case "getClass": // Does this actually get called? - if (instance == null) return proxyClass; - else return instance.getClass(); + case "getTargetClass": + return targetClass; } } else if (paramCount == 1) { switch (name) { case "bindTo": return bindTo(args[0]); + case "isInstance": + return targetClass.isInstance(args[0]); case "equals": return instance == null ? proxyClass == args[0] : instance.equals(args[0]); case "wait": @@ -354,7 +385,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Object result; - if (reflectedHandle.pTypes != null) { + if (reflectedHandle.pTypes != null && args != null) { for (int i = 0; i < args.length; i++) { Object arg = args[i]; if (arg instanceof ReflectiveProxyObject) { @@ -367,8 +398,14 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // MethodHandle#invoke is a special case due to its @PolymorphicSignature nature. // The signature of the method is simply a placeholder which is replaced by JVM. // We use invokeWithArguments which accepts working with Object.class - if (args == null) result = reflectedHandle.handle.invoke(); - else result = reflectedHandle.handle.invokeWithArguments(args); + // But we already changed the method signature to (Object[])Object so we can safely + // use invokeExact() + if (args == null) { + result = reflectedHandle.handle.invokeExact(); + } else { + // result = reflectedHandle.handle.invokeWithArguments(args); + result = reflectedHandle.handle.invoke(args); + } } catch (Throwable ex) { throw new IllegalStateException("Failed to execute " + method + " -> " + reflectedHandle.handle + " with args " diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxyObject.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxyObject.java index bd7eb8d0..31a86ae8 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxyObject.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/ReflectiveProxyObject.java @@ -24,6 +24,7 @@ import com.cryptomorin.xseries.reflection.proxy.annotations.Ignore; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,17 +42,50 @@ */ @ApiStatus.Experimental public interface ReflectiveProxyObject { - @Nullable + /** + * Gets the instance that the methods are delegating to. + * Throw an exception if used on the factory object. + */ @Ignore + @NotNull + @ApiStatus.NonExtendable + @Contract(pure = true) Object instance(); + /** + * Get the real class object that this proxy object is referencing. + * + * @since 14.0.0 + */ + @Ignore + @NotNull + @ApiStatus.NonExtendable + @Contract(pure = true) + Class getTargetClass(); + + /** + * Equivalent to the code: + *

    {@code object instanceof TargetClass}
    + * This results in more performance in generated code instead of doing: + *
    {@code getTargetClass().isInstance(object.getClass())}
    + * So do note that this will never return true if you pass a {@link ReflectiveProxyObject} to it. + * + * @since 14.0.0 + */ + @Ignore + @NotNull + @ApiStatus.NonExtendable + @Contract(pure = true) + boolean isInstance(@Nullable Object object); + /** * Returns a new {@link ReflectiveProxyObject} that's linked to a new {@link ReflectiveProxy} with the given instance. * * @param instance the instance to bind. */ + @Ignore @NotNull @ApiStatus.OverrideOnly - @Ignore + @Contract(value = "_ -> new", pure = true) ReflectiveProxyObject bindTo(@NotNull Object instance); } diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/Class.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/Proxify.java similarity index 98% rename from src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/Class.java rename to src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/Proxify.java index ac9d69ba..eb337fb7 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/Class.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/annotations/Proxify.java @@ -29,7 +29,7 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -public @interface Class { +public @interface Proxify { java.lang.Class target() default void.class; String packageName() default ""; diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/generator/XProxifier.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/generator/XProxifier.java new file mode 100644 index 00000000..920a1866 --- /dev/null +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/generator/XProxifier.java @@ -0,0 +1,494 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Crypto Morin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.cryptomorin.xseries.reflection.proxy.generator; + +import com.cryptomorin.xseries.reflection.XAccessFlag; +import com.cryptomorin.xseries.reflection.jvm.objects.ReflectedObject; +import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxyObject; +import com.cryptomorin.xseries.reflection.proxy.annotations.*; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * This class is used for scanning other compiled classes using reflection in order to generate template + * interfaces classes for {@link com.cryptomorin.xseries.reflection.XReflection#proxify(Class)}. + *

    + * Turn this into an IntelliJ plugin? + * TODO Add a way to determine which referenced classes also require proxy classes + * and rename those, and add a method for generating a string for them as well. + * + * @since 14.0.0 + */ +@SuppressWarnings("StringBufferField") +@ApiStatus.Internal +public final class XProxifier { + private static final String MEMBER_SPACES = " "; + + private final StringBuilder writer = new StringBuilder(1000); + private final Set imports = new HashSet<>(20); + + private final String proxifiedClassName; + private final Class clazz; + + //// Settings //// TODO implement this + private final boolean generateIntelliJAnnotations = true; + private final boolean generateInaccessibleMembers = true; + private final boolean copyAnnotations = true; + private final boolean writeComments = true; + private final boolean writeInfoAnnotationsAsComments = true; // like doing /* private static final */ before the member + private boolean disableIDEFormatting; + private Function, String> remapper; // useful for mapping classes to a hypothetical proxy class + + public XProxifier(Class clazz) { + this.clazz = clazz; + this.proxifiedClassName = clazz.getSimpleName() + "Proxified"; + proxify(); + } + + private static Class unwrapArrayType(Class clazz) { + while (clazz.isArray()) clazz = clazz.getComponentType(); + return clazz; + } + + private void imports(Class clazz) { + clazz = unwrapArrayType(clazz); + if (!clazz.isPrimitive() && !clazz.getPackage().getName().equals("java.lang")) + imports.add(clazz.getName().replace('$', '.')); + } + + private void writeComments(String... comments) { + boolean multiLine = comments.length > 1; + if (!multiLine) { + writer.append("// ").append(comments[0]).append('\n'); + } + + writer.append("/**\n"); + + for (String comment : comments) { + writer.append(" * "); + writer.append(comment); + writer.append('\n'); + } + + writer.append(" */\n"); + } + + private void writeThrownExceptions(Class[] exceptionTypes) { + if (exceptionTypes == null || exceptionTypes.length == 0) return; + writer.append(" throws "); + StringJoiner exs = new StringJoiner(", "); + for (Class ex : exceptionTypes) { + imports(ex); + exs.add(ex.getSimpleName()); + } + writer.append(exs); + } + + private void writeMember(ReflectedObject jvm) { + writeMember(jvm, false); + } + + private void writeMember(ReflectedObject jvm, boolean generateGetterField) { + writer.append(annotationsToString(true, true, jvm)); + + Set accessFlags = jvm.accessFlags(); + if (accessFlags.contains(XAccessFlag.PRIVATE)) writeAnnotation(Private.class); + if (accessFlags.contains(XAccessFlag.PROTECTED)) writeAnnotation(Protected.class); + if (accessFlags.contains(XAccessFlag.STATIC)) writeAnnotation(Static.class); + if (accessFlags.contains(XAccessFlag.FINAL)) writeAnnotation(Final.class); + + switch (jvm.type()) { + case CONSTRUCTOR: + writeAnnotation(com.cryptomorin.xseries.reflection.proxy.annotations.Constructor.class); + writeAnnotation("NotNull"); + + Constructor ctor = (Constructor) jvm.unreflect(); + String contractParams = Arrays.stream(ctor.getParameterTypes()).map(x -> "_").collect(Collectors.joining(", ")); + writeAnnotation("Contract", + "value = \"" + contractParams + " -> new\"", + "pure = true" + ); + + break; + case FIELD: + writeAnnotation(com.cryptomorin.xseries.reflection.proxy.annotations.Field.class); + if (generateGetterField) { + writeAnnotation("Contract", "pure = true"); + } else { + writeAnnotation("Contract", "mutates = \"this\""); + } + break; + } + + StringJoiner parameters = new StringJoiner(", ", "(", ")"); + Class[] exceptionTypes = null; + writer.append(MEMBER_SPACES); + switch (jvm.type()) { + case CONSTRUCTOR: + Constructor constructor = (Constructor) jvm.unreflect(); + exceptionTypes = constructor.getExceptionTypes(); + writer.append(proxifiedClassName).append(' ').append("construct"); + writeParameters(parameters, constructor.getParameters()); + break; + case FIELD: + Field field = (Field) jvm.unreflect(); + imports(field.getType()); + + if (generateGetterField) { + writer.append(field.getType().getSimpleName()); + } else { + writer.append("void"); + parameters.add(field.getType().getSimpleName() + " value"); + } + + writer.append(' '); + writer.append(jvm.name()); + break; + case METHOD: + Method method = (Method) jvm.unreflect(); + exceptionTypes = method.getExceptionTypes(); + + imports(method.getReturnType()); + + writer.append(method.getReturnType().getSimpleName()); + writer.append(' '); + writer.append(jvm.name()); + writeParameters(parameters, method.getParameters()); + break; + } + + writer.append(parameters); + writeThrownExceptions(exceptionTypes); + writer.append(";\n\n"); + } + + /** + * Boxes primitive types into an object because a primitive array like int[] cannot be cast to Object[] + */ + private static Object[] getArray(Object val) { + if (val instanceof Object[]) return (Object[]) val; + int arrlength = Array.getLength(val); + Object[] outputArray = new Object[arrlength]; + for (int i = 0; i < arrlength; ++i) { + outputArray[i] = Array.get(val, i); + } + return outputArray; + } + + private String constantToString(Object obj) { + if (obj instanceof String) return '"' + obj.toString() + '"'; + if (obj instanceof Class) { + Class clazz = (Class) obj; + imports(clazz); + return clazz.getSimpleName() + ".class"; + } + if (obj instanceof Annotation) { + Annotation annotation = (Annotation) obj; + return annotationToString(annotation); + } + if (obj.getClass().isEnum()) { + imports(obj.getClass()); + return obj.getClass().getSimpleName() + '.' + ((Enum) obj).name(); + } + if (obj.getClass().isArray()) { + // Multidimensional arrays aren't allowed in annotations. + Object[] array = getArray(obj); + StringJoiner builder; + if (array.length == 0) return "{}"; + if (array.length == 1) builder = new StringJoiner(", "); + else builder = new StringJoiner(", ", "{", "}"); + + for (Object element : array) { + builder.add(constantToString(element)); + } + + return builder.toString(); + } + + // Numbers and booleans + return obj.toString(); + } + + private String annotationsToString(boolean member, boolean newLine, AnnotatedElement annotatable) { + StringJoiner builder = new StringJoiner( + (newLine ? '\n' : "") + (member ? MEMBER_SPACES : ""), + (member ? MEMBER_SPACES : ""), + (newLine ? "\n" : "") + ).setEmptyValue(""); + + for (Annotation annotation : annotatable.getAnnotations()) { + Annotation[] unwrapped = unwrapRepeatElement(annotation); + if (unwrapped != null) { + for (Annotation inner : unwrapped) { + builder.add(annotationToString(inner)); + } + } else { + builder.add(annotationToString(annotation)); + } + } + + return builder.toString(); + } + + private static Annotation[] unwrapRepeatElement(Annotation annotation) { + try { + Method method = annotation.annotationType().getDeclaredMethod("value"); + if (method.getReturnType().isArray()) { + // Multidimensional arrays aren't allowed, but we will use this just in case. + Class rawReturn = unwrapArrayType(method.getReturnType()); + if (rawReturn.isAnnotation()) { + Repeatable repeatable = rawReturn.getAnnotation(Repeatable.class); + if (repeatable != null && repeatable.value() == annotation.annotationType()) { + try { + return (Annotation[]) method.invoke(annotation); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException(e); + } + } + } + } + } catch (NoSuchMethodException ignored) { + } + return null; + } + + private String annotationToString(Annotation annotation) { + List builder = new ArrayList<>(); + boolean visitedValue = false; + + for (Method entry : annotation.annotationType().getDeclaredMethods()) { + try { + entry.setAccessible(true); + String key = entry.getName(); + Object value = entry.invoke(annotation); + try { + @Nullable Object defaultValue = entry.getDefaultValue(); + + // The default value isn't directly passed, they're not actually identical. + if (defaultValue != null) { + if (defaultValue.getClass().isArray()) { + if (Arrays.equals(getArray(defaultValue), getArray(value))) continue; + } else { + if (value.equals(defaultValue)) continue; + } + } + } catch (TypeNotPresentException ignored) { + // If it's not an annotation. + } + + if (key.equals("value")) visitedValue = true; + builder.add(key + " = " + constantToString(value)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Failed to get annotation value " + entry, e); + } + } + + imports(annotation.annotationType()); + String annotationValues; + if (builder.isEmpty()) annotationValues = ""; + else if (builder.size() == 1 && visitedValue) { + annotationValues = builder.get(0); + int equalsSign = annotationValues.indexOf('='); + annotationValues = '(' + annotationValues.substring(equalsSign + 2) + ')'; + } else { + annotationValues = '(' + String.join(", ", builder) + ')'; + } + + return '@' + annotation.annotationType().getSimpleName() + annotationValues; + } + + private StringJoiner writeParameters(StringJoiner joiner, Parameter[] parameters) { + for (Parameter parameter : parameters) { + imports(parameter.getType()); + + String type; + if (parameter.isVarArgs()) { + type = parameter.getType().getSimpleName() + "... "; + } else { + type = parameter.getType().getSimpleName(); + } + + String annotations = annotationsToString(false, false, parameter); + joiner.add(annotations + (annotations.isEmpty() ? "" : " ") + type + ' ' + parameter.getName()); + } + return joiner; + } + + private void writeAnnotation(Class annotation, String... values) { + writeAnnotation(true, annotation, values); + } + + private void writeAnnotation(boolean member, Class annotation, String... values) { + imports(annotation); + writeAnnotation(member, annotation.getSimpleName(), values); + } + + private void writeAnnotation(String annotation, String... values) { + writeAnnotation(true, annotation, values); + } + + private void writeAnnotation(boolean member, String annotation, String... values) { + if (member) writer.append(MEMBER_SPACES); + writer.append('@').append(annotation); + if (values.length != 0) { + StringJoiner valueJoiner = new StringJoiner(", ", "(", ")"); + for (String value : values) valueJoiner.add(value); + writer.append(valueJoiner); + } + writer.append('\n'); + } + + private void proxify() { + if (disableIDEFormatting) { + // This is intentionally written like this because IntelliJ will even recognize this + // text sequence even if it's not written as a comment. + writer.append("// ").append("@formatter:").append("OFF").append('\n'); + } + + if (writeComments) { + writeComments( + "This is a generated proxified class for " + clazz.getSimpleName() + ". However, you might", + "want to review each member and correct its annotations when needed.", + "

    ", + "It's also recommended to use your IDE's code formatter to adjust", + "imports and spaces according to your settings.", + "In IntelliJ, this can be done by with Ctrl+Alt+L", + "

    ", + "Full Target Class Path:", + clazz.getName() + ); + } + + writer.append(annotationsToString(false, true, clazz)); + + writeAnnotation( + false, + Proxify.class, + "target = " + clazz.getSimpleName() + ".class" + ); + if (!XAccessFlag.PUBLIC.isSet(clazz.getModifiers())) { + writeAnnotation(false, Private.class); + } + if (XAccessFlag.FINAL.isSet(clazz.getModifiers())) { + writeAnnotation(false, Final.class); + writeAnnotation(false, "ApiStatus.NonExtendable"); + } + writer + .append("public interface ") + .append(proxifiedClassName) + .append(" extends ") + .append(ReflectiveProxyObject.class.getSimpleName()) + .append(" {\n"); + + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + if (field.isSynthetic()) continue; + if (!XAccessFlag.FINAL.isSet(field.getModifiers())) { + writeMember(ReflectedObject.of(field), false); + } + writeMember(ReflectedObject.of(field), true); + } + if (declaredFields.length != 0) writer.append('\n'); + + Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : declaredConstructors) { + if (constructor.isSynthetic()) continue; + writeMember(ReflectedObject.of(constructor)); + } + if (declaredConstructors.length != 0) writer.append('\n'); + + for (Method method : clazz.getDeclaredMethods()) { + if (method.getDeclaringClass() == Object.class) continue; + if (method.isSynthetic()) continue; + if (method.isBridge()) continue; + writeMember(ReflectedObject.of(method)); + } + + writer.append('\n'); + writeAnnotation(Ignore.class); + writeAnnotation("NotNull"); + writeAnnotation("ApiStatus.OverrideOnly"); + writeAnnotation("Contract", + "value = \"_ -> new\"", + "pure = true" + ); + writer.append(MEMBER_SPACES).append(proxifiedClassName).append(" bindTo(@NotNull Object instance);\n"); + + writer.append("}\n"); + finalizeString(); + } + + /** + * After gathering all analysis data (currently only imports), construct the final string. + */ + private void finalizeString() { + StringBuilder whole = new StringBuilder(writer.length() + (imports.size() * 100)); + whole.append("import org.jetbrains.annotations.*;\n"); + // whole.append("import ").append(com.cryptomorin.xseries.reflection.proxy.annotations.Field.class.getPackage().getName()).append(".*;\n"); + + List sortedImports = new ArrayList<>(imports); + sortedImports.sort(Comparator.naturalOrder()); + + for (String anImport : sortedImports) { + whole.append("import ").append(anImport).append(";\n"); + } + + whole.append('\n'); + this.writer.insert(0, whole); + imports(ReflectiveProxyObject.class); + } + + + public String getString() { + if (writer.length() == 0) proxify(); + return writer.toString(); + } + + public void writeTo(Path path) { + if (Files.isDirectory(path)) { + path = path.resolve(proxifiedClassName + ".java"); + } + + try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + writer.write(getString()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ProxyMethodInfo.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ProxyMethodInfo.java index 3679a4f2..32aee1ea 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ProxyMethodInfo.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ProxyMethodInfo.java @@ -23,9 +23,11 @@ package com.cryptomorin.xseries.reflection.proxy.processors; import com.cryptomorin.xseries.reflection.ReflectiveHandle; +import org.jetbrains.annotations.ApiStatus; import java.lang.reflect.Method; +@ApiStatus.Internal public class ProxyMethodInfo { public final ReflectiveHandle handle; public final Method interfaceMethod; @@ -38,4 +40,9 @@ public ProxyMethodInfo(ReflectiveHandle handle, Method interfaceMethod, Mappe this.rType = rType; this.pTypes = pTypes; } + + @Override + public String toString() { + return getClass().getSimpleName() + '(' + interfaceMethod + ')'; + } } diff --git a/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ReflectiveAnnotationProcessor.java b/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ReflectiveAnnotationProcessor.java index f9c9425c..1891228b 100644 --- a/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ReflectiveAnnotationProcessor.java +++ b/src/main/java/com/cryptomorin/xseries/reflection/proxy/processors/ReflectiveAnnotationProcessor.java @@ -40,7 +40,6 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import java.lang.Class; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.reflect.AnnotatedElement; @@ -52,7 +51,6 @@ public final class ReflectiveAnnotationProcessor { private final Class interfaceClass; private ClassOverloadedMethods mapped; - private Function descriptorProcessor; private Class targetClass; @@ -104,7 +102,6 @@ private void loadDependency(MappedType type, Function, Boolean> isLoade } public void process(Function descriptorProcessor) { - this.descriptorProcessor = descriptorProcessor; ClassHandle classHandle = processTargetClass(); Method[] interfaceMethods = interfaceClass.getMethods(); // It's an interface, all are public OverloadedMethod.Builder mappedHandles = new OverloadedMethod.Builder<>(descriptorProcessor); @@ -139,10 +136,10 @@ public void process(Function descriptorProcessor) { error("Field setter method must have only one parameter: " + method); } - Class parameterType = method.getParameterTypes()[0]; - rType = unwrap(parameterType); - - field.returns(rType.real); + MappedType fieldType = unwrap(method.getParameterTypes()[0]); + rType = new MappedType(void.class, void.class); + pTypes = new MappedType[]{fieldType}; + field.returns(fieldType.real); } else { field.getter(); if (method.getParameterCount() != 0) { @@ -191,6 +188,7 @@ public void process(Function descriptorProcessor) { error("Failed to map " + method, e); } + System.out.println("Adding method of type " + method.getName() + ": " + rType + " - " + Arrays.toString(pTypes)); ProxyMethodInfo methodInfo = new ProxyMethodInfo(cached, method, rType, pTypes); mappedHandles.add(methodInfo, method.getName()); } @@ -199,8 +197,8 @@ public void process(Function descriptorProcessor) { } public @NotNull ClassHandle processTargetClass() { - com.cryptomorin.xseries.reflection.proxy.annotations.Class reflectClass = - interfaceClass.getAnnotation(com.cryptomorin.xseries.reflection.proxy.annotations.Class.class); + Proxify reflectClass = + interfaceClass.getAnnotation(Proxify.class); ReflectMinecraftPackage mcClass = interfaceClass.getAnnotation(ReflectMinecraftPackage.class); if (reflectClass == null && mcClass == null) { diff --git a/src/test/com/cryptomorin/xseries/test/Constants.java b/src/test/com/cryptomorin/xseries/test/Constants.java index 4fcf68f3..727516de 100644 --- a/src/test/com/cryptomorin/xseries/test/Constants.java +++ b/src/test/com/cryptomorin/xseries/test/Constants.java @@ -23,6 +23,7 @@ package com.cryptomorin.xseries.test; import com.cryptomorin.xseries.reflection.XReflection; +import com.cryptomorin.xseries.test.util.XLogger; import org.bukkit.Bukkit; import org.bukkit.World; @@ -33,13 +34,23 @@ public final class Constants { private Constants() {} public static final Object LOCK = new Object(); - public static final Path DESKTOP = Paths.get(System.getProperty("user.home") + "/Desktop/"); + + @SuppressWarnings("ConstantValue") + public static Path getTestPath() { + // It's inside "XSeries\target\tests" folder. + XLogger.log("System test path is " + System.getProperty("user.dir")); + if (Bukkit.getServer() == null) { + return Paths.get(System.getProperty("user.dir")); + } else { + return Bukkit.getWorldContainer().toPath(); + } + } /** * This sends unnecessary requests to Mojang and also delays out work too, * so let's not test when it's not needed. */ - public static final boolean TEST_MOJANG_API = true; + public static final boolean TEST_MOJANG_API = false; public static final boolean TEST_MOJANG_API_BULK = false; diff --git a/src/test/com/cryptomorin/xseries/test/XSeriesTests.java b/src/test/com/cryptomorin/xseries/test/XSeriesTests.java index caeacdf1..fb7e1f91 100644 --- a/src/test/com/cryptomorin/xseries/test/XSeriesTests.java +++ b/src/test/com/cryptomorin/xseries/test/XSeriesTests.java @@ -73,7 +73,7 @@ public final class XSeriesTests { public void enumToRegistry() throws URISyntaxException { URL resource = XSeriesTests.class.getResource("XEnchantment.java"); Path path = Paths.get(resource.toURI()); - ClassConverter.enumToRegistry(path, Constants.DESKTOP); + ClassConverter.enumToRegistry(path, Constants.getTestPath()); } public static void test() { @@ -225,6 +225,17 @@ private static void testXPotion() { assertNotNull(XPotion.of(potionType), () -> "null for (Bukkit -> XForm): " + potionType); assertPresent(XPotion.of(bukkitName), "null for (String -> XForm): " + bukkitName); } + + assertPotionEffect(XPotion.parseEffect("STRENGTH, 10, 3"), XPotion.STRENGTH, 10, 3); + assertPotionEffect(XPotion.parseEffect("BLINDNESS, 30, 1"), XPotion.BLINDNESS, 30, 1); + assertPotionEffect(XPotion.parseEffect("SLOWNESS, 200, 10, %75"), XPotion.SLOWNESS, 200, 10); + } + + private static void assertPotionEffect(XPotion.Effect effect, XPotion type, int duration, int amplifier) { + assertNotNull(effect, "Effect could not be parsed"); + assertEquals(type, effect.getXPotion(), "Potion effect types don't match"); + assertEquals(amplifier - 1, effect.getEffect().getAmplifier(), "Potion effect amplifiers don't match"); + assertEquals(duration * 20, effect.getEffect().getDuration(), "Potion effect durations don't match"); } private static void testXEnchantment() { diff --git a/src/test/com/cryptomorin/xseries/test/benchmark/reflection/ReflectionBenchmarkTargetMethodProxy.java b/src/test/com/cryptomorin/xseries/test/benchmark/reflection/ReflectionBenchmarkTargetMethodProxy.java index de2520b0..e3415c5e 100644 --- a/src/test/com/cryptomorin/xseries/test/benchmark/reflection/ReflectionBenchmarkTargetMethodProxy.java +++ b/src/test/com/cryptomorin/xseries/test/benchmark/reflection/ReflectionBenchmarkTargetMethodProxy.java @@ -23,14 +23,14 @@ package com.cryptomorin.xseries.test.benchmark.reflection; import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxyObject; -import com.cryptomorin.xseries.reflection.proxy.annotations.Class; import com.cryptomorin.xseries.reflection.proxy.annotations.Private; +import com.cryptomorin.xseries.reflection.proxy.annotations.Proxify; import com.cryptomorin.xseries.reflection.proxy.annotations.ReflectName; import org.jetbrains.annotations.NotNull; import java.util.Optional; -@Class(packageName = "com.cryptomorin.xseries.test.benchmark.reflection", ignoreCurrentName = true) +@Proxify(packageName = "com.cryptomorin.xseries.test.benchmark.reflection", ignoreCurrentName = true) @ReflectName("ReflectionBenchmarkTargetMethod") public interface ReflectionBenchmarkTargetMethodProxy extends ReflectiveProxyObject { @Private diff --git a/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMGeneratedSample.java b/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMGeneratedSample.java index 884c814d..d5f36de1 100644 --- a/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMGeneratedSample.java +++ b/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMGeneratedSample.java @@ -23,6 +23,8 @@ package com.cryptomorin.xseries.test.reflection.asm; import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxyObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -71,6 +73,16 @@ public ASMGeneratedSample instance() { return (ASMGeneratedSample) instance; } + @Override + public @NotNull Class getTargetClass() { + return null; + } + + @Override + public @NotNull boolean isInstance(@Nullable Object object) { + return false; + } + private Object doubleCtor() { return new ASMGeneratedSample(new StringBuilder(3)); } diff --git a/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMTests.java b/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMTests.java index a1ff5e80..c7930bb4 100644 --- a/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMTests.java +++ b/src/test/com/cryptomorin/xseries/test/reflection/asm/ASMTests.java @@ -22,7 +22,9 @@ package com.cryptomorin.xseries.test.reflection.asm; +import com.cryptomorin.xseries.reflection.XReflection; import com.cryptomorin.xseries.reflection.asm.XReflectASM; +import com.cryptomorin.xseries.test.Constants; import com.cryptomorin.xseries.test.reflection.proxy.ProxyTestProxified; import com.cryptomorin.xseries.test.reflection.proxy.ProxyTests; import com.cryptomorin.xseries.test.util.XLogger; @@ -30,14 +32,13 @@ public final class ASMTests { public static void test() { XLogger.log("[ASM] Testing XReflectASM generation..."); - // XReflectASM asm = XReflectASM.proxify(ServerLevel.class); - // asm.verify(false); - // asm.writeToFile(Constants.DESKTOP); XReflectASM asm = XReflectASM.proxify(ProxyTestProxified.class); + asm.writeToFile(Constants.getTestPath()); ProxyTestProxified factoryInstance = asm.create(); ProxyTests.normalProxyTest(factoryInstance); - ProxyTests.minecraftProxyTest((clazz) -> XReflectASM.proxify(clazz).create()); + if (XReflection.supports(20)) + ProxyTests.minecraftProxyTest((clazz) -> XReflectASM.proxify(clazz).create()); } } diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestClass.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestClass.java index 3eb15a85..07f97e71 100644 --- a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestClass.java +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestClass.java @@ -22,9 +22,14 @@ package com.cryptomorin.xseries.test.reflection.proxy; +import com.cryptomorin.xseries.reflection.jvm.objects.ReflectedObject; +import org.jetbrains.annotations.NotNull; + +@TestAnnotation(reference = String.class, filter = true, type = ReflectedObject.Type.FIELD) +@TestAnnotation2({100, 200}) public class ProxyTestClass { public static final int finalId = 555; - public static int id = 555; + public static int id = 500; public int date; private String operationField; @@ -37,11 +42,20 @@ public ProxyTestClass(String operationField, int date) { this.date = date; } - private ProxyTestClass(int date) { + protected static boolean isBeyond555() { + return id > 555; + } + + @Deprecated + @TestAnnotation(values = {@TestAnnotation2(3), @TestAnnotation2({4, 10})}) + @TestAnnotation(index = 1) + @TestAnnotation(name = "toaster") + @TestAnnotation(reference = String.class, filter = true, type = ReflectedObject.Type.FIELD) + private ProxyTestClass(@TestAnnotation(index = 3) int date) { this("OperationPrimus", 2027); } - public int compareTo(ProxyTestClass other) { + public int compareTo(@Deprecated @NotNull ProxyTestClass other) { return Integer.compare(this.date, other.date); } @@ -55,7 +69,7 @@ public static StringBuilder doStaticThings(int times) { @SuppressWarnings("MethodMayBeStatic") private StringBuilder doSomethingPrivate(int length) { - return new StringBuilder(length); + return new StringBuilder(length).append(operationField).append(finalId); } public void doSomething(String add, boolean add2) { @@ -70,7 +84,10 @@ public String getSomething(String add, short add2) { return add + add2; } - public String getSomething(String add) { + public String getSomething(String add) throws IllegalArgumentException, IllegalStateException { + if ("!".equals(add)) throw new IllegalArgumentException("Invalid argument: " + add); + if (operationField == null) throw new IllegalStateException("Operation is not set: " + add); + return add + add; } diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProcessor.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProcessor.java new file mode 100644 index 00000000..75d4379d --- /dev/null +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProcessor.java @@ -0,0 +1,37 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Crypto Morin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.cryptomorin.xseries.test.reflection.proxy; + +public abstract class ProxyTestProcessor { + private int processorCount = 4; + + public abstract O process(I first, I second); + + public int getProcessorCount() { + return processorCount; + } + + public void setProcessorCount(int processorCount) { + this.processorCount = processorCount; + } +} diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProxified.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProxified.java index 1ad12781..e9b2834c 100644 --- a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProxified.java +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTestProxified.java @@ -23,11 +23,10 @@ package com.cryptomorin.xseries.test.reflection.proxy; import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxyObject; -import com.cryptomorin.xseries.reflection.proxy.annotations.Class; import com.cryptomorin.xseries.reflection.proxy.annotations.*; import org.jetbrains.annotations.NotNull; -@Class(target = ProxyTestClass.class) +@Proxify(target = ProxyTestClass.class) public interface ProxyTestProxified extends ReflectiveProxyObject { // Fields @Static @@ -39,6 +38,14 @@ public interface ProxyTestProxified extends ReflectiveProxyObject { @Field int id(); + @Static + @Field + void id(int newValue); + + @Protected + @Static + boolean isBeyond555(); + @Field int date(); @@ -46,6 +53,10 @@ public interface ProxyTestProxified extends ReflectiveProxyObject { @Field String operationField(); + @Private + @Field + void operationField(String value); + @Static StringBuilder doStaticThings(int times); @@ -75,7 +86,7 @@ public interface ProxyTestProxified extends ReflectiveProxyObject { String getSomething(String add, short add2); - String getSomething(String add); + String getSomething(String add) throws IllegalArgumentException, IllegalStateException; int getSomething(String add, int add2); diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTests.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTests.java index 2f9eff16..e8cce2fa 100644 --- a/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTests.java +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/ProxyTests.java @@ -25,6 +25,7 @@ import com.cryptomorin.xseries.reflection.XReflection; import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxy; import com.cryptomorin.xseries.reflection.proxy.ReflectiveProxyObject; +import com.cryptomorin.xseries.reflection.proxy.generator.XProxifier; import com.cryptomorin.xseries.test.Constants; import com.cryptomorin.xseries.test.reflection.proxy.minecraft.BlockPos; import com.cryptomorin.xseries.test.reflection.proxy.minecraft.CraftWorld; @@ -40,33 +41,66 @@ public final class ProxyTests { public static void test() { XLogger.log("[Proxy] Testing ReflectiveProxy generation..."); - normalProxyTest(XReflection.proxify(ProxyTestProxified.class)); + normalProxyTest(ReflectiveProxy.proxify(ProxyTestProxified.class).proxy()); if (XReflection.supports(20)) minecraftProxyTest((x) -> ReflectiveProxy.proxify(x).proxy()); + new XProxifier(ProxyTestClass.class).writeTo(Constants.getTestPath()); + // new XProxifier( + // XReflection.ofMinecraft().inPackage(MinecraftPackage.NMS, "server.level") + // .map(MinecraftMapping.MOJANG, "ServerPlayer") + // .map(MinecraftMapping.SPIGOT, "EntityPlayer") + // .map(MinecraftMapping.OBFUSCATED, "are") + // .unreflect() + // ).writeTo(Constants.getTestPath()); + testCreateLambda(); testCreateXReflectionLambda(); } public static void normalProxyTest(ProxyTestProxified factoryProxy) { + assertSame(factoryProxy.getTargetClass(), ProxyTestClass.class); + + // Final member tests. + int initialValue = ProxyTestClass.id; assertEquals(ProxyTestClass.finalId, factoryProxy.finalId()); assertEquals(ProxyTestClass.finalId, factoryProxy.finalId()); assertEquals(ProxyTestClass.finalId, factoryProxy.finalId()); assertEquals(ProxyTestClass.id, factoryProxy.id()); + assertFalse(factoryProxy.isBeyond555()); + factoryProxy.id(777); + assertEquals(777, factoryProxy.id()); + assertTrue(factoryProxy.isBeyond555()); + factoryProxy.id(initialValue); // We don't want other tests to fail assertEquals("0123456789", factoryProxy.doStaticThings(10).toString()); ProxyTestClass instance = factoryProxy.ProxyTestProxified("OperationTestum", 2025); ProxyTestProxified unusInstance = factoryProxy.bindTo(instance); + // isInstance() test + assertTrue(factoryProxy.isInstance(instance)); + assertTrue(factoryProxy.isInstance(unusInstance.instance())); + assertFalse(factoryProxy.isInstance(unusInstance)); + + // First instance member tests assertEquals("OperationTestum", unusInstance.operationField()); assertEquals(2025, unusInstance.date()); assertEquals("OperationTestum12false", unusInstance.getSomething("12", false)); unusInstance.iForgotTheName("20", true); assertEquals("OperationTestumdoSomething20true", unusInstance.operationField()); + // noinspection StringBufferReplaceableByString + assertEquals( + new StringBuilder(10).append(unusInstance.operationField()).append(factoryProxy.finalId()).toString(), + unusInstance.doSomethingPrivate(10).toString() + ); + unusInstance.operationField("SomeValue"); + assertEquals("SomeValue", unusInstance.operationField()); // Cannot invoke constructor twice assertThrows(Exception.class, () -> unusInstance.ProxyTestProxified("OperationDuoTestum")); + // Second instance member tests ProxyTestProxified duoInstance = factoryProxy.ProxyTestProxified("OperationDuoTestum"); + assertTrue(factoryProxy.isInstance(duoInstance.instance())); assertEquals("0123456789", factoryProxy.doStaticThings(10).toString()); assertEquals("OperationDuoTestum", duoInstance.operationField()); assertEquals("soosoo", duoInstance.getSomething("soo")); diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation.java new file mode 100644 index 00000000..6edb939b --- /dev/null +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation.java @@ -0,0 +1,44 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Crypto Morin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.cryptomorin.xseries.test.reflection.proxy; + +import com.cryptomorin.xseries.reflection.jvm.objects.ReflectedObject; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE_PARAMETER, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(TestAnnotationList.class) +public @interface TestAnnotation { + String name() default ""; + + int index() default -1; + + boolean filter() default false; + + Class reference() default void.class; + + ReflectedObject.Type type() default ReflectedObject.Type.CONSTRUCTOR; + + TestAnnotation2[] values() default {}; +} diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation2.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation2.java new file mode 100644 index 00000000..8be06bb0 --- /dev/null +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotation2.java @@ -0,0 +1,34 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Crypto Morin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.cryptomorin.xseries.test.reflection.proxy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE_PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestAnnotation2 { + int[] value(); +} diff --git a/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotationList.java b/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotationList.java new file mode 100644 index 00000000..abfd3160 --- /dev/null +++ b/src/test/com/cryptomorin/xseries/test/reflection/proxy/TestAnnotationList.java @@ -0,0 +1,34 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Crypto Morin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.cryptomorin.xseries.test.reflection.proxy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE_PARAMETER, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestAnnotationList { + TestAnnotation[] value(); +} diff --git a/src/test/com/cryptomorin/xseries/test/writer/DifferenceHelper.java b/src/test/com/cryptomorin/xseries/test/writer/DifferenceHelper.java index 520d952b..e75fb682 100644 --- a/src/test/com/cryptomorin/xseries/test/writer/DifferenceHelper.java +++ b/src/test/com/cryptomorin/xseries/test/writer/DifferenceHelper.java @@ -28,8 +28,8 @@ import com.cryptomorin.xseries.particles.XParticle; import com.cryptomorin.xseries.reflection.XReflection; import com.cryptomorin.xseries.reflection.minecraft.MinecraftPackage; +import com.cryptomorin.xseries.test.Constants; import com.cryptomorin.xseries.test.util.XLogger; -import org.bukkit.Bukkit; import org.bukkit.Keyed; import org.bukkit.Particle; import org.bukkit.Sound; @@ -62,7 +62,7 @@ public final class DifferenceHelper { * Writes the material and sound differences to files in the server's root folder for updating purposes. */ public static void versionDifference() { - Path serverFolder = Bukkit.getWorldContainer().toPath(); + Path serverFolder = Constants.getTestPath(); XLogger.log("Server container: " + serverFolder.toAbsolutePath()); Path materials = serverFolder.resolve("XMaterial.txt"), diff --git a/src/test/resources/server.properties b/src/test/resources/server.properties index 4b0aa74e..9ad5eef3 100644 --- a/src/test/resources/server.properties +++ b/src/test/resources/server.properties @@ -23,7 +23,7 @@ view-distance=10 server-ip= resource-pack-prompt= allow-nether=false -server-port=25565 +server-port=25566 enable-rcon=false sync-chunk-writes=true op-permission-level=4 @@ -49,4 +49,4 @@ spawn-monsters=true enforce-whitelist=false resource-pack-sha1= spawn-protection=16 -max-world-size=29999984 +max-world-size=1000