[ 
https://issues.apache.org/jira/browse/GROOVY-11896?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18071653#comment-18071653
 ] 

ASF GitHub Bot commented on GROOVY-11896:
-----------------------------------------

Copilot commented on code in PR #2429:
URL: https://github.com/apache/groovy/pull/2429#discussion_r3044013466


##########
src/antlr/GroovyParser.g4:
##########
@@ -118,7 +118,7 @@ packageDeclaration
     ;
 
 importDeclaration
-    :   annotationsOpt IMPORT STATIC? qualifiedName (DOT MUL | AS 
alias=identifier)?
+    :   annotationsOpt IMPORT (STATIC | MODULE)? qualifiedName (DOT MUL | AS 
alias=identifier)?
     ;
 
 

Review Comment:
   `importDeclaration` allows `import module <name>.*` and `import module 
<name> as alias`, but the implementation in `AstBuilder#visitImportDeclaration` 
ignores `MUL`/`alias` when `MODULE` is present. This makes those forms either 
silently miscompiled or ambiguously specified. Consider splitting the grammar 
into a dedicated `import module` alternative that only permits a qualified 
module name (no star / no alias) and rejecting unsupported combinations at 
parse time.
   ```suggestion
       :   importModuleDeclaration
       |   annotationsOpt IMPORT STATIC? qualifiedName (DOT MUL | AS 
alias=identifier)?
       ;
   
   importModuleDeclaration
       :   annotationsOpt IMPORT MODULE qualifiedName
       ;
   ```



