You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/04/19 12:56:59 UTC
(groovy) branch master updated (cf8535cd22 -> 75d9302fcd)
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
from cf8535cd22 GROOVY-11932: Allow theme switching light/dark/system in groovyConsole (allow custom themes)
new 1024ef73b7 GROOVY-11896: Support module import declarations
new 75d9302fcd GROOVY-11896: Adjust Module import resolution
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
src/antlr/GroovyLexer.g4 | 1 +
src/antlr/GroovyParser.g4 | 2 +
.../apache/groovy/parser/antlr4/AstBuilder.java | 61 +++++++++
.../java/org/codehaus/groovy/ast/ModuleNode.java | 18 +++
.../groovy/control/ModuleImportHelper.java | 145 +++++++++++++++++++++
.../codehaus/groovy/control/ResolveVisitor.java | 25 ++++
.../control/customizers/ImportCustomizer.java | 45 ++++++-
.../builder/ImportCustomizerFactory.java | 5 +
src/spec/doc/core-program-structure.adoc | 35 +++++
src/spec/test/PackageTest.groovy | 44 +++++++
src/test/groovy/groovy/ImportTest.groovy | 143 ++++++++++++++++++++
.../customizers/ImportCustomizerTest.groovy | 17 +++
.../CompilerCustomizationBuilderTest.groovy | 24 ++++
.../apache/groovy/groovysh/jline/GroovyEngine.java | 18 ++-
.../groovysh/commands/ImportCommand.properties | 2 +-
.../groovy-groovysh/src/spec/doc/groovysh.adoc | 9 ++
.../groovy/groovysh/commands/ImportTest.groovy | 4 +-
subprojects/groovy-jmx/build.gradle | 5 +
.../groovy/groovy/jmx/ImportModuleJmxTest.groovy} | 30 ++---
19 files changed, 612 insertions(+), 21 deletions(-)
create mode 100644 src/main/java/org/codehaus/groovy/control/ModuleImportHelper.java
copy subprojects/{groovy-astbuilder/src/test/groovy/groovy/bugs/Groovy5025Bug.groovy => groovy-jmx/src/test/groovy/groovy/jmx/ImportModuleJmxTest.groovy} (57%)
(groovy) 01/02: GROOVY-11896: Support module import declarations
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 1024ef73b7208ed132c6688a0bb850aa9feb4ad0
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Apr 2 23:27:00 2026 +1000
GROOVY-11896: Support module import declarations
---
src/antlr/GroovyLexer.g4 | 1 +
src/antlr/GroovyParser.g4 | 2 +
.../apache/groovy/parser/antlr4/AstBuilder.java | 58 +++++++++
.../groovy/control/ModuleImportHelper.java | 145 +++++++++++++++++++++
.../control/customizers/ImportCustomizer.java | 45 ++++++-
.../builder/ImportCustomizerFactory.java | 5 +
src/spec/doc/core-program-structure.adoc | 35 +++++
src/spec/test/PackageTest.groovy | 44 +++++++
src/test/groovy/groovy/ImportTest.groovy | 109 ++++++++++++++++
.../customizers/ImportCustomizerTest.groovy | 17 +++
.../CompilerCustomizationBuilderTest.groovy | 24 ++++
.../apache/groovy/groovysh/jline/GroovyEngine.java | 18 ++-
.../groovysh/commands/ImportCommand.properties | 2 +-
.../groovy-groovysh/src/spec/doc/groovysh.adoc | 9 ++
.../groovy/groovysh/commands/ImportTest.groovy | 4 +-
subprojects/groovy-jmx/build.gradle | 5 +
.../groovy/groovy/jmx/ImportModuleJmxTest.groovy} | 34 ++---
17 files changed, 535 insertions(+), 22 deletions(-)
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index f1d69c5c95..803afd8bc3 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -457,6 +457,7 @@ INT : 'int';
fragment
LONG : 'long';
+MODULE : 'module';
NATIVE : 'native';
NEW : 'new';
NON_SEALED : 'non-sealed';
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 7eaa0cae10..8501b7658e 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -119,6 +119,7 @@ packageDeclaration
importDeclaration
: annotationsOpt IMPORT STATIC? qualifiedName (DOT MUL | AS alias=identifier)?
+ | annotationsOpt IMPORT MODULE qualifiedName
;
@@ -1243,6 +1244,7 @@ identifier
| AWAIT
| DEFER
| IN
+ | MODULE
| PERMITS
| RECORD
| SEALED
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 3e5cedcfce..69c6b011d5 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -124,6 +124,7 @@ import org.codehaus.groovy.ast.tools.ClosureUtils;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.ModuleImportHelper;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.transform.AsyncTransformHelper;
@@ -140,8 +141,10 @@ import java.lang.annotation.Annotation;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
@@ -149,6 +152,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -338,6 +342,11 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) {
List<AnnotationNode> annotations = this.visitAnnotationsOpt(ctx.annotationsOpt());
+ if (asBoolean(ctx.MODULE())) {
+ String moduleName = this.visitQualifiedName(ctx.qualifiedName());
+ return expandModuleImport(moduleName, annotations, ctx);
+ }
+
boolean hasStatic = asBoolean(ctx.STATIC());
boolean hasStar = asBoolean(ctx.MUL());
boolean hasAlias = asBoolean(ctx.alias);
@@ -388,6 +397,55 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return configureAST(importNode, ctx);
}
+ /**
+ * Expands {@code import module java.base} into star imports for all
+ * packages exported (unqualified) by the named module and, recursively,
+ * by any modules it {@code requires transitive} (per JEP 476).
+ * 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) {
+ var finder = ModuleImportHelper.moduleFinder(sourceUnit);
+ List<String> packageNames;
+ try {
+ packageNames = ModuleImportHelper.resolveModulePackages(moduleName, finder);
+ } catch (IllegalArgumentException e) {
+ throw createParsingFailedException(e.getMessage(), ctx);
+ }
+ 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;
+ 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 covered by existing imports
+ lastImport = last(moduleNode.getStarImports());
+ }
+ return configureAST(lastImport, ctx);
+ }
+
private static AnnotationNode makeAnnotationNode(final Class<? extends Annotation> type) {
AnnotationNode node = new AnnotationNode(ClassHelper.make(type));
// TODO: source offsets
diff --git a/src/main/java/org/codehaus/groovy/control/ModuleImportHelper.java b/src/main/java/org/codehaus/groovy/control/ModuleImportHelper.java
new file mode 100644
index 0000000000..3ec21ddefd
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/control/ModuleImportHelper.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.control;
+
+import java.lang.module.FindException;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Shared utilities for expanding JPMS module imports into package-level
+ * star imports. Used by both the parser ({@code AstBuilder}) and the
+ * {@link org.codehaus.groovy.control.customizers.ImportCustomizer}.
+ *
+ * @since 6.0.0
+ */
+public final class ModuleImportHelper {
+
+ private ModuleImportHelper() { }
+
+ /**
+ * Builds a {@link ModuleFinder} that combines the system module finder
+ * with one scanning the compilation classpath for modular JARs.
+ *
+ * @param source the source unit providing classpath and classloader
+ * @return a composite module finder
+ */
+ public static ModuleFinder moduleFinder(final SourceUnit source) {
+ return ModuleFinder.compose(ModuleFinder.ofSystem(), classpathModuleFinder(source));
+ }
+
+ /**
+ * Collects the exported package names from the given module and,
+ * recursively, from any modules it {@code requires transitive}.
+ * This implements the transitive readability semantics specified
+ * by JEP 476.
+ *
+ * @param moduleRef the module to inspect
+ * @param finder the finder used to resolve transitive dependencies
+ * @param packageNames accumulator for discovered package names
+ * @param visited set of already-visited module names (to avoid cycles)
+ */
+ public static void collectModuleExports(final ModuleReference moduleRef, final ModuleFinder finder,
+ final List<String> packageNames, final Set<String> visited) {
+ ModuleDescriptor descriptor = moduleRef.descriptor();
+ if (!visited.add(descriptor.name())) return;
+ // Automatic modules have no exports — use packages() instead
+ if (descriptor.isAutomatic()) {
+ packageNames.addAll(descriptor.packages());
+ } else {
+ descriptor.exports().stream()
+ .filter(e -> !e.isQualified())
+ .map(ModuleDescriptor.Exports::source)
+ .forEach(packageNames::add);
+ }
+ // Recursively process transitive dependencies (JEP 476)
+ for (ModuleDescriptor.Requires req : descriptor.requires()) {
+ if (req.modifiers().contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE)) {
+ finder.find(req.name()).ifPresent(ref ->
+ collectModuleExports(ref, finder, packageNames, visited));
+ }
+ }
+ }
+
+ /**
+ * Resolves a module by name and collects its exported packages
+ * (including transitive dependencies).
+ *
+ * @param moduleName the JPMS module name
+ * @param finder the module finder to use
+ * @return the list of exported package names
+ * @throws IllegalArgumentException if the module is not found
+ */
+ public static List<String> resolveModulePackages(final String moduleName, final ModuleFinder finder) {
+ ModuleReference moduleRef = finder.find(moduleName).orElse(null);
+ if (moduleRef == null) {
+ throw new IllegalArgumentException("Unknown module: " + moduleName);
+ }
+ List<String> packageNames = new ArrayList<>();
+ collectModuleExports(moduleRef, finder, packageNames, new HashSet<>());
+ return packageNames;
+ }
+
+ /**
+ * Builds a {@link 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 static ModuleFinder classpathModuleFinder(final SourceUnit source) {
+ Set<Path> paths = new LinkedHashSet<>();
+ for (String entry : source.getConfiguration().getClasspath()) {
+ Path p = Path.of(entry);
+ if (Files.exists(p)) {
+ paths.add(p);
+ }
+ }
+ ClassLoader cl = source.getClassLoader();
+ while (cl != null) {
+ if (cl instanceof URLClassLoader) {
+ for (URL url : ((URLClassLoader) cl).getURLs()) {
+ try {
+ Path p = Path.of(url.toURI());
+ if (Files.exists(p)) {
+ paths.add(p);
+ }
+ } catch (URISyntaxException ignore) {
+ }
+ }
+ }
+ cl = cl.getParent();
+ }
+ try {
+ return ModuleFinder.of(paths.toArray(Path[]::new));
+ } catch (FindException e) {
+ return ModuleFinder.of();
+ }
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/control/customizers/ImportCustomizer.java b/src/main/java/org/codehaus/groovy/control/customizers/ImportCustomizer.java
index 971c397d6c..c4b71cec56 100644
--- a/src/main/java/org/codehaus/groovy/control/customizers/ImportCustomizer.java
+++ b/src/main/java/org/codehaus/groovy/control/customizers/ImportCustomizer.java
@@ -20,13 +20,21 @@ package org.codehaus.groovy.control.customizers;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.ModuleImportHelper;
+import org.codehaus.groovy.control.ResolveVisitor;
import org.codehaus.groovy.control.SourceUnit;
+import java.lang.module.ModuleFinder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
/**
* This compilation customizer allows adding various types of imports to the compilation unit. Supports adding:
@@ -35,6 +43,7 @@ import java.util.List;
* <li>star imports via {@link #addStarImports(String...)}</li>
* <li>static imports via {@link #addStaticImport(String, String)} or {@link #addStaticImport(String, String, String)}</li>
* <li>static star imports via {@link #addStaticStars(String...)}</li>
+ * <li>module imports via {@link #addModuleImports(String...)}</li>
* </ul>
*
* @since 1.8.0
@@ -68,6 +77,24 @@ public class ImportCustomizer extends CompilationCustomizer {
case star:
ast.addStarImport(anImport.star);
break;
+ case moduleImport:
+ expandModuleImport(source, ast, anImport.star);
+ break;
+ }
+ }
+ }
+
+ private static void expandModuleImport(final SourceUnit source, final ModuleNode ast, final String moduleName) {
+ ModuleFinder finder = ModuleImportHelper.moduleFinder(source);
+ List<String> packageNames = ModuleImportHelper.resolveModulePackages(moduleName, finder);
+ Set<String> skip = new HashSet<>(Arrays.asList(ResolveVisitor.DEFAULT_IMPORTS));
+ ast.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
+ ast.getModuleStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
+ for (String pkg : packageNames) {
+ String packageName = pkg + ".";
+ if (!skip.contains(packageName)) {
+ ast.addModuleStarImport(packageName, Collections.emptyList());
+ skip.add(packageName);
}
}
}
@@ -108,6 +135,21 @@ public class ImportCustomizer extends CompilationCustomizer {
return this;
}
+ /**
+ * Adds module imports. Each module name (e.g. {@code "java.sql"}) is expanded
+ * at compilation time into star imports for all packages exported by that module,
+ * including packages from transitively required modules (per JEP 476).
+ *
+ * @param moduleNames the JPMS module names to import
+ * @since 6.0.0
+ */
+ public ImportCustomizer addModuleImports(final String... moduleNames) {
+ for (String moduleName : moduleNames) {
+ imports.add(new Import(ImportType.moduleImport, moduleName));
+ }
+ return this;
+ }
+
//
private void addImport(final String className) {
@@ -164,6 +206,7 @@ public class ImportCustomizer extends CompilationCustomizer {
regular,
staticImport,
staticStar,
- star
+ star,
+ moduleImport
}
}
diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java
index 77778efbb9..27a99a5b95 100644
--- a/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java
+++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java
@@ -36,6 +36,7 @@ import java.util.Map;
* <li><i>staticStar</i> for "static star" imports</li>
* <li><i>alias</i> for imports with alias</li>
* <li><i>staticMember</i> for static imports of individual members</li>
+ * <li><i>module</i> for module imports (JEP 476)</li>
* </ul>
*
* For example:
@@ -130,6 +131,10 @@ public class ImportCustomizerFactory extends AbstractFactory {
customizer.addStaticImport(alias, name, field);
}
+ protected void module(String... moduleNames) {
+ customizer.addModuleImports(moduleNames);
+ }
+
}
}
diff --git a/src/spec/doc/core-program-structure.adoc b/src/spec/doc/core-program-structure.adoc
index 0b0704787a..a7e052d65e 100644
--- a/src/spec/doc/core-program-structure.adoc
+++ b/src/spec/doc/core-program-structure.adoc
@@ -164,6 +164,41 @@ For example we can import `java.sql.Date` as `SQLDate` and use it in the same fi
include::../test/PackageTest.groovy[tags=alias_import,indent=0]
----
+=== Module import
+
+Groovy supports Java's module import declarations (https://openjdk.org/jeps/476[JEP 476], finalized in Java 25).
+A module import imports all public types from all packages exported by the named module with a single statement:
+
+[source,groovy]
+----
+include::../test/PackageTest.groovy[tags=module_import,indent=0]
+----
+
+This is equivalent to writing a star import (`import pkg.*`) for every package the module exports.
+
+Module imports also include packages from any modules that the named module `requires transitive`.
+For example, `java.sql` transitively requires `java.xml`, `java.logging`, and `java.transaction.xa`:
+
+[source,groovy]
+----
+include::../test/PackageTest.groovy[tags=module_import_transitive,indent=0]
+----
+
+Module imports work with system modules (JDK modules like `java.base`, `java.sql`, `java.desktop`),
+JPMS modules from modular JARs on the classpath (those containing `module-info.class`),
+and automatic modules (JARs with an `Automatic-Module-Name` manifest entry).
+
+Explicit single-type imports take priority over module-expanded imports, which allows you to
+resolve any ambiguity by adding a specific import:
+
+[source,groovy]
+----
+include::../test/PackageTest.groovy[tags=module_import_with_explicit,indent=0]
+----
+
+NOTE: The `module` keyword is context-sensitive — it can still be used as a variable, method, or class name.
+Wildcard (`import module java.base.*`) and alias (`import module java.base as jb`) forms are not supported.
+
=== Namespace conflicts
Similar to Java, it is an error in Groovy to specify multiple imports with the same name but different types:
diff --git a/src/spec/test/PackageTest.groovy b/src/spec/test/PackageTest.groovy
index b0bbbfbf2c..5389759bce 100644
--- a/src/spec/test/PackageTest.groovy
+++ b/src/spec/test/PackageTest.groovy
@@ -162,4 +162,48 @@ final class PackageTest {
// end::alias_import[]
'''
}
+
+ @Test
+ void testModuleImport() {
+ assertScript '''
+ // tag::module_import[]
+ import module java.sql
+
+ assert Connection.name == 'java.sql.Connection'
+ assert DriverManager.name == 'java.sql.DriverManager'
+ // end::module_import[]
+ '''
+ }
+
+ @Test
+ void testModuleImportTransitive() {
+ assertScript '''
+ // tag::module_import_transitive[]
+ import module java.sql
+
+ // Direct exports of java.sql
+ assert Connection.name == 'java.sql.Connection'
+
+ // From java.xml (requires transitive)
+ assert Source.name == 'javax.xml.transform.Source'
+
+ // From java.logging (requires transitive)
+ assert Logger.name == 'java.util.logging.Logger'
+ // end::module_import_transitive[]
+ '''
+ }
+
+ @Test
+ void testModuleImportWithExplicit() {
+ assertScript '''
+ // tag::module_import_with_explicit[]
+ import module java.desktop
+ import java.util.List
+
+ // The explicit single-type import wins over the
+ // module-expanded java.awt.List
+ assert List.name == 'java.util.List'
+ // end::module_import_with_explicit[]
+ '''
+ }
}
diff --git a/src/test/groovy/groovy/ImportTest.groovy b/src/test/groovy/groovy/ImportTest.groovy
index 152115df49..8d07b5c03b 100644
--- a/src/test/groovy/groovy/ImportTest.groovy
+++ b/src/test/groovy/groovy/ImportTest.groovy
@@ -163,4 +163,113 @@ 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 == 'java.sql.Connection'
+ assert DriverManager.name == 'java.sql.DriverManager'
+ '''
+ }
+
+ @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'
+ '''
+ }
+
+ // GROOVY-11896: JEP 476 specifies that `import module M` should also import
+ // packages from modules that M requires transitively.
+ @Test
+ void testImportModuleTransitiveDependencies() {
+ // java.sql requires transitive java.xml, java.logging, java.transaction.xa
+ assertScript '''
+ import module java.sql
+
+ // Direct exports of java.sql
+ assert Connection.name == 'java.sql.Connection'
+
+ // From java.xml (requires transitive): javax.xml.transform
+ assert Source.name == 'javax.xml.transform.Source'
+
+ // From java.logging (requires transitive): java.util.logging
+ assert Logger.name == 'java.util.logging.Logger'
+
+ // From java.transaction.xa (requires transitive): javax.transaction.xa
+ assert XAResource.name == 'javax.transaction.xa.XAResource'
+ '''
+ }
+
+ @Test
+ void testImportModuleUnknown() {
+ shouldFail '''
+ import module no.such.module
+ '''
+ }
+
+ @Test
+ void testImportModuleStarNotAllowed() {
+ shouldFail '''
+ import module java.base.*
+ '''
+ }
+
+ @Test
+ void testImportModuleAliasNotAllowed() {
+ shouldFail '''
+ import module java.base as jb
+ '''
+ }
+
+ @Test
+ void testModuleAsIdentifier() {
+ // 'module' can still be used as a variable name
+ assertScript '''
+ def module = 'hello'
+ assert module.toUpperCase() == 'HELLO'
+ '''
+ }
}
diff --git a/src/test/groovy/org/codehaus/groovy/control/customizers/ImportCustomizerTest.groovy b/src/test/groovy/org/codehaus/groovy/control/customizers/ImportCustomizerTest.groovy
index 5c9559f0cd..896afeb620 100644
--- a/src/test/groovy/org/codehaus/groovy/control/customizers/ImportCustomizerTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/control/customizers/ImportCustomizerTest.groovy
@@ -177,6 +177,23 @@ final class ImportCustomizerTest {
''')
}
+ @Test
+ void testAddModuleImport() {
+ importCustomizer.addModuleImports('java.sql')
+ def shell = new GroovyShell(configuration)
+ shell.evaluate('assert Connection.name == "java.sql.Connection"')
+ // no exception means success
+ }
+
+ @Test
+ void testAddModuleImportTransitive() {
+ importCustomizer.addModuleImports('java.sql')
+ def shell = new GroovyShell(configuration)
+ // java.sql requires transitive java.logging
+ shell.evaluate('assert Logger.name == "java.util.logging.Logger"')
+ // no exception means success
+ }
+
//--------------------------------------------------------------------------
protected static class Inner {}
diff --git a/src/test/groovy/org/codehaus/groovy/control/customizers/builder/CompilerCustomizationBuilderTest.groovy b/src/test/groovy/org/codehaus/groovy/control/customizers/builder/CompilerCustomizationBuilderTest.groovy
index cdd1ce7bfd..c3f83e1a34 100644
--- a/src/test/groovy/org/codehaus/groovy/control/customizers/builder/CompilerCustomizationBuilderTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/control/customizers/builder/CompilerCustomizationBuilderTest.groovy
@@ -180,6 +180,30 @@ class CompilerCustomizationBuilderTest {
assert cz.imports[0].alias == 'pi'
}
+ @Test
+ void testImportBuilderModule() {
+ def builder = new CompilerCustomizationBuilder()
+ def cz = builder.imports {
+ module 'java.sql'
+ }
+ assert cz instanceof ImportCustomizer
+ assert cz.imports.size() == 1
+ assert cz.imports[0].type.toString() == 'moduleImport'
+ assert cz.imports[0].star == 'java.sql'
+ }
+
+ @Test
+ void testImportBuilderModuleIntegration() {
+ def config = new CompilerConfiguration()
+ CompilerCustomizationBuilder.withConfig(config) {
+ imports {
+ module 'java.sql'
+ }
+ }
+ def shell = new GroovyShell(config)
+ shell.evaluate('assert Connection.name == "java.sql.Connection"')
+ }
+
@Test
void testSourceAwareCustomizerBuilder() {
def builder = new CompilerCustomizationBuilder()
diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyEngine.java b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyEngine.java
index 8b9c8ca1c5..544cf299f4 100644
--- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyEngine.java
+++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyEngine.java
@@ -407,7 +407,21 @@ public class GroovyEngine implements ScriptEngine {
@Override
public Object execute(String statement) throws Exception {
Object out = null;
- if (statement.matches("import\\s+(static\\s+)?(([^;\\s])+)(?:\\s+as\\s+(" + BASE_REGEX_VAR + "))?\\s*(;)?")) {
+ if (statement.matches("import\\s+module\\s+([^;\\s]+)\\s*(;)?")) {
+ String[] p = statement.split("\\s+");
+ String moduleName = p[2].replaceAll(";", "");
+ executeStatement(shell, snippets, EnumSet.of(SnippetType.IMPORT), statement);
+ imports.put("module " + moduleName, addSnippet(SnippetType.IMPORT, statement));
+ // Populate the completion cache with exported packages from the module
+ try {
+ var finder = java.lang.module.ModuleFinder.ofSystem();
+ for (String pkg : org.codehaus.groovy.control.ModuleImportHelper.resolveModulePackages(moduleName, finder)) {
+ addToNameClass(pkg + ".*");
+ }
+ } catch (Exception ignore) {
+ // module resolution may not be available
+ }
+ } else if (statement.matches("import\\s+(static\\s+)?(([^;\\s])+)(?:\\s+as\\s+(" + BASE_REGEX_VAR + "))?\\s*(;)?")) {
String[] p = statement.split("\\s+");
int classIdx = 1;
boolean isStatic = p[1].equals("static");
@@ -681,7 +695,7 @@ public class GroovyEngine implements ScriptEngine {
public void removeImport(String name) {
Integer gone = imports.remove(name);
if (gone != null) snippets.set(gone, null);
- if (name.endsWith(".*")) {
+ if (name.endsWith(".*") || name.startsWith("module ")) {
refreshNameClass();
} else {
classLoader.purgeClassCache(name + "(\\$.*)?");
diff --git a/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/ImportCommand.properties b/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/ImportCommand.properties
index 53e4e344d7..42449e6e97 100644
--- a/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/ImportCommand.properties
+++ b/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/ImportCommand.properties
@@ -17,6 +17,6 @@
# under the License.
#
command.description=Import a class into the namespace
-command.usage=[static] <classname> [as <alias>]
+command.usage=[static] <classname> [as <alias>] | module <moduleName>
command.help=Add a custom import which will be included for all shell evaluations.
diff --git a/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc b/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc
index 20d6590da8..b222f49047 100644
--- a/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc
+++ b/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc
@@ -638,6 +638,15 @@ Add a custom import which will be included for all shell evaluations.
groovy> import java.util.concurrent.BlockingDeque
--------------
+Module imports (JEP 476) are also supported, which import all exported packages from a named module:
+
+[source,jshell]
+--------------
+groovy> import module java.sql
+groovy> Connection.name
+===> java.sql.Connection
+--------------
+
This command can be given at any time to add new imports.
Completion is available and prompts a level at a time using the package structure of all known classes.
diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy
index 241fe3d142..7fdbf8bf52 100644
--- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy
+++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy
@@ -35,7 +35,7 @@ class ImportTest extends SystemTestSupport {
@Test
void testModuleImport() {
-// system.execute('import module java.sql')
-// system.execute('assert Connection.name == "java.sql.Connection"')
+ system.execute('import module java.sql')
+ system.execute('assert Connection.name == "java.sql.Connection"')
}
}
diff --git a/subprojects/groovy-jmx/build.gradle b/subprojects/groovy-jmx/build.gradle
index 67d220ae56..35feabec6c 100644
--- a/subprojects/groovy-jmx/build.gradle
+++ b/subprojects/groovy-jmx/build.gradle
@@ -32,3 +32,8 @@ dependencies {
testRuntimeOnly projects.groovyGrapeIvy
testRuntimeOnly projects.groovySwing
}
+
+tasks.named('test') {
+ dependsOn 'jarjar'
+ systemProperty 'groovy.jmx.jar', tasks.named('jarjar').flatMap { it.outputFile }.get().asFile.path
+}
diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/ImportModuleJmxTest.groovy
similarity index 52%
copy from subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy
copy to subprojects/groovy-jmx/src/test/groovy/groovy/jmx/ImportModuleJmxTest.groovy
index 241fe3d142..72887226f9 100644
--- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ImportTest.groovy
+++ b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/ImportModuleJmxTest.groovy
@@ -16,26 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.groovy.groovysh.commands
+package groovy.jmx
import org.junit.jupiter.api.Test
-/**
- * Tests for the {@code /imports} command.
- */
-class ImportTest extends SystemTestSupport {
- @Test
- void testImport() {
- system.execute('/imports')
- assert !printer.output.join().contains('import java.awt.TextField')
- system.execute('import java.awt.TextField')
- system.execute('/imports')
- assert printer.output.join().contains('import java.awt.TextField')
- }
+import static groovy.test.GroovyAssert.assertScript
+
+final class ImportModuleJmxTest {
@Test
- void testModuleImport() {
-// system.execute('import module java.sql')
-// system.execute('assert Connection.name == "java.sql.Connection"')
+ void testImportModuleAutomatic() {
+ // groovy-jmx is an automatic module (Automatic-Module-Name in MANIFEST.MF);
+ // JAR path is passed as a system property from build.gradle
+ def jmxJar = System.getProperty('groovy.jmx.jar')
+ assert jmxJar, 'groovy.jmx.jar system property not set'
+ def config = new org.codehaus.groovy.control.CompilerConfiguration()
+ config.classpathList = [jmxJar]
+ def loader = new GroovyClassLoader(getClass().classLoader, config)
+ def shell = new GroovyShell(loader)
+ shell.evaluate '''
+ import module org.apache.groovy.jmx
+
+ assert GroovyMBean.name == 'groovy.jmx.GroovyMBean'
+ '''
}
}
(groovy) 02/02: GROOVY-11896: Adjust Module import resolution
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 75d9302fcdd498b7dc6ba6f9b98948b7dc06a603
Author: Jonny Carter <jc...@adaptavist.com>
AuthorDate: Wed Apr 15 14:25:35 2026 -0500
GROOVY-11896: Adjust Module import resolution
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.
---
.../apache/groovy/parser/antlr4/AstBuilder.java | 25 +++++++++-------
.../java/org/codehaus/groovy/ast/ModuleNode.java | 18 ++++++++++++
.../codehaus/groovy/control/ResolveVisitor.java | 25 ++++++++++++++++
src/test/groovy/groovy/ImportTest.groovy | 34 ++++++++++++++++++++++
4 files changed, 91 insertions(+), 11 deletions(-)
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 69c6b011d5..ab506a3483 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -410,14 +410,13 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
* {@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>
+ * As per JLS 6.4.1, explicit single-type imports and type-on-demand (star)
+ * imports take priority over module-expanded imports. Ambiguous class names
+ * from multiple module imports produce a compile-time error (JLS 7.5.5).
+ * <p>
+ * Known difference from Java: Groovy's star imports (including module-expanded
+ * ones) can resolve package-private types, whereas Java only exposes public
+ * types from exported packages (see GROOVY-11916).
*/
private ImportNode expandModuleImport(final String moduleName, final List<AnnotationNode> annotations, final ImportDeclarationContext ctx) {
var finder = ModuleImportHelper.moduleFinder(sourceUnit);
@@ -430,18 +429,22 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
Set<String> skip = new HashSet<>(Arrays.asList(
org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS));
moduleNode.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
+ moduleNode.getModuleStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
ImportNode lastImport = null;
for (String pkg : packageNames) {
String packageName = pkg + DOT_STR;
if (!skip.contains(packageName)) {
- moduleNode.addStarImport(packageName, annotations);
- lastImport = last(moduleNode.getStarImports());
+ // Separate list so resolution can apply JLS 6.4.1 shadowing:
+ // a user-written `import foo.*` beats a module-expanded `foo.*`.
+ moduleNode.addModuleStarImport(packageName, annotations);
+ lastImport = last(moduleNode.getModuleStarImports());
skip.add(packageName);
}
}
if (lastImport == null) {
// All exported packages were already covered by existing imports
- lastImport = last(moduleNode.getStarImports());
+ List<ImportNode> existing = moduleNode.getModuleStarImports();
+ lastImport = !existing.isEmpty() ? last(existing) : last(moduleNode.getStarImports());
}
return configureAST(lastImport, ctx);
}
diff --git a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
index 6c0760ba69..23ddc43910 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
@@ -70,6 +70,7 @@ public class ModuleNode extends ASTNode {
private final List<MethodNode> methods = new ArrayList<>();
private final List<ImportNode> imports = new ArrayList<>();
private final List<ImportNode> starImports = new ArrayList<>();
+ private final List<ImportNode> moduleStarImports = new ArrayList<>();
private final Map<String, ImportNode> staticImports = new LinkedHashMap<>();
private final Map<String, ImportNode> staticStarImports = new LinkedHashMap<>();
private CompileUnit unit;
@@ -183,6 +184,23 @@ public class ModuleNode extends ASTNode {
storeLastAddedImportNode(importNode);
}
+ /**
+ * @return star imports that originated from {@code import module M} declarations.
+ * Separate from {@link #getStarImports()} so that resolution can apply
+ * JLS 6.4.1 shadowing (type-import-on-demand shadows module-import).
+ */
+ public List<ImportNode> getModuleStarImports() {
+ return moduleStarImports;
+ }
+
+ public void addModuleStarImport(final String packageName, final List<AnnotationNode> annotations) {
+ ImportNode importNode = new ImportNode(packageName);
+ importNode.addAnnotations(annotations);
+ moduleStarImports.add(importNode);
+
+ storeLastAddedImportNode(importNode);
+ }
+
public void addStaticImport(final ClassNode type, final String memberName, final String simpleName) {
addStaticImport(type, memberName, simpleName, Collections.emptyList());
}
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index ec4335ceaf..c5904834c8 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -768,6 +768,31 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
}
}
}
+ // Module-expanded star imports — lowest precedence per JLS 6.4.1.
+ // Only consulted if nothing above resolved the type, and ambiguity
+ // between two module-expanded packages is a compile-time error
+ // (matching Java's behavior per JLS 7.5.5 Example 7.5.5-3).
+ ClassNode moduleMatch = null;
+ String moduleMatchPkg = null;
+ for (ImportNode importNode : module.getModuleStarImports()) {
+ ClassNode tmp = new ConstructedClassWithPackage(importNode.getPackageName(), name);
+ if (resolve(tmp, false, false, true)) {
+ ClassNode resolved = tmp.redirect();
+ if (moduleMatch != null && !moduleMatch.getName().equals(resolved.getName())) {
+ addError("reference to " + name + " is ambiguous, both "
+ + moduleMatch.getName() + " (from " + moduleMatchPkg + ")"
+ + " and " + resolved.getName()
+ + " (from " + importNode.getPackageName() + ") match", type);
+ return true;
+ }
+ moduleMatch = resolved;
+ moduleMatchPkg = importNode.getPackageName();
+ }
+ }
+ if (moduleMatch != null) {
+ type.setRedirect(moduleMatch);
+ return true;
+ }
}
return false;
diff --git a/src/test/groovy/groovy/ImportTest.groovy b/src/test/groovy/groovy/ImportTest.groovy
index 8d07b5c03b..171318c719 100644
--- a/src/test/groovy/groovy/ImportTest.groovy
+++ b/src/test/groovy/groovy/ImportTest.groovy
@@ -272,4 +272,38 @@ final class ImportTest {
assert module.toUpperCase() == 'HELLO'
'''
}
+
+ // GROOVY-11896: JLS 7.5.5 Example 7.5.5-3 — when a module import exposes
+ // two packages that both contain a public type with the same simple name,
+ // using that simple name must be a compile-time error, not a silent pick.
+ // Here, java.desktop exports both javax.swing.text (with Element interface)
+ // and javax.swing.text.html.parser (with Element class).
+ @Test
+ void testImportModuleAmbiguousSimpleName() {
+ def err = shouldFail '''
+ import module java.desktop
+ Element e = null
+ '''
+ assert err.message.contains('ambiguous'),
+ "expected ambiguity error, got: ${err.message}"
+ }
+
+ // GROOVY-11896: JLS 6.4.1 — a type-import-on-demand declaration shadows
+ // types imported by a single-module-import declaration. The `import module`
+ // is written FIRST so that without proper tier separation, the module-
+ // expanded java.awt.* would win by list ordering. With correct shadowing,
+ // the user's `import java.util.*` wins regardless of source order, and
+ // List resolves to java.util.List rather than java.awt.List.
+ @Test
+ void testImportModuleShadowedByStarImport() {
+ assertScript '''
+ import module java.desktop
+ assert List.name == 'java.awt.List'
+ '''
+ assertScript '''
+ import module java.desktop
+ import java.util.*
+ assert List.name == 'java.util.List'
+ '''
+ }
}