Skip to content
Open
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);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findMainMethod uses Class#getMethod, which returns only public methods, so setAccessible(true) should be unnecessary. Keeping it can introduce avoidable failures under restrictive security/module settings; consider removing it (or only setting accessible when using getDeclaredMethod).

Suggested change
selected.setAccessible(true);

Copilot uses AI. Check for mistakes.
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 on lines +279 to +280
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When main throws a checked exception or an Error, wrapping the InvocationTargetException itself can obscure the real cause. Consider (a) rethrowing Error causes directly, and (b) wrapping e.getCause() (not e) so stack traces and messages point at the user exception rather than InvocationTargetException.

Suggested change
throw e.getCause() instanceof RuntimeException re ? re
: new InvokerInvocationException(e);
Throwable cause = e.getCause();
if (cause instanceof RuntimeException re) {
throw re;
}
if (cause instanceof Error err) {
throw err;
}
throw new InvokerInvocationException(cause);

Copilot uses AI. Check for mistakes.
} 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 on lines 517 to 522
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The foundStatic/foundInstance guard still throws when there is more than one valid static (or instance) main, which contradicts the new behavior and docs (multiple overloads are now expected and should be prioritized). Remove this guard or replace it with a check that only rejects true duplicates (same signature), which should typically be impossible anyway.

Copilot uses AI. Check for mistakes.
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 on lines 530 to 536
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The foundStatic/foundInstance guard still throws when there is more than one valid static (or instance) main, which contradicts the new behavior and docs (multiple overloads are now expected and should be prioritized). Remove this guard or replace it with a check that only rejects true duplicates (same signature), which should typically be impossible anyway.

Copilot uses AI. Check for mistakes.
}
}
}
}

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 on lines +544 to +548
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparator doesn’t implement the documented priority between main(String[] args) and main(Object args) because both have getParameters().length == 1. The resulting winner depends on the original method iteration order (and sort stability), making selection potentially non-deterministic and not aligned with the stated priority. Add a tie-breaker that ranks parameter type (String[] higher priority than Object) after the static/instance comparison and before the no-arg comparison.

Copilot uses AI. Check for mistakes.
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 on lines +554 to +557
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MethodNode.getText() can be overly verbose and/or unstable for user-facing warnings (it may include more than a concise signature). Consider formatting a compact signature for both methods (e.g., name + parameter types + static/instance) to keep warnings readable and consistent.

Copilot uses AI. Check for mistakes.
}

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