You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/06/14 10:22:45 UTC
(groovy) branch master updated: GROOVY-12069: introduce methodhandles in dgm wrappers (#2598)
This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 579c8e45fd GROOVY-12069: introduce methodhandles in dgm wrappers (#2598)
579c8e45fd is described below
commit 579c8e45fd9b0d492ed25ce90d742c4043ecd095
Author: Jochen Theodorou <bl...@gmx.org>
AuthorDate: Sun Jun 14 12:22:39 2026 +0200
GROOVY-12069: introduce methodhandles in dgm wrappers (#2598)
* GROOVY-12069: add method handle to dgm class
* GROOVY-12069: cleanup warnings
* GROOVY-12069: adjust bytecode generation of dgm to remove surplus store/load, add handling of Generated method in Selector for methods
* GROOVY-12069: break up big setHandleForMetaMethod method
---
.../groovy/reflection/GeneratedMetaMethod.java | 15 +-
.../org/codehaus/groovy/tools/DgmConverter.java | 130 +++--
.../org/codehaus/groovy/vmplugin/v8/Selector.java | 176 ++++---
.../codehaus/groovy/tools/TestDgmConverter.java | 566 ++++++++++++++++++++-
4 files changed, 759 insertions(+), 128 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java b/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java
index e8253c3bf0..7d263f529b 100644
--- a/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java
+++ b/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java
@@ -20,7 +20,6 @@ package org.codehaus.groovy.reflection;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MetaMethod;
-
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
@@ -29,6 +28,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
+import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@@ -70,6 +70,19 @@ public abstract class GeneratedMetaMethod extends MetaMethod {
return declaringClass;
}
+ /**
+ * Returns a {@link MethodHandle} pointing directly to the underlying target method,
+ * or {@code null} if not available.
+ * <p>
+ * Generated DGM adapter classes override this to provide a pre-computed handle that
+ * avoids the boxing overhead of {@link #invoke(Object, Object[])}.
+ *
+ * @return a method handle for direct invocation, or {@code null}
+ */
+ public MethodHandle getTargetMethodHandle() {
+ return null;
+ }
+
public static class Proxy extends GeneratedMetaMethod {
private volatile MetaMethod proxy;
private final String className;
diff --git a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java
index 59639388ee..b41e49a6fc 100644
--- a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java
+++ b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java
@@ -41,11 +41,14 @@ import static java.lang.System.Logger.Level.INFO;
import static org.objectweb.asm.Opcodes.AALOAD;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASTORE;
+import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
@@ -55,7 +58,9 @@ import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
+import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Type.getMethodType;
/**
* Generates {@code GeneratedMetaMethod} adapter classes and metadata for the
@@ -64,6 +69,8 @@ import static org.objectweb.asm.Opcodes.RETURN;
public class DgmConverter {
private static final System.Logger LOGGER = System.getLogger(DgmConverter.class.getName());
+ private static final String TARGET = "TARGET";
+ private static final String METHOD_HANDLE_CLASS_NAME = "Ljava/lang/invoke/MethodHandle;";
/**
* Generates DGM adapter classes into the target directory.
@@ -79,36 +86,29 @@ public class DgmConverter {
targetDirectory = args[1];
if (!targetDirectory.endsWith("/")) targetDirectory += "/";
}
- List<CachedMethod> cachedMethodsList = new ArrayList<CachedMethod>();
- for (Class aClass : DefaultGroovyMethods.DGM_LIKE_CLASSES) {
+ List<CachedMethod> cachedMethodsList = new ArrayList<>();
+ for (Class<?> aClass : DefaultGroovyMethods.DGM_LIKE_CLASSES) {
Collections.addAll(cachedMethodsList, ReflectionCache.getCachedClass(aClass).getMethods());
}
final CachedMethod[] cachedMethods = cachedMethodsList.toArray(CachedMethod.EMPTY_ARRAY);
- List<GeneratedMetaMethod.DgmMethodRecord> records = new ArrayList<GeneratedMetaMethod.DgmMethodRecord>();
+ List<GeneratedMetaMethod.DgmMethodRecord> records = new ArrayList<>();
int cur = 0;
for (CachedMethod method : cachedMethods) {
- if (!method.isStatic() || !method.isPublic())
- continue;
+ if (skipMethod(method)) continue;
- if (method.getAnnotation(Deprecated.class) != null)
- continue;
-
- if (method.getParameterTypes().length == 0)
- continue;
-
- final Class returnType = method.getReturnType();
+ final Class<?> returnType = method.getReturnType();
final String className = "org/codehaus/groovy/runtime/dgm$" + cur++;
- GeneratedMetaMethod.DgmMethodRecord record = new GeneratedMetaMethod.DgmMethodRecord();
- records.add(record);
+ GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord = new GeneratedMetaMethod.DgmMethodRecord();
+ records.add(dgmMethodRecord);
- record.methodName = method.getName();
- record.returnType = method.getReturnType();
- record.parameters = method.getNativeParameterTypes();
- record.className = className;
+ dgmMethodRecord.methodName = method.getName();
+ dgmMethodRecord.returnType = method.getReturnType();
+ dgmMethodRecord.parameters = method.getNativeParameterTypes();
+ dgmMethodRecord.className = className;
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cw.visit(CompilerConfiguration.DEFAULT.getBytecodeVersion(), ACC_PUBLIC, className, null, "org/codehaus/groovy/reflection/GeneratedMetaMethod", null);
@@ -117,12 +117,16 @@ public class DgmConverter {
final String methodDescriptor = BytecodeHelper.getMethodDescriptor(returnType, method.getNativeParameterTypes());
+ createTargetMethodHandleField(cw, method, className);
+
createInvokeMethod(method, cw, returnType, methodDescriptor);
createDoMethodInvokeMethod(method, cw, className, returnType, methodDescriptor);
createIsValidMethodMethod(method, cw, className);
+ createGetTargetMethodHandleMethod(cw, className);
+
cw.visitEnd();
final byte[] bytes = cw.toByteArray();
@@ -141,6 +145,16 @@ public class DgmConverter {
LOGGER.log(INFO, "Saved {0} dgm records to: {1}/META-INF/dgminfo", cur, targetDirectory);
}
+ private static boolean skipMethod(CachedMethod method) {
+ if (!method.isStatic() || !method.isPublic())
+ return true;
+
+ if (method.getAnnotation(Deprecated.class) != null)
+ return true;
+
+ return method.getParameterTypes().length == 0;
+ }
+
private static void createConstructor(ClassWriter cw) {
MethodVisitor mv;
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;Lorg/codehaus/groovy/reflection/CachedClass;Ljava/lang/Class;[Ljava/lang/Class;)V", null, null);
@@ -188,7 +202,10 @@ public class DgmConverter {
}
}
- private static void createDoMethodInvokeMethod(CachedMethod method, ClassWriter cw, String className, Class returnType, String methodDescriptor) {
+ /**
+ * Generates bytecode for method invocation with argument coercion
+ */
+ private static void createDoMethodInvokeMethod(CachedMethod method, ClassWriter cw, String className, Class<?> returnType, String methodDescriptor) {
MethodVisitor mv;
mv = cw.visitMethod(ACC_PUBLIC + ACC_FINAL, "doMethodInvoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", null, null);
mv.visitCode();
@@ -207,7 +224,7 @@ public class DgmConverter {
// cast argument to parameter class, inclusive unboxing
// for methods with primitive types
- Class type = method.getParameterTypes()[1].getTheClass();
+ Class<?> type = method.getParameterTypes()[1].getTheClass();
BytecodeHelper.doCast(mv, type);
} else {
mv.visitVarInsn(ALOAD, 0);
@@ -216,9 +233,14 @@ public class DgmConverter {
mv.visitVarInsn(ASTORE, 2);
mv.visitVarInsn(ALOAD, 1);
BytecodeHelper.doCast(mv, method.getParameterTypes()[0].getTheClass());
- loadParameters(method, 2, mv);
+ loadParameters(method, mv);
}
+ writeMethodCall(method, returnType, methodDescriptor, mv);
+ }
+
+ private static void writeMethodCall(CachedMethod method, Class<?> returnType, String methodDescriptor, MethodVisitor mv) {
mv.visitMethodInsn(INVOKESTATIC, BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass()), method.getName(), methodDescriptor, false);
+ // Handles primitive return types via autoboxing
if (returnType == void.class) {
mv.visitInsn(ACONST_NULL);
} else if (returnType.isPrimitive()) {
@@ -230,23 +252,17 @@ public class DgmConverter {
mv.visitEnd();
}
- private static void createInvokeMethod(CachedMethod method, ClassWriter cw, Class returnType, String methodDescriptor) {
+ /**
+ * Generates bytecode for method invocation with return handling
+ */
+ private static void createInvokeMethod(CachedMethod method, ClassWriter cw, Class<?> returnType, String methodDescriptor) {
MethodVisitor mv;
mv = cw.visitMethod(ACC_PUBLIC, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 1);
BytecodeHelper.doCast(mv, method.getParameterTypes()[0].getTheClass());
- loadParameters(method, 2, mv);
- mv.visitMethodInsn(INVOKESTATIC, BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass()), method.getName(), methodDescriptor, false);
- if (returnType == void.class) {
- mv.visitInsn(ACONST_NULL);
- } else if (returnType.isPrimitive()) {
- Class<?> wrapperType = TypeUtil.autoboxType(returnType);
- mv.visitMethodInsn(INVOKESTATIC, BytecodeHelper.getClassInternalName(wrapperType), "valueOf", "(" + BytecodeHelper.getTypeDescription(returnType) + ")" + BytecodeHelper.getTypeDescription(wrapperType), false);
- }
- mv.visitInsn(ARETURN);
- mv.visitMaxs(0, 0);
- mv.visitEnd();
+ loadParameters(method, mv);
+ writeMethodCall(method, returnType, methodDescriptor, mv);
}
/**
@@ -254,22 +270,62 @@ public class DgmConverter {
* method from an {@code Object[]} local variable.
*
* @param method the cached method whose parameters are being loaded
- * @param argumentIndex the local-variable slot containing the argument array
* @param mv the visitor receiving the bytecode instructions
*/
- protected static void loadParameters(CachedMethod method, int argumentIndex, MethodVisitor mv) {
+ protected static void loadParameters(CachedMethod method, MethodVisitor mv) {
CachedClass[] parameters = method.getParameterTypes();
int size = parameters.length - 1;
for (int i = 0; i < size; i++) {
// unpack argument from Object[]
- mv.visitVarInsn(ALOAD, argumentIndex);
+ mv.visitVarInsn(ALOAD, 2);
BytecodeHelper.pushConstant(mv, i);
mv.visitInsn(AALOAD);
// cast argument to parameter class, inclusive unboxing
// for methods with primitive types
- Class type = parameters[i + 1].getTheClass();
+ Class<?> type = parameters[i + 1].getTheClass();
BytecodeHelper.doCast(mv, type);
}
}
+
+ private static void createTargetMethodHandleField(ClassWriter cw, CachedMethod method, String className) {
+ // private static final java.lang.invoke.MethodHandle TARGET
+ cw.visitField(ACC_PRIVATE + ACC_STATIC + ACC_FINAL,
+ TARGET, METHOD_HANDLE_CLASS_NAME, null, null).visitEnd();
+
+ // static initializer
+ MethodVisitor mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
+ mv.visitCode();
+
+ // Lookup lookup = java.lang.invoke.MethodHandles.lookup()
+ mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup",
+ "()Ljava/lang/invoke/MethodHandles$Lookup;", false);
+ // Class ownerClass = <declaring class>.class
+ String ownerInternal = BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass());
+ mv.visitLdcInsn(org.objectweb.asm.Type.getObjectType(ownerInternal));
+ // String methodName = "<method name>"
+ mv.visitLdcInsn(method.getName());
+ // MethodType methodType = MethodType.methodType(<return>, <param1>, <param2>, ...)
+ mv.visitLdcInsn(getMethodType(method.getDescriptor()));
+ // TARGET = lookup.findStatic(ownerClass, methodName, methodType)
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",
+ "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false);
+ mv.visitFieldInsn(PUTSTATIC, className, TARGET, METHOD_HANDLE_CLASS_NAME);
+
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ }
+
+ private static void createGetTargetMethodHandleMethod(ClassWriter cw, String className) {
+ MethodVisitor mv;
+ // public MethodHandle getTargetMethodHandle() { return TARGET; }
+ mv = cw.visitMethod(ACC_PUBLIC, "getTargetMethodHandle",
+ "()Ljava/lang/invoke/MethodHandle;", null, null);
+ mv.visitCode();
+ mv.visitFieldInsn(GETSTATIC, className, TARGET, METHOD_HANDLE_CLASS_NAME);
+ mv.visitInsn(ARETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
index 12ce177122..4510eb934e 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
@@ -31,6 +31,18 @@ import groovy.lang.MetaMethod;
import groovy.lang.MissingMethodException;
import groovy.lang.ProxyMetaClass;
import groovy.transform.Internal;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.reflection.CachedField;
import org.codehaus.groovy.reflection.CachedMethod;
@@ -40,7 +52,6 @@ import org.codehaus.groovy.reflection.stdclasses.CachedSAMClass;
import org.codehaus.groovy.runtime.ArrayTypeUtils;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.GroovyCategorySupport;
-import org.codehaus.groovy.runtime.GroovyCategorySupport.CategoryMethod;
import org.codehaus.groovy.runtime.MetaClassHelper;
import org.codehaus.groovy.runtime.NullObject;
import org.codehaus.groovy.runtime.dgmimpl.NumberNumberMetaMethod;
@@ -55,19 +66,6 @@ import org.codehaus.groovy.runtime.wrappers.Wrapper;
import org.codehaus.groovy.vmplugin.VMPlugin;
import org.codehaus.groovy.vmplugin.VMPluginFactory;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-import java.lang.reflect.Array;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Objects;
-
import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.ARRAYLIST_CONSTRUCTOR;
import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.BEAN_CONSTRUCTOR_PROPERTY_SETTER;
import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.CLASS_FOR_NAME;
@@ -744,85 +742,107 @@ public abstract class Selector {
/**
* Creates a MethodHandle using a before selected MetaMethod.
- * If the MetaMethod has reflective information available, then
- * we will use that information to create the target MethodHandle.
- * If that is not the case we will produce a handle, which will use the
- * MetaMethod itself for invocation.
+ * NumberNumberMetaMethod and GeneratedMetaMethod can be handled in a simplified way,
+ * ReflectionMetaMethod will be unwrapped and unreflected for a handle. If
+ * all that does not apply, we will fall back to using the MetaMethod itself.
*/
public void setHandleForMetaMethod() {
+ isCategoryMethod = (method instanceof GroovyCategorySupport.CategoryMethod);
+ if (setHandleForSimpleCases()) return;
+
MetaMethod metaMethod = method;
- isCategoryMethod = (method instanceof CategoryMethod);
+ if (metaMethod instanceof ReflectionMetaMethod rmm) {
+ if (LOG_ENABLED) LOG.info("meta method is reflective method");
+ metaMethod = rmm.getCachedMethod();
+ }
- if (metaMethod instanceof NumberNumberMetaMethod
- || (method instanceof GeneratedMetaMethod && ("next".equals(name) || "previous".equals(name)))) {
- if (LOG_ENABLED) LOG.info("meta method is number method");
- if (IndyMath.chooseMathMethod(this, metaMethod)) {
- catchException = false;
- if (LOG_ENABLED) LOG.info("indy math successful");
- return;
- }
+ if (metaMethod instanceof CachedMethod cm) {
+ setHandleForGeneralCachedMethod(cm, metaMethod);
+ } else if (method != null) {
+ setHandleForGenericMetaMethod();
}
+ }
- boolean isCategoryTypeMethod = (metaMethod instanceof NewInstanceMetaMethod);
- if (LOG_ENABLED) LOG.info("meta method is category type method: " + isCategoryTypeMethod);
- boolean isStaticCategoryTypeMethod = (metaMethod instanceof NewStaticMetaMethod);
- if (LOG_ENABLED) LOG.info("meta method is static category type method: " + isCategoryTypeMethod);
+ private void setHandleForGenericMetaMethod() {
+ if (LOG_ENABLED) LOG.info("meta method is generic meta method");
+ // generic meta method invocation path
+ handle = META_METHOD_INVOKER;
+ handle = handle.bindTo(method);
+ if (spread) {
+ args = originalArguments;
+ skipSpreadCollector = true;
+ } else {
+ // wrap arguments from call site in Object[]
+ handle = handle.asCollector(Object[].class, targetType.parameterCount() - 1);
+ }
+ currentType = removeWrapper(targetType);
+ if (LOG_ENABLED) LOG.info("bound method name to META_METHOD_INVOKER");
+ }
- if (metaMethod instanceof ReflectionMetaMethod) {
- if (LOG_ENABLED) LOG.info("meta method is reflective method");
- metaMethod = ((ReflectionMetaMethod) metaMethod).getCachedMethod();
+ private void setHandleForGeneralCachedMethod(CachedMethod cm, MetaMethod metaMethod) {
+ boolean isCategoryTypeMethod = (method instanceof NewInstanceMetaMethod);
+ if (LOG_ENABLED) LOG.info("meta method is category type method: " + isCategoryTypeMethod);
+ boolean isStaticCategoryTypeMethod = (method instanceof NewStaticMetaMethod);
+ if (LOG_ENABLED) LOG.info("meta method is static category type method: " + isStaticCategoryTypeMethod);
+
+ isVargs = metaMethod.isVargsMethod();
+ VMPlugin vmplugin = VMPluginFactory.getPlugin();
+ cm = (CachedMethod) vmplugin.transformMetaMethod(mc, cm, sender);
+ setBaseHandleForCachedMethod(cm);
+ if (isStaticCategoryTypeMethod) {
+ handle = MethodHandles.insertArguments(handle, 0, SINGLE_NULL_ARRAY);
+ handle = MethodHandles.dropArguments(handle, 0, targetType.parameterType(0));
+ } else if (!isCategoryTypeMethod && cm.isStatic()) {
+ // drop the receiver, which might be a Class (invocation on Class)
+ // or it might be an object (static method invocation on instance)
+ // Object.class handles both cases at once
+ handle = MethodHandles.dropArguments(handle, 0, Object.class);
}
+ }
- if (metaMethod instanceof CachedMethod cm) {
- isVargs = metaMethod.isVargsMethod();
- VMPlugin vmplugin = VMPluginFactory.getPlugin();
- cm = (CachedMethod) vmplugin.transformMetaMethod(mc, cm, sender);
- try {
- var declaringClass = cm.getDeclaringClass().getTheClass();
- int parameterCount = cm.getParamsCount();
- if (parameterCount == 0 && "clone".equals(name) && declaringClass == Object.class) {
- var receiverClass = getCorrectedReceiver().getClass();
- if (receiverClass.isArray()) { // GROOVY-10733, et al.
- handle = MethodHandles.publicLookup().findVirtual(receiverClass, "clone", MethodType.methodType(Object.class));
- } else { // GROOVY-10319
- handle = MethodHandles.throwException(Object.class, CloneNotSupportedException.class) // prevent illegal access
- .bindTo(new CloneNotSupportedException());
- handle = MethodHandles.dropArguments(handle, 0, Object.class); // discard receiver
- }
- } else if (parameterCount == 1 && "forName".equals(name) && declaringClass == Class.class) {
- handle = MethodHandles.insertArguments(CLASS_FOR_NAME, 1, Boolean.TRUE, sender.getClassLoader());
- } else {
- handle = unreflect(cm.getCachedMethod());
+ private void setBaseHandleForCachedMethod(CachedMethod cm) {
+ try {
+ var declaringClass = cm.getDeclaringClass().getTheClass();
+ int parameterCount = cm.getParamsCount();
+ if (parameterCount == 0 && "clone".equals(name) && declaringClass == Object.class) {
+ var receiverClass = getCorrectedReceiver().getClass();
+ if (receiverClass.isArray()) { // GROOVY-10733, et al.
+ handle = MethodHandles.publicLookup().findVirtual(receiverClass, "clone", MethodType.methodType(Object.class));
+ } else { // GROOVY-10319
+ handle = MethodHandles.throwException(Object.class, CloneNotSupportedException.class) // prevent illegal access
+ .bindTo(new CloneNotSupportedException());
+ handle = MethodHandles.dropArguments(handle, 0, Object.class); // discard receiver
}
- } catch (ReflectiveOperationException e) {
- throw new GroovyBugError(e);
- }
- if (isStaticCategoryTypeMethod) {
- handle = MethodHandles.insertArguments(handle, 0, SINGLE_NULL_ARRAY);
- handle = MethodHandles.dropArguments(handle, 0, targetType.parameterType(0));
- } else if (!isCategoryTypeMethod && cm.isStatic()) {
- // drop the receiver, which might be a Class (invocation on Class)
- // or it might be an object (static method invocation on instance)
- // Object.class handles both cases at once
- handle = MethodHandles.dropArguments(handle, 0, Object.class);
- }
- } else if (method != null) {
- if (LOG_ENABLED) LOG.info("meta method is dgm helper");
- // generic meta method invocation path
- handle = META_METHOD_INVOKER;
- handle = handle.bindTo(method);
- if (spread) {
- args = originalArguments;
- skipSpreadCollector = true;
+ } else if (parameterCount == 1 && "forName".equals(name) && declaringClass == Class.class) {
+ handle = MethodHandles.insertArguments(CLASS_FOR_NAME, 1, Boolean.TRUE, sender.getClassLoader());
} else {
- // wrap arguments from call site in Object[]
- handle = handle.asCollector(Object[].class, targetType.parameterCount() - 1);
+ handle = unreflect(cm.getCachedMethod());
}
- currentType = removeWrapper(targetType);
- if (LOG_ENABLED) LOG.info("bound method name to META_METHOD_INVOKER");
+ } catch (ReflectiveOperationException e) {
+ throw new GroovyBugError(e);
}
}
+ private boolean setHandleForSimpleCases() {
+ if (method instanceof NumberNumberMetaMethod
+ || (method instanceof GeneratedMetaMethod && ("next".equals(name) || "previous".equals(name)))) {
+ if (LOG_ENABLED) LOG.info("meta method is number method");
+ if (IndyMath.chooseMathMethod(this, method)) {
+ catchException = false;
+ if (LOG_ENABLED) LOG.info("indy math successful");
+ return true;
+ }
+ }
+ if (method instanceof GeneratedMetaMethod gmm && gmm.getTargetMethodHandle() != null) {
+ if (LOG_ENABLED) LOG.info("meta method is generated method");
+ handle = gmm.getTargetMethodHandle();
+ catchException = false;
+ isVargs = method.isVargsMethod();
+ return true;
+ }
+ return false;
+ }
+
/**
* Unreflects a cached reflective method against the call-site lookup.
*
diff --git a/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java b/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java
index 30bb647d66..e35d5776a0 100644
--- a/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java
+++ b/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java
@@ -19,36 +19,106 @@
package org.codehaus.groovy.tools;
import groovy.lang.MetaMethod;
-import junit.framework.TestCase;
-import org.codehaus.groovy.runtime.DefaultGroovyMethods;
-import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl;
-
import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import junit.framework.TestCase;
+import org.codehaus.groovy.reflection.CachedClass;
+import org.codehaus.groovy.reflection.GeneratedMetaMethod;
+import org.codehaus.groovy.reflection.ReflectionCache;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+/**
+ * Tests for {@link DgmConverter} covering generated adapter class structure,
+ * functional behavior, bytecode correctness, and DGM record serialization.
+ */
public class TestDgmConverter extends TestCase {
private static final String REFERENCE_CLASS = "/org/codehaus/groovy/runtime/dgm$0.class";
+ private static final ClassLoader DGM_CLASS_LOADER = DefaultGroovyMethods.class.getClassLoader();
+
+ /**
+ * Finds the first dgm$ class file on the classpath for inspection.
+ */
+ private static File findFirstDgmClassFile() throws URISyntaxException {
+ File dgmClassDirectory = new File(Objects.requireNonNull(TestDgmConverter.class.getResource(REFERENCE_CLASS)).toURI()).getParentFile();
+ File[] files = dgmClassDirectory.listFiles();
+ assertNotNull("dgm$ class directory not found", files);
+ for (File file : files) {
+ if (file.getName().startsWith("dgm$")) {
+ return file;
+ }
+ }
+ fail("No dgm$ class files found");
+ return null; // unreachable
+ }
+
+ /**
+ * Loads all dgm$ class files sorted by name.
+ */
+ private static List<File> findAllDgmClassFiles() throws URISyntaxException {
+ File dgmClassDirectory = new File(Objects.requireNonNull(TestDgmConverter.class.getResource(REFERENCE_CLASS)).toURI()).getParentFile();
+ File[] files = dgmClassDirectory.listFiles();
+ assertNotNull("dgm$ class directory not found", files);
+ List<File> dgmFiles = new ArrayList<>();
+ for (File file : files) {
+ if (file.getName().startsWith("dgm$")) {
+ dgmFiles.add(file);
+ }
+ }
+ dgmFiles.sort((o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()));
+ return dgmFiles;
+ }
+
+ /**
+ * Reads the raw bytes of a class file.
+ */
+ private static byte[] readClassBytes(File classFile) throws IOException {
+ return Files.readAllBytes(classFile.toPath());
+ }
- public void testConverter () throws URISyntaxException {
- File dgmClassDirectory = new File(TestDgmConverter.class.getResource(REFERENCE_CLASS).toURI()).getParentFile();
+ // ==================== Existing tests (preserved) ====================
+
+ public void testConverter() throws URISyntaxException {
+ File dgmClassDirectory = new File(Objects.requireNonNull(TestDgmConverter.class.getResource(REFERENCE_CLASS)).toURI()).getParentFile();
final File[] files = dgmClassDirectory.listFiles();
+ assertNotNull(files);
Arrays.sort(files, (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()));
- for (int i = 0; i < files.length; i++) {
- File file = files[i];
+ for (File file : files) {
final String name = file.getName();
if (name.startsWith("dgm$")) {
final String className = "org.codehaus.groovy.runtime." + name.substring(0, name.length() - ".class".length());
try {
- Class cls = Class.forName(className, false, DefaultGroovyMethods.class.getClassLoader());
- Constructor[] declaredConstructors = cls.getDeclaredConstructors();
+ Class<?> cls = Class.forName(className, false, DefaultGroovyMethods.class.getClassLoader());
+ Constructor<?>[] declaredConstructors = cls.getDeclaredConstructors();
assertEquals(1, declaredConstructors.length);
- Constructor constructor = declaredConstructors[0];
- final MetaMethod metaMethod = (MetaMethod) constructor.newInstance(null,null, null, null);
+ Constructor<?> constructor = declaredConstructors[0];
+ final MetaMethod metaMethod = (MetaMethod) constructor.newInstance(null, null, null, null);
+ assertNotNull(metaMethod);
} catch (ClassNotFoundException e) {
fail("Failed to load " + className);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
@@ -63,4 +133,476 @@ public class TestDgmConverter extends TestCase {
int instanceMethods = metaClassRegistry.getInstanceMethods().size();
assertTrue(instanceMethods > 0);
}
+
+ // ==================== Bytecode structure tests ====================
+
+ /**
+ * Verifies every generated dgm$ class extends GeneratedMetaMethod.
+ */
+ public void testGeneratedClassesExtendGeneratedMetaMethod() throws Exception {
+ for (File file : findAllDgmClassFiles()) {
+ String name = file.getName().replace(".class", "");
+ Class<?> cls = Class.forName("org.codehaus.groovy.runtime." + name, true, DGM_CLASS_LOADER);
+ assertTrue(
+ cls.getName() + " should extend GeneratedMetaMethod",
+ GeneratedMetaMethod.class.isAssignableFrom(cls)
+ );
+ }
+ }
+
+ /**
+ * Verifies every generated dgm$ class has exactly one public constructor
+ * with the expected parameter types: (String, CachedClass, Class, Class[]).
+ */
+ public void testGeneratedClassesHaveCorrectConstructor() throws Exception {
+ for (File file : findAllDgmClassFiles()) {
+ String name = file.getName().replace(".class", "");
+ Class<?> cls = Class.forName("org.codehaus.groovy.runtime." + name, true, DGM_CLASS_LOADER);
+ Constructor<?>[] constructors = cls.getDeclaredConstructors();
+ assertEquals(cls.getName() + " should have exactly one constructor", 1, constructors.length);
+ Constructor<?> ctor = constructors[0];
+ Class<?>[] paramTypes = ctor.getParameterTypes();
+ assertEquals(cls.getName() + " constructor should have 4 parameters", 4, paramTypes.length);
+ assertEquals(String.class, paramTypes[0]);
+ assertEquals(CachedClass.class, paramTypes[1]);
+ assertEquals(Class.class, paramTypes[2]);
+ assertEquals(Class[].class, paramTypes[3]);
+ }
+ }
+
+ /**
+ * Uses ASM to verify that a generated dgm$ class has a private static final
+ * MethodHandle field named TARGET.
+ */
+ public void testGeneratedClassHasTargetMethodHandleField() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ byte[] bytes = readClassBytes(dgmFile);
+
+ final boolean[] found = {false};
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
+ if ("TARGET".equals(name)) {
+ assertEquals("Ljava/lang/invoke/MethodHandle;", descriptor);
+ assertEquals("TARGET should be private", ACC_PRIVATE, access & ACC_PRIVATE);
+ assertEquals("TARGET should be static", ACC_STATIC, access & ACC_STATIC);
+ assertEquals("TARGET should be final", ACC_FINAL, access & ACC_FINAL);
+ found[0] = true;
+ }
+ return null;
+ }
+ }, 0);
+ assertTrue("TARGET MethodHandle field should exist", found[0]);
+ }
+
+ /**
+ * Uses ASM to verify that a generated dgm$ class has a static initializer
+ * that calls MethodHandles.lookup() and Lookup.findStatic().
+ */
+ public void testStaticInitializerUsesMethodHandlesLookup() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ byte[] bytes = readClassBytes(dgmFile);
+
+ final boolean[] hasClinit = {false};
+ final boolean[] hasLookup = {false};
+ final boolean[] hasFindStatic = {false};
+
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if ("<clinit>".equals(name)) {
+ hasClinit[0] = true;
+ return new MethodVisitor(Opcodes.ASM9) {
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String mname, String mdescriptor, boolean isInterface) {
+ if ("java/lang/invoke/MethodHandles".equals(owner) && "lookup".equals(mname)) {
+ hasLookup[0] = true;
+ }
+ if ("java/lang/invoke/MethodHandles$Lookup".equals(owner) && "findStatic".equals(mname)) {
+ hasFindStatic[0] = true;
+ }
+ }
+ };
+ }
+ return null;
+ }
+ }, 0);
+ assertTrue("Static initializer (<clinit>) should exist", hasClinit[0]);
+ assertTrue("<clinit> should call MethodHandles.lookup()", hasLookup[0]);
+ assertTrue("<clinit> should call Lookup.findStatic()", hasFindStatic[0]);
+ }
+
+ /**
+ * Uses ASM to verify that a generated dgm$ class has a getTargetMethodHandle()
+ * public method returning MethodHandle.
+ */
+ public void testGetTargetMethodHandleMethodExists() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ byte[] bytes = readClassBytes(dgmFile);
+
+ final boolean[] found = {false};
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if ("getTargetMethodHandle".equals(name)) {
+ assertEquals("()Ljava/lang/invoke/MethodHandle;", descriptor);
+ assertEquals("getTargetMethodHandle should be public", ACC_PUBLIC, access & ACC_PUBLIC);
+ found[0] = true;
+ }
+ return null;
+ }
+ }, 0);
+ assertTrue("getTargetMethodHandle method should exist", found[0]);
+ }
+
+ /**
+ * Uses ASM to verify that a generated dgm$ class has an invoke(Object, Object[])
+ * public method.
+ */
+ public void testInvokeMethodExists() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ byte[] bytes = readClassBytes(dgmFile);
+
+ final boolean[] found = {false};
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if ("invoke".equals(name)) {
+ assertEquals("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", descriptor);
+ assertEquals("invoke should be public", ACC_PUBLIC, access & ACC_PUBLIC);
+ found[0] = true;
+ }
+ return null;
+ }
+ }, 0);
+ assertTrue("invoke method should exist", found[0]);
+ }
+
+ /**
+ * Uses ASM to verify that a generated dgm$ class has a doMethodInvoke(Object, Object[])
+ * public final method.
+ */
+ public void testDoMethodInvokeMethodExists() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ byte[] bytes = readClassBytes(dgmFile);
+
+ final boolean[] found = {false};
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if ("doMethodInvoke".equals(name)) {
+ assertEquals("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", descriptor);
+ assertEquals("doMethodInvoke should be public", ACC_PUBLIC, access & ACC_PUBLIC);
+ assertEquals("doMethodInvoke should be final", ACC_FINAL, access & ACC_FINAL);
+ found[0] = true;
+ }
+ return null;
+ }
+ }, 0);
+ assertTrue("doMethodInvoke method should exist", found[0]);
+ }
+
+ /**
+ * Uses ASM to verify that the constructor of a generated dgm$ class
+ * delegates to GeneratedMetaMethod.<init>.
+ */
+ public void testConstructorDelegatesToGeneratedMetaMethod() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ byte[] bytes = readClassBytes(dgmFile);
+
+ final boolean[] found = {false};
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if ("<init>".equals(name)) {
+ return new MethodVisitor(Opcodes.ASM9) {
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String mname, String mdescriptor, boolean isInterface) {
+ if ("org/codehaus/groovy/reflection/GeneratedMetaMethod".equals(owner)
+ && "<init>".equals(mname)) {
+ found[0] = true;
+ }
+ }
+ };
+ }
+ return null;
+ }
+ }, 0);
+ assertTrue("Constructor should delegate to GeneratedMetaMethod.<init>", found[0]);
+ }
+
+ // ==================== Functional / behavioral tests ====================
+
+ /**
+ * Verifies that getTargetMethodHandle() returns a non-null MethodHandle.
+ */
+ public void testGetTargetMethodHandleReturnsNonNull() throws Exception {
+ File dgmFile = findFirstDgmClassFile();
+ assertNotNull(dgmFile);
+ String name = dgmFile.getName().replace(".class", "");
+ Class<?> cls = Class.forName("org.codehaus.groovy.runtime." + name, true, DGM_CLASS_LOADER);
+
+ Constructor<?> ctor = cls.getDeclaredConstructors()[0];
+ assertNotNull(ctor);
+ // Use reflection to determine the declaring class, return type, and parameter types
+ // from the original DGM method. We can get these from the GeneratedMetaMethod metadata.
+ // Construct with nulls first to get metadata, then check a method handle.
+ // Actually, the MethodHandle is a static field initialized in <clinit>, so we can access
+ // it via reflection on the class.
+ Field targetField = cls.getDeclaredField("TARGET");
+ targetField.setAccessible(true);
+ Object mh = targetField.get(null);
+ assertNotNull("TARGET MethodHandle field should not be null", mh);
+ assertTrue("TARGET should be a MethodHandle", mh instanceof MethodHandle);
+ }
+
+ /**
+ * Verifies that the Target MethodHandle is invokable (smoke test).
+ * Selects a generated class and invokes the MethodHandle through the adapter.
+ */
+ public void testMethodHandleIsInvokable() throws Exception {
+ // Load a known DGM method through the registry and invoke it
+ // to verify end-to-end functionality
+ List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+ assertFalse("DGM records should not be empty", records.isEmpty());
+
+ for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+ try {
+ // record.className already contains the fully qualified path (org/codehaus/groovy/runtime/dgm$N)
+ Class<?> cls = Class.forName(dgmMethodRecord.className.replace('/', '.'),
+ true, DGM_CLASS_LOADER);
+ Constructor<?> ctor = cls.getDeclaredConstructors()[0];
+
+ // Build the CachedClass[] for parameters
+ CachedClass[] cachedParams = new CachedClass[dgmMethodRecord.parameters.length];
+ for (int i = 0; i < dgmMethodRecord.parameters.length; i++) {
+ cachedParams[i] = ReflectionCache.getCachedClass(dgmMethodRecord.parameters[i]);
+ }
+
+ MetaMethod adapter = (MetaMethod) ctor.newInstance(
+ dgmMethodRecord.methodName,
+ cachedParams[0], // declaring class
+ dgmMethodRecord.returnType,
+ dgmMethodRecord.parameters
+ );
+
+ assertEquals(dgmMethodRecord.methodName, adapter.getName());
+ assertEquals(dgmMethodRecord.returnType, adapter.getReturnType());
+ assertEquals(dgmMethodRecord.parameters.length, adapter.getParameterTypes().length);
+
+ // Verify getTargetMethodHandle returns a MethodHandle
+ Method getMhMethod = cls.getMethod("getTargetMethodHandle");
+ MethodHandle mh = (MethodHandle) getMhMethod.invoke(adapter);
+ assertNotNull("MethodHandle should not be null for " + dgmMethodRecord.methodName, mh);
+
+ // Verify the MethodHandle type is compatible
+ assertEquals(dgmMethodRecord.returnType, mh.type().returnType());
+
+ break;
+ } catch (Exception e) {
+ // Some records may not work with null args, skip and try next
+ }
+ }
+ }
+
+ /**
+ * Verifies that the doMethodInvoke method works for a simple non-coercion case.
+ * Uses a concrete DGM method (e.g., String.center) to test end-to-end invocation.
+ */
+ public void testDoMethodInvokeWithConcreteExample() throws Exception {
+ List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+
+ // Find a suitable record for a String method
+ for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+ if (dgmMethodRecord.parameters.length > 1 && dgmMethodRecord.parameters[0] == String.class) {
+ // record.className already contains the fully qualified path (org/codehaus/groovy/runtime/dgm$N)
+ Class<?> cls = Class.forName(dgmMethodRecord.className.replace('/', '.'),
+ true, DGM_CLASS_LOADER);
+ Constructor<?> ctor = cls.getDeclaredConstructors()[0];
+
+ CachedClass[] cachedParams = new CachedClass[dgmMethodRecord.parameters.length];
+ for (int i = 0; i < dgmMethodRecord.parameters.length; i++) {
+ cachedParams[i] = ReflectionCache.getCachedClass(dgmMethodRecord.parameters[i]);
+ }
+
+ MetaMethod adapter = (MetaMethod) ctor.newInstance(
+ dgmMethodRecord.methodName,
+ cachedParams[0],
+ dgmMethodRecord.returnType,
+ dgmMethodRecord.parameters
+ );
+
+ // Test doMethodInvoke with proper arguments
+ // Build Object[] args for the method (excluding the receiver which is the first param)
+ Object[] args = new Object[dgmMethodRecord.parameters.length - 1];
+ // Fill with dummy values matching parameter types where possible
+ for (int i = 0; i < args.length; i++) {
+ Class<?> pType = dgmMethodRecord.parameters[i + 1];
+ if (pType == int.class || pType == Integer.class) args[i] = 0;
+ else if (pType == String.class) args[i] = "";
+ else if (pType == long.class || pType == Long.class) args[i] = 0L;
+ else if (pType == boolean.class || pType == Boolean.class) args[i] = false;
+ else if (pType == char.class || pType == Character.class) args[i] = ' ';
+ else if (pType == double.class || pType == Double.class) args[i] = 0.0;
+ else if (pType == float.class || pType == Float.class) args[i] = 0.0f;
+ else if (pType == short.class || pType == Short.class) args[i] = (short) 0;
+ else if (pType == byte.class || pType == Byte.class) args[i] = (byte) 0;
+ else if (pType == Object[].class) args[i] = new Object[0];
+ else if (pType == int[].class) args[i] = new int[0];
+ else if (pType == char[].class) args[i] = new char[0];
+ else args[i] = null;
+ }
+
+ try {
+ Object result = adapter.doMethodInvoke("test", args);
+ // Verify we can call it without unexpected exceptions
+ assertNotNull(result);
+ break;
+ } catch (Exception e) {
+ // Some methods may still fail with empty string args, skip and try next
+ }
+ }
+ }
+ }
+
+ // ==================== Filtering logic tests ====================
+
+ /**
+ * Verifies that no generated dgm$ class corresponds to a deprecated method.
+ */
+ public void testNoDeprecatedMethodsInRecords() throws Exception {
+ List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+ for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+ // Load the declaring class from the record to get the actual original method
+ // The record.parameters includes the receiver as the first element
+ // The declaring class is the class that contains the static method
+ Class<?> declaringClass = dgmMethodRecord.parameters[0];
+ for (java.lang.reflect.Method m : declaringClass.getMethods()) {
+ if (m.getName().equals(dgmMethodRecord.methodName)
+ && java.util.Arrays.equals(m.getParameterTypes(), dgmMethodRecord.parameters)) {
+ assertFalse(
+ "Deprecated method " + dgmMethodRecord.methodName + " on " + declaringClass.getName()
+ + " should not be in DGM records",
+ m.isAnnotationPresent(Deprecated.class)
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Verifies that no zero-parameter methods appear in DGM records.
+ * (DgmConverter filters out methods with 0 parameters.)
+ */
+ public void testNoZeroParamMethodsInRecords() throws Exception {
+ List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+ assertFalse("DGM records should not be empty", records.isEmpty());
+ for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+ assertTrue(
+ "DGM record " + dgmMethodRecord.methodName + " should have at least 1 parameter",
+ dgmMethodRecord.parameters.length >= 1
+ );
+ }
+ }
+
+ // ==================== DgmMethodRecord serialization round-trip ====================
+
+ /**
+ * Tests that DgmMethodRecord.saveDgmInfo/loadDgmInfo round-trips correctly.
+ */
+ public void testDgmMethodRecordRoundTrip() throws Exception {
+ List<GeneratedMetaMethod.DgmMethodRecord> original = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+ assertFalse("Original records should not be empty", original.isEmpty());
+
+ Path tempFile = Files.createTempFile("dgminfo-test", ".dat");
+ try {
+ // Save to a temp file
+ GeneratedMetaMethod.DgmMethodRecord.saveDgmInfo(original, tempFile.toString());
+
+ // Verify a file was created and has content
+ assertTrue("Saved DGM info file should exist", Files.exists(tempFile));
+ assertTrue("Saved DGM info file should not be empty", Files.size(tempFile) > 0);
+
+ // Load back from the temp file by reading the raw bytes and creating a
+ // DataInputStream to verify the format
+ try (var in = new java.io.DataInputStream(new java.io.BufferedInputStream(new FileInputStream(tempFile.toFile())))) {
+ // Read the class table (skipping primitives)
+ int classCount = 0;
+ while (true) {
+ String name = in.readUTF();
+ if (name.isEmpty()) break;
+ in.readInt(); // class id
+ classCount++;
+ }
+ assertTrue("Class table should contain at least 1 class", classCount > 0);
+ int recordCount = in.readInt();
+ assertEquals("Loaded record count should match", original.size(), recordCount);
+
+ for (int i = 0; i < recordCount; i++) {
+ String className = in.readUTF();
+ assertNotNull(className);
+ String methodName = in.readUTF();
+ assertNotNull(methodName);
+ in.readInt(); // return type id
+ int paramCount = in.readInt();
+ for (int j = 0; j < paramCount; j++) {
+ in.readInt(); // param type id
+ }
+ }
+ assertEquals("File should be fully consumed", -1, in.read());
+ }
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ // ==================== main() argument parsing tests ====================
+
+ /**
+ * Tests that DgmConverter.main() produces output without errors
+ * using the default arguments.
+ */
+ public void testMainWithDefaultArgs() throws Exception {
+ Path tempDir = Files.createTempDirectory("dgm-test-output");
+ try {
+ // Ensure META-INF directory exists (DgmConverter.main() expects it pre-created)
+ Files.createDirectories(tempDir.resolve("META-INF"));
+
+ // Redirect to temp directory
+ DgmConverter.main(new String[]{"--info", tempDir.toString()});
+
+ // Verify META-INF/dgminfo was created
+ Path dgminfo = tempDir.resolve("META-INF/dgminfo");
+ assertTrue("META-INF/dgminfo should be created", Files.exists(dgminfo));
+ assertTrue("META-INF/dgminfo should not be empty", Files.size(dgminfo) > 0);
+
+ // Verify at least one dgm$ class file was created
+ Path dgmClasses = tempDir.resolve("org/codehaus/groovy/runtime");
+ assertTrue("dgm$ class directory should exist", Files.exists(dgmClasses));
+ try (var stream = Files.list(dgmClasses)) {
+ assertTrue("dgm$ class directory should have files",
+ stream.anyMatch(p -> p.getFileName().toString().startsWith("dgm$")));
+ }
+ } finally {
+ // Clean up
+ deleteRecursively(tempDir);
+ }
+ }
+
+ private static void deleteRecursively(Path dir) throws IOException {
+ if (Files.isDirectory(dir)) {
+ try (var stream = Files.list(dir)) {
+ for (Path child : stream.toList()) {
+ deleteRecursively(child);
+ }
+ }
+ }
+ Files.deleteIfExists(dir);
+ }
}