Skip to content

GROOVY-11896: Support module import declarations#2429

Merged
paulk-asert merged 2 commits into
apache:masterfrom
paulk-asert:groovy11896
Apr 19, 2026
Merged

GROOVY-11896: Support module import declarations#2429
paulk-asert merged 2 commits into
apache:masterfrom
paulk-asert:groovy11896

Conversation

@paulk-asert
Copy link
Copy Markdown
Contributor

@paulk-asert paulk-asert commented Apr 2, 2026

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 85.12397% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.1458%. Comparing base (ef61daf) to head (74ab632).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...rg/codehaus/groovy/control/ModuleImportHelper.java 75.0000% 7 Missing and 3 partials ⚠️
...va/org/apache/groovy/parser/antlr4/AstBuilder.java 82.6087% 3 Missing and 1 partial ⚠️
...org/apache/groovy/groovysh/jline/GroovyEngine.java 84.6154% 2 Missing ⚠️
...va/org/codehaus/groovy/control/ResolveVisitor.java 94.4444% 0 Missing and 1 partial ⚠️
...s/groovy/control/customizers/ImportCustomizer.java 94.4444% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2429        +/-   ##
==================================================
+ Coverage     67.1014%   67.1458%   +0.0444%     
- Complexity      30870      30901        +31     
==================================================
  Files            1435       1436         +1     
  Lines          119619     119738       +119     
  Branches        21182      21206        +24     
==================================================
+ Hits            80266      80399       +133     
+ Misses          32664      32635        -29     
- Partials         6689       6704        +15     
Files with missing lines Coverage Δ
.../main/java/org/codehaus/groovy/ast/ModuleNode.java 89.0365% <100.0000%> (+0.2610%) ⬆️
...l/customizers/builder/ImportCustomizerFactory.java 77.5510% <100.0000%> (+0.9553%) ⬆️
...va/org/codehaus/groovy/control/ResolveVisitor.java 90.2349% <94.4444%> (+0.0958%) ⬆️
...s/groovy/control/customizers/ImportCustomizer.java 97.5610% <94.4444%> (-0.8765%) ⬇️
...org/apache/groovy/groovysh/jline/GroovyEngine.java 14.2576% <84.6154%> (+1.6169%) ⬆️
...va/org/apache/groovy/parser/antlr4/AstBuilder.java 86.5100% <82.6087%> (-0.0394%) ⬇️
...rg/codehaus/groovy/control/ModuleImportHelper.java 75.0000% <75.0000%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@daniellansun
Copy link
Copy Markdown
Contributor

daniellansun commented Apr 4, 2026

It's better to verify if current implementation supports --add-opens.

Here are some discussions about module discovery functionality:
https://mail.openjdk.org/pipermail/jigsaw-dev/2017-December/013439.html

This comment was marked as outdated.

@paulk-asert paulk-asert force-pushed the groovy11896 branch 2 times, most recently from 3b4fbe8 to 07e5977 Compare April 7, 2026 10:19
Comment thread src/antlr/GroovyLexer.g4 Outdated
Comment on lines +420 to +427
* Known differences from Java's module import behavior:
* <ul>
* <li>Ambiguous class names from multiple module imports silently resolve
* to the last match, consistent with Groovy's existing star import
* semantics. Java reports a compile-time error for such ambiguities.</li>
* <li>Explicit single-type imports take priority over module-expanded
* star imports (same as Java).</li>
* </ul>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think Java's semantic is all public classes. Adding all these star imports includes private types as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, existing star imports includes package-private while Java doesn't. I created GROOVY-11916 in case we want to change that. I think it is okay to follow existing Groovy behavior and change it in GROOVY-11916 if we don't like that. I am not sure we document that difference for star imports. But we should do for both the same way. I'll await thoughts on GROOVY-11916 before making changes here.

Copy link
Copy Markdown
Contributor

@jonnybot0 jonnybot0 Apr 10, 2026

Choose a reason for hiding this comment

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

I could see it getting vexing if module imports and star imports have conflicts around one module's package-private class beating out another one's public class. That would violate the principle of least surprise, I think. It's hard to think of a case where implementing https://issues.apache.org/jira/browse/GROOVY-11916 would trip somebody up, since actual construction or use of the classes would throw an exception anyway.

I'd support merging this and moving on GROOVY-11916 separately, though, if we're trying to avoid that becoming a blocker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jonnybot0 I agree. Public trumps package private from my testing if I tested all cases.

