Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 46 additions & 32 deletions src/main/java/groovy/lang/GroovyShell.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
paulk-asert marked this conversation as resolved.
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);
Comment thread
paulk-asert marked this conversation as resolved.
} 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);
Expand All @@ -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;
Expand Down
28 changes: 24 additions & 4 deletions src/main/java/org/codehaus/groovy/ast/ModuleNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<MethodNode> methods) {
boolean foundInstance = false;
boolean foundStatic = false;
MethodNode result = null;
List<MethodNode> validMains = new ArrayList<>();
for (MethodNode node : methods) {
if ("main".equals(node.getName()) && !node.isPrivate()) {
int numParams = node.getParameters().length;
Comment thread
paulk-asert marked this conversation as resolved.
Expand All @@ -527,16 +530,33 @@ private MethodNode handleMainMethodIfPresent(final List<MethodNode> 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;
Comment thread
paulk-asert marked this conversation as resolved.
}
}
}
}

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);
});
Comment thread
paulk-asert marked this conversation as resolved.
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);
Comment thread
paulk-asert marked this conversation as resolved.
}

return result;
}

Expand Down
25 changes: 25 additions & 0 deletions src/spec/doc/core-program-structure.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading