From 38ecbb870dbfd18a52b35b250d730093bd4682f9 Mon Sep 17 00:00:00 2001 From: Paul King Date: Thu, 2 Apr 2026 22:10:55 +1000 Subject: [PATCH] GROOVY-11737: Improve clarity of Groovy main method selection priority --- src/main/java/groovy/lang/GroovyShell.java | 78 +++++++++++-------- .../org/codehaus/groovy/ast/ModuleNode.java | 28 ++++++- src/spec/doc/core-program-structure.adoc | 25 ++++++ 3 files changed, 95 insertions(+), 36 deletions(-) diff --git a/src/main/java/groovy/lang/GroovyShell.java b/src/main/java/groovy/lang/GroovyShell.java index f2000a6aba0..cab11c467e2 100644 --- a/src/main/java/groovy/lang/GroovyShell.java +++ b/src/main/java/groovy/lang/GroovyShell.java @@ -259,39 +259,29 @@ private Object runScriptOrMainOrTestOrRunnable(Class scriptClass, String[] args) // ignore instantiation errors, try to do main } } - try { - // let's find a String[] main method - Method stringArrayMain = scriptClass.getMethod(MAIN_METHOD_NAME, String[].class); - // if that main method exists, invoke it - if (Modifier.isStatic(stringArrayMain.getModifiers())) { - return InvokerHelper.invokeStaticMethod(scriptClass, MAIN_METHOD_NAME, new Object[]{args}); - } else { - Object script = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass); - return InvokerHelper.invokeMethod(script, MAIN_METHOD_NAME, args); - } - } catch (NoSuchMethodException ignore) { } - try { - // let's find an Object main method - Method stringArrayMain = scriptClass.getMethod(MAIN_METHOD_NAME, Object.class); - // if that main method exists, invoke it - if (Modifier.isStatic(stringArrayMain.getModifiers())) { - return InvokerHelper.invokeStaticMethod(scriptClass, MAIN_METHOD_NAME, new Object[]{args}); - } else { - Object script = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass); - return InvokerHelper.invokeMethod(script, MAIN_METHOD_NAME, new Object[]{args}); - } - } catch (NoSuchMethodException ignore) { } - try { - // let's find a no-arg main method - Method noArgMain = scriptClass.getMethod(MAIN_METHOD_NAME); - // if that main method exists, invoke it - if (Modifier.isStatic(noArgMain.getModifiers())) { - return InvokerHelper.invokeStaticNoArgumentsMethod(scriptClass, MAIN_METHOD_NAME); - } else { - Object script = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass); - return InvokerHelper.invokeMethod(script, MAIN_METHOD_NAME, EMPTY_ARGS); + // Select main method using JEP-512 priority: static before instance, args before no-args. + // Uses Method.invoke() directly to avoid Groovy multimethod dispatch selecting a different overload. + Method selected = findMainMethod(scriptClass); + if (selected != null) { + try { + selected.setAccessible(true); + if (Modifier.isStatic(selected.getModifiers())) { + return selected.getParameterCount() == 0 + ? selected.invoke(null) + : selected.invoke(null, (Object) args); + } else { + Object instance = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass); + return selected.getParameterCount() == 0 + ? selected.invoke(instance) + : selected.invoke(instance, (Object) args); + } + } catch (InvocationTargetException e) { + throw e.getCause() instanceof RuntimeException re ? re + : new InvokerInvocationException(e); + } catch (ReflectiveOperationException e) { + throw new GroovyRuntimeException("Failed to invoke main method: " + e, e); } - } catch (NoSuchMethodException ignore) { } + } // if it implements Runnable, try to instantiate it if (Runnable.class.isAssignableFrom(scriptClass)) { return runRunnable(scriptClass, args); @@ -318,6 +308,30 @@ private Object runScriptOrMainOrTestOrRunnable(Class scriptClass, String[] args) throw new GroovyRuntimeException(message.toString()); } + /** + * Finds the main method to invoke using JEP-512 priority order: + * static main(String[]) > static main(Object) > static main() + * > instance main(String[]) > instance main(Object) > instance main(). + */ + private static Method findMainMethod(Class scriptClass) { + Class[][] signatures = { {String[].class}, {Object.class}, {} }; + // static methods first + for (Class[] paramTypes : signatures) { + try { + Method m = scriptClass.getMethod(MAIN_METHOD_NAME, paramTypes); + if (Modifier.isStatic(m.getModifiers())) return m; + } catch (NoSuchMethodException ignore) { } + } + // then instance methods + for (Class[] paramTypes : signatures) { + try { + Method m = scriptClass.getMethod(MAIN_METHOD_NAME, paramTypes); + if (!Modifier.isStatic(m.getModifiers())) return m; + } catch (NoSuchMethodException ignore) { } + } + return null; + } + private static Object runRunnable(Class scriptClass, String[] args) { Constructor constructor = null; Runnable runnable = null; diff --git a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java index 6c0760ba69e..af8092fbb26 100644 --- a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java +++ b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java @@ -509,11 +509,14 @@ private MethodNode findRun() { * We retain the 'main' method if a compatible one is found. * A compatible one has no parameters or 1 (Object or String[]) parameter. * The return type must be void or Object. + * When multiple valid main methods exist, a warning is issued for those + * that would not be reachable from the command-line runner. + * Priority follows JEP-512: static before instance, args before no-args. */ private MethodNode handleMainMethodIfPresent(final List methods) { boolean foundInstance = false; boolean foundStatic = false; - MethodNode result = null; + List validMains = new ArrayList<>(); for (MethodNode node : methods) { if ("main".equals(node.getName()) && !node.isPrivate()) { int numParams = node.getParameters().length; @@ -527,9 +530,7 @@ private MethodNode handleMainMethodIfPresent(final List methods) { if (node.isStatic() ? foundStatic : foundInstance) { throw new RuntimeException("Repetitive main method found."); } - if (!foundStatic) { // static trumps instance - result = node; - } + validMains.add(node); if (node.isStatic()) foundStatic = true; else foundInstance = true; @@ -537,6 +538,25 @@ private MethodNode handleMainMethodIfPresent(final List methods) { } } } + + if (validMains.isEmpty()) return null; + + // Select winner using JEP-512 priority: static before instance, args before no-args + validMains.sort((a, b) -> { + if (a.isStatic() != b.isStatic()) return a.isStatic() ? -1 : 1; + return Integer.compare(b.getParameters().length, a.getParameters().length); + }); + MethodNode result = validMains.get(0); + + // Warn about unreachable main methods + for (int i = 1; i < validMains.size(); i++) { + MethodNode unreachable = validMains.get(i); + getContext().addWarning("Method '" + unreachable.getText() + + "' is not reachable from the Groovy runner" + + " because a higher-priority main method '" + + result.getText() + "' exists", unreachable); + } + return result; } diff --git a/src/spec/doc/core-program-structure.adoc b/src/spec/doc/core-program-structure.adoc index 0b0704787a9..bfd7c58bdfc 100644 --- a/src/spec/doc/core-program-structure.adoc +++ b/src/spec/doc/core-program-structure.adoc @@ -402,3 +402,28 @@ with versions of Groovy prior to Groovy 5 (where JEP 445 support was added). As a consequence, such classes are compatible with the Java launch protocol prior to JEP 445 support. * Groovy's runner has been made aware of JEP 445 compatible classes and can run all variations for JDK11 and above and without the need for preview mode to be enabled. + +=== Main method selection priority + +When a class has multiple valid `main` method signatures, the Groovy runner selects one +using a priority order aligned with JEP 512. Static methods take priority over instance +methods, and methods with parameters take priority over no-arg methods: + +[cols="1,3"] +|=== +| Priority | Method signature + +| 1 | `static main(String[] args)` +| 2 | `static main(Object args)` +| 3 | `static main()` +| 4 | `main(String[] args)` (instance) +| 5 | `main(Object args)` (instance) +| 6 | `main()` (instance) +|=== + +Only one method is selected — lower-priority `main` methods are not reachable from the +Groovy runner. A compile-time warning is issued for any valid `main` method that would +be shadowed by a higher-priority one. + +NOTE: Having multiple `main` method overloads in a single class is considered poor style. +We recommend providing a single `main` method to avoid any confusion about which one will run.