Copy link
Copy Markdown
Member

@eric-milles eric-milles left a comment

Choose a reason for hiding this comment

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

Does the original import stay in the AST? If not, then there is nothing left for code navigation support in eclipse. Although, I'm not 100% what sort of nav is possible for a module name.

@paulk-asert
Copy link
Copy Markdown
Contributor Author

Does the original import stay in the AST? If not, then there is nothing left for code navigation support in eclipse. Although, I'm not 100% what sort of nav is possible for a module name.

This doesn't create some kind of (new) ModuleImportNode AST type but rather replaces with one or more star imports. We could track via node metadata as some kind of alternative to a new AST type. But as you say, is there a need?

@jonnybot0
Copy link
Copy Markdown
Contributor

Does the original import stay in the AST? If not, then there is nothing left for code navigation support in eclipse. Although, I'm not 100% what sort of nav is possible for a module name.

This doesn't create some kind of (new) ModuleImportNode AST type but rather replaces with one or more star imports. We could track via node metadata as some kind of alternative to a new AST type. But as you say, is there a need?

For module navigation, I can see that IntelliJ just takes me to module-info.java if I use the Go-To Declaration feature for the import statement import module java.base. So navigation is certainly possible from a module name and it does refer to something in particular that a static star wouldn't. Outside of code editors, I can't think of a particular use case for that metadata.

@jonnybot0
Copy link
Copy Markdown
Contributor

Overall, I agree that following Java's example seems more prudent than Scala's.

I suspect that treating module imports as a distinct thing from static star imports, at least at some level in the AST, might be prudent. That could be because I never fully recovered from the psychic damage I took in the OSGi salt mines. 🙂

That caveat aside, it does seem like there's something from the module imports JEP that we're not supporting: Transitive dependencies across modules (as described in JEP 476).

The ability to import at the level of modules would be especially helpful when APIs in one module have a close relationship with APIs in another module. This is common in large multi-module libraries such as the JDK. For example, the java.sql module provides database access via its java.sql and javax.sql packages, but one of its interfaces, java.sql.SQLXML, declares public methods whose signatures use interfaces from the javax.xml.transform package in the java.xml module. Developers who call these methods in java.sql.SQLXML typically import both the java.sql package and the javax.xml.transform package. To facilitate this extra import, the java.sql module depends on the java.xml module transitively, so that a program which depends on the java.sql module depends automatically on the java.xml module.

Unless I have made an error, the existing branch does not do this. I've added a test to my fork (see 301582f) that demonstrates the issue.

This is part of what makes me think we may well need specific semantics for the AST of module imports. There are additional expectations built into the JEP that make me think we'd need some specific things to handle them properly.

@paulk-asert
Copy link
Copy Markdown
Contributor Author

Overall, I agree that following Java's example seems more prudent than Scala's.

I suspect that treating module imports as a distinct thing from static star imports, at least at some level in the AST, might be prudent. That could be because I never fully recovered from the psychic damage I took in the OSGi salt mines. 🙂

That caveat aside, it does seem like there's something from the module imports JEP that we're not supporting: Transitive dependencies across modules (as described in JEP 476).

The ability to import at the level of modules would be especially helpful when APIs in one module have a close relationship with APIs in another module. This is common in large multi-module libraries such as the JDK. For example, the java.sql module provides database access via its java.sql and javax.sql packages, but one of its interfaces, java.sql.SQLXML, declares public methods whose signatures use interfaces from the javax.xml.transform package in the java.xml module. Developers who call these methods in java.sql.SQLXML typically import both the java.sql package and the javax.xml.transform package. To facilitate this extra import, the java.sql module depends on the java.xml module transitively, so that a program which depends on the java.sql module depends automatically on the java.xml module.

Unless I have made an error, the existing branch does not do this. I've added a test to my fork (see 301582f) that demonstrates the issue.

This is part of what makes me think we may well need specific semantics for the AST of module imports. There are additional expectations built into the JEP that make me think we'd need some specific things to handle them properly.

Nice catch @jonnybot0! I updated the PR to include this ability.

@jonnybot0
Copy link
Copy Markdown
Contributor

I think this is good to merge, but I have found some other things we might want to fix-forward. I'm going to note them here, but can open up another PR, as they may be worth discussing separately and I don't want to be a blocker.

I've combed over this by doing some comparisons with the relevant JEPs (476, 494, 511) and the Java Language Specification for Module Import Declarations. Besides importing the package-private stuff (which is fine and appropriately backlogged), there are a handful of other differences with how Java handles module imports.

