GROOVY-11737: Improve clarity of Groovy main method selection priority#2428
GROOVY-11737: Improve clarity of Groovy main method selection priority#2428paulk-asert wants to merge 1 commit intoapache:masterfrom
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2428 +/- ##
==================================================
+ Coverage 66.3339% 66.3402% +0.0063%
- Complexity 29941 29949 +8
==================================================
Files 1396 1396
Lines 117085 117101 +16
Branches 20730 20737 +7
==================================================
+ Hits 77667 77685 +18
- Misses 33037 33039 +2
+ Partials 6381 6377 -4
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Improves how Groovy chooses among multiple valid main methods by documenting the priority order and updating both compilation-time and runtime selection logic to align with that order.
Changes:
- Documented
mainmethod selection priority and warning behavior in the core spec docs. - Updated
ModuleNodeto select a “winning”mainby priority and emit compile-time warnings for unreachable overloads. - Updated
GroovyShellto deterministically select and invoke the prioritizedmainmethod via reflection.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/spec/doc/core-program-structure.adoc | Adds a dedicated section describing main selection priority and shadowing warnings. |
| src/main/java/org/codehaus/groovy/ast/ModuleNode.java | Collects valid main methods, selects a winner, and warns on unreachable overloads. |
| src/main/java/groovy/lang/GroovyShell.java | Centralizes prioritized main lookup and invokes it directly to avoid multimethod dispatch surprises. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| // 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); | ||
| }); |
There was a problem hiding this comment.
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.
| getContext().addWarning("Method '" + unreachable.getText() | ||
| + "' is not reachable from the Groovy runner" | ||
| + " because a higher-priority main method '" | ||
| + result.getText() + "' exists", unreachable); |
There was a problem hiding this comment.
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.
| Method selected = findMainMethod(scriptClass); | ||
| if (selected != null) { | ||
| try { | ||
| selected.setAccessible(true); |
There was a problem hiding this comment.
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).
| selected.setAccessible(true); |
| throw e.getCause() instanceof RuntimeException re ? re | ||
| : new InvokerInvocationException(e); |
There was a problem hiding this comment.
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.
| 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); |
No description provided.