##########
src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java:
##########
@@ -338,9 +349,15 @@ public ImportNode visitImportDeclaration(final 
ImportDeclarationContext ctx) {
         List<AnnotationNode> annotations = 
this.visitAnnotationsOpt(ctx.annotationsOpt());
 
         boolean hasStatic = asBoolean(ctx.STATIC());
+        boolean hasModule = asBoolean(ctx.MODULE());
         boolean hasStar   = asBoolean(ctx.MUL());
         boolean hasAlias  = asBoolean(ctx.alias);
 
+        if (hasModule) {
+            String moduleName = this.visitQualifiedName(ctx.qualifiedName());
+            return expandModuleImport(moduleName, annotations, ctx);
+        }
+

Review Comment:
   When `MODULE` is present, `hasStar` / `hasAlias` are currently ignored. If 
those forms are meant to be illegal (recommended for alignment with the 
documented module-import syntax), detect them here and throw a parsing error; 
otherwise, implement consistent semantics for `import module ... .*` / `as ...`.



##########
src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java:
##########
@@ -387,6 +404,88 @@ public ImportNode visitImportDeclaration(final 
ImportDeclarationContext ctx) {
         return configureAST(importNode, ctx);
     }
 
+    /**
+     * Expands {@code import module java.base} into star imports for all
+     * packages exported (unqualified) by the named module.
+     * Packages already covered by Groovy's default imports or existing
+     * star imports are skipped to avoid redundant resolution work.
+     * <p>
+     * Modules are located from system modules (JDK) first, then from
+     * modular JARs on the compilation classpath. Automatic modules
+     * (JARs with an {@code Automatic-Module-Name} manifest entry but no
+     * {@code module-info.class}) are supported — all packages in the JAR
+     * are imported since automatic modules have no explicit exports.
+     * <p>
+     * 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>
+     */
+    private ImportNode expandModuleImport(final String moduleName, final 
List<AnnotationNode> annotations, final ImportDeclarationContext ctx) {
+        ModuleFinder finder = ModuleFinder.compose(
+                ModuleFinder.ofSystem(), classpathModuleFinder());
+        ModuleReference moduleRef = finder.find(moduleName).orElse(null);
+        if (moduleRef == null) {
+            throw createParsingFailedException("Unknown module: " + 
moduleName, ctx);
+        }
+        ModuleDescriptor descriptor = moduleRef.descriptor();
+        Set<String> skip = new HashSet<>(Arrays.asList(
+                org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS));
+        
moduleNode.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
+        ImportNode lastImport = null;
+        // Automatic modules have no exports — use packages() instead
+        Collection<String> packageNames = descriptor.isAutomatic()
+                ? descriptor.packages()
+                : descriptor.exports().stream()
+                    .filter(e -> !e.isQualified())
+                    .map(ModuleDescriptor.Exports::source)
+                    .collect(Collectors.toList());
+        for (String pkg : packageNames) {
+            String packageName = pkg + DOT_STR;
+            if (!skip.contains(packageName)) {
+                moduleNode.addStarImport(packageName, annotations);
+                lastImport = last(moduleNode.getStarImports());
+                skip.add(packageName);
+            }
+        }
+        if (lastImport == null) {
+            // All exported packages were already imported — add a no-op 
marker so AST is valid
+            
moduleNode.addStarImport(org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS[0],
 annotations);
+            lastImport = last(moduleNode.getStarImports());
+        }
+        return configureAST(lastImport, ctx);
+    }
+
+    /**
+     * Builds a {@link java.lang.module.ModuleFinder} that scans the 
compilation
+     * classpath for modular JARs (those containing {@code module-info.class}).
+     * Collects paths from both the compiler configuration classpath and any
+     * URLClassLoaders in the classloader hierarchy.
+     */
+    private ModuleFinder classpathModuleFinder() {
+        Set<Path> paths = new LinkedHashSet<>();
+        for (String entry : sourceUnit.getConfiguration().getClasspath()) {
+            paths.add(Path.of(entry));
+        }
+        ClassLoader cl = sourceUnit.getClassLoader();
+        while (cl != null) {
+            if (cl instanceof URLClassLoader) {
+                for (URL url : ((URLClassLoader) cl).getURLs()) {
+                    try {
+                        paths.add(Path.of(url.toURI()));
+                    } catch (URISyntaxException ignore) {
+                    }
+                }
+            }
+            cl = cl.getParent();
+        }
+        return ModuleFinder.of(paths.toArray(Path[]::new));
+    }

Review Comment:
   `classpathModuleFinder()` builds a `ModuleFinder` from every 
compiler/classloader classpath entry without validating existence/type. 
`ModuleFinder.of(...)` can throw `IllegalArgumentException` for 
non-existent/unsupported paths, which would make `import module ...` fail 
unexpectedly depending on classpath contents. Consider filtering to existing 
`Path`s (jar/jmod/dir) and guarding `ModuleFinder.of(...)` with error handling 
that reports a clear `ParsingFailedException`.



##########
src/test/groovy/groovy/ImportTest.groovy:
##########
@@ -163,4 +163,95 @@ final class ImportTest {
             Entry entry = [foo:'bar'].entrySet().first()
         '''
     }
+
+    // module imports
+
+    @Test
+    void testImportModuleJavaBase() {
+        assertScript '''
+            import module java.base
+
+            // java.util
+            List list = [1, 2, 3]
+            Map map = [a: 1]
+
+            // java.io
+            File f = new File('.')
+
+            // java.net
+            URI uri = new URI('http://example.com')
+
+            // java.time
+            LocalDate date = LocalDate.now()
+
+            // java.util.concurrent
+            CountDownLatch latch = new CountDownLatch(1)
+
+            assert list.size() == 3
+        '''
+    }
+
+    @Test
+    void testImportModuleJavaSql() {
+        assertScript '''
+            import module java.sql
+
+            // java.sql package
+            assert Connection.name.size() == 19
+            assert DriverManager.name.size() == 22

Review Comment:
   These assertions rely on the *length* of the class name string, which is 
brittle and obscures intent. Prefer asserting the fully-qualified name (e.g., 
`Connection.name == 'java.sql.Connection'`) so the test fails only when 
resolution is wrong, not when unrelated naming/formatting changes occur.
   ```suggestion
               assert Connection.name == 'java.sql.Connection'
               assert DriverManager.name == 'java.sql.DriverManager'
   ```



##########
src/test/groovy/groovy/ImportTest.groovy:
##########
@@ -163,4 +163,95 @@ final class ImportTest {
             Entry entry = [foo:'bar'].entrySet().first()
         '''
     }
+
+    // module imports
+
+    @Test
+    void testImportModuleJavaBase() {
+        assertScript '''
+            import module java.base
+
+            // java.util
+            List list = [1, 2, 3]
+            Map map = [a: 1]
+
+            // java.io
+            File f = new File('.')
+
+            // java.net
+            URI uri = new URI('http://example.com')
+
+            // java.time
+            LocalDate date = LocalDate.now()
+
+            // java.util.concurrent
+            CountDownLatch latch = new CountDownLatch(1)
+
+            assert list.size() == 3
+        '''
+    }
+
+    @Test
+    void testImportModuleJavaSql() {
+        assertScript '''
+            import module java.sql
+
+            // java.sql package
+            assert Connection.name.size() == 19
+            assert DriverManager.name.size() == 22
+        '''
+    }
+
+    @Test
+    void testImportModuleFromClasspath() {
+        // Find JUnit JAR from the test classloader and add it to the compiler 
classpath
+        def junitUrl = Test.class.protectionDomain.codeSource.location
+        def config = new org.codehaus.groovy.control.CompilerConfiguration()
+        config.classpathList = [new File(junitUrl.toURI()).path]
+        def loader = new GroovyClassLoader(getClass().classLoader, config)
+        def shell = new GroovyShell(loader)
+        shell.evaluate '''
+            import module org.junit.jupiter.api
+
+            // org.junit.jupiter.api package
+            assert Test.name == 'org.junit.jupiter.api.Test'
+            assert Assertions.name == 'org.junit.jupiter.api.Assertions'
+            assert DisplayName.name == 'org.junit.jupiter.api.DisplayName'
+            assert DisabledIf.name == 
'org.junit.jupiter.api.condition.DisabledIf'
+        '''
+    }
+
+    @Test
+    void testImportModuleAutomatic() {
+        // groovy-jmx is an automatic module (Automatic-Module-Name in 
MANIFEST.MF)
+        def jmxJar = new 
File('subprojects/groovy-jmx/build/libs').listFiles()?.find {
+            it.name.startsWith('groovy-jmx-') && it.name.endsWith('.jar') && 
!it.name.contains('-raw')
+        }
+        assert jmxJar?.exists(), 'groovy-jmx JAR not found; run ./gradlew 
:groovy-jmx:jar first'
+        def config = new org.codehaus.groovy.control.CompilerConfiguration()
+        config.classpathList = [jmxJar.path]
+        def loader = new GroovyClassLoader(getClass().classLoader, config)
+        def shell = new GroovyShell(loader)

Review Comment:
   This test depends on a locally-built `subprojects/groovy-jmx/build/libs` jar 
and instructs running a Gradle task manually. That makes the test suite 
order-/environment-dependent and likely to fail on clean/CI checkouts. Consider 
wiring the jar creation as part of the test build (declared task dependency) or 
locating the needed artifact from the test runtime classpath instead of the 
filesystem.



##########
src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java:
##########
@@ -387,6 +404,88 @@ public ImportNode visitImportDeclaration(final 
ImportDeclarationContext ctx) {
         return configureAST(importNode, ctx);
     }
 
+    /**
+     * Expands {@code import module java.base} into star imports for all
+     * packages exported (unqualified) by the named module.
+     * Packages already covered by Groovy's default imports or existing
+     * star imports are skipped to avoid redundant resolution work.
+     * <p>
+     * Modules are located from system modules (JDK) first, then from
+     * modular JARs on the compilation classpath. Automatic modules
+     * (JARs with an {@code Automatic-Module-Name} manifest entry but no
+     * {@code module-info.class}) are supported — all packages in the JAR
+     * are imported since automatic modules have no explicit exports.
+     * <p>
+     * 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>
+     */
+    private ImportNode expandModuleImport(final String moduleName, final 
List<AnnotationNode> annotations, final ImportDeclarationContext ctx) {
+        ModuleFinder finder = ModuleFinder.compose(
+                ModuleFinder.ofSystem(), classpathModuleFinder());
+        ModuleReference moduleRef = finder.find(moduleName).orElse(null);
+        if (moduleRef == null) {
+            throw createParsingFailedException("Unknown module: " + 
moduleName, ctx);
+        }
+        ModuleDescriptor descriptor = moduleRef.descriptor();
+        Set<String> skip = new HashSet<>(Arrays.asList(
+                org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS));
+        
moduleNode.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
+        ImportNode lastImport = null;
+        // Automatic modules have no exports — use packages() instead
+        Collection<String> packageNames = descriptor.isAutomatic()
+                ? descriptor.packages()
+                : descriptor.exports().stream()
+                    .filter(e -> !e.isQualified())
+                    .map(ModuleDescriptor.Exports::source)
+                    .collect(Collectors.toList());
+        for (String pkg : packageNames) {
+            String packageName = pkg + DOT_STR;
+            if (!skip.contains(packageName)) {
+                moduleNode.addStarImport(packageName, annotations);
+                lastImport = last(moduleNode.getStarImports());
+                skip.add(packageName);
+            }
+        }
+        if (lastImport == null) {
+            // All exported packages were already imported — add a no-op 
marker so AST is valid
+            
moduleNode.addStarImport(org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS[0],
 annotations);
+            lastImport = last(moduleNode.getStarImports());

Review Comment:
   The "no-op marker" branch adds an extra star import (`DEFAULT_IMPORTS[0]`) 
even when the module import expands to no new packages. This is not a true 
no-op: it mutates `moduleNode.getStarImports()` ordering and can affect 
ambiguous-type resolution (Groovy resolves ambiguities by last match when FQNs 
differ). Prefer returning `null` (since imports are applied via side effects) 
or returning an `ImportNode` without adding any additional star import.
   ```suggestion
               // All exported packages were already imported — no AST node 
needs to be added
               return null;
   ```





> Support module import declarations
> ----------------------------------
>
>                 Key: GROOVY-11896
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11896
>             Project: Groovy
>          Issue Type: New Feature
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> In Java, _module import declarations_ allow you to import all exported 
> packages of a module with a single statement. It was introduced as a preview 
> feature in Java 23 ([JEP 476|https://openjdk.org/jeps/476]) and finalized in 
> Java 25 ([JEP 511|https://openjdk.org/jeps/511]).
> The syntax is as follows:
> {code:java}
> import module java.base;    // Java
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to