Issue 1: Ambiguous Imports

The most significant difference is how this would handle ambiguity between module imports. In Java, this is a compile-time error, but Groovy will just pick one silently and run with it. There are a few specific examples of this in the Java specification (7.5.5, https://docs.oracle.com/javase/specs/jls/se25/html/jls-7.html#jls-7.5.5). You can reproduce these in jshell:

import module java.base;
import module java.desktop;

List l; // Error - Ambiguous name!

Granted, we're already more chill about static star imports than Java, so maybe not throwing that error is the consistent thing? But given that module support is new, I think we could go the more "conservative" way and be okay, since no one is going to depend on the more lax behavior.

Issue 2: Import priority

The Java Language specification specifies a priority for imports of single-type > type-on-demand > module-import. See 6.4.1 of the spec.

On this branch, if a user writes import foo.* and also import module M where M exports foo, the list ordering decides which import wins, not the specification precedence.

Addressing the issues

There's a couple of ways Claude & I could hack up that would address these two problems.

The first is a smaller change: simply make a separate list of module imports, then resolve them later.
https://github.com/jonnybot0/groovy/pull/new/groovy11896-ammendments

The second is a bigger departure from this branch, and actually represents the module imports within the AST.
https://github.com/jonnybot0/groovy/pull/new/groovy11896-bigger-refactor

Both approaches solve these two problems (as shown by having the same passing test in their diff). I suspect that the second path is a better direction to head. It looks like it would better support other aspects of and expansions to the JPMS down the road. That said, it's a more significant departure, and may have more knock-on effects; there's a lot of code using isStar() || isStatic() that might want auditing. Also, it should probably be benchmarked, as I suspect it could perform worse.

Way Forward

I think the best approach is probably to merge this branch, then open a PR from my second branch and take feedback on that separately. That said, it might be quickest for @paulk-asert to just take the diff from the first branch, incorporate it in, and then do a PR from something like the second branch at our leisure, while still pocketing the win today.

Let me know what you think is best. Thanks for giving a generous period for feedback!

@paulk-asert
Copy link
Copy Markdown
Contributor Author

paulk-asert commented Apr 16, 2026

@jonnybot0 I initially thought just to bring in half of your first branch - let duplicate imports from modules be treated the same as duplicates from star imports. And in fact changed (locally) the PR branch to have that. But the order from the module imports isn't deterministic from the JDK APIs, so in fact what you have in the first branch is exactly what we need as the smallest bump from what I had. So I added it to this PR with a minor tweak, hopefully okay.

We have GROOVY-11916 for the package-private anomaly. I wonder whether we should take this opportunity (via another issue) to give an error for the multiple static import ambiguity case too. It would be breaking but I imagine easy for folks to understand.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java Outdated
Comment thread src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java Outdated
Comment thread src/main/java/org/codehaus/groovy/control/customizers/ImportCustomizer.java Outdated
Comment thread src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
Comment thread subprojects/groovy-jmx/build.gradle
@paulk-asert paulk-asert force-pushed the groovy11896 branch 2 times, most recently from 554434e to 7791c1a Compare April 16, 2026 20:30
paulk-asert and others added 2 commits April 17, 2026 06:43
This better accommodates single-type > type-on-demand > module-import precedence of Java.
See [JLS 6.4.1](https://docs.oracle.com/javase/specs/jls/se25/html/jls-6.html#jls-6.4)

This also lays a little bit of groundwork for
https://issues.apache.org/jira/browse/GROOVY-11916 if/when we action
that.
@paulk-asert
Copy link
Copy Markdown
Contributor Author

@jonnybot0 With regard to your second branch, maybe deeper JPMS support might be a Groovy 7 thing. On the one hand getting a few needed pieces in place couldn't hurt, but on the other there will possibly be numerous pieces and thinking about all of them together has its benefits.

@jonnybot0
Copy link
Copy Markdown
Contributor

jonnybot0 commented Apr 17, 2026

I wonder whether we should take this opportunity (via another issue) to give an error for the multiple static import ambiguity case too. It would be breaking but I imagine easy for folks to understand.

That does seem like a good idea for Groovy 6. We could PR that separately.

I'd say you've answered all my questions at this point.

@paulk-asert paulk-asert merged commit 75d9302 into apache:master Apr 19, 2026
23 checks passed
@paulk-asert paulk-asert deleted the groovy11896 branch April 19, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants