Skip to content

Commit 1024ef7

Browse files
committed
GROOVY-11896: Support module import declarations
1 parent cf8535c commit 1024ef7

17 files changed

Lines changed: 560 additions & 6 deletions

File tree

src/antlr/GroovyLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ INT : 'int';
457457
fragment
458458
LONG : 'long';
459459

460+
MODULE : 'module';
460461
NATIVE : 'native';
461462
NEW : 'new';
462463
NON_SEALED : 'non-sealed';

src/antlr/GroovyParser.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ packageDeclaration
119119

120120
importDeclaration
121121
: annotationsOpt IMPORT STATIC? qualifiedName (DOT MUL | AS alias=identifier)?
122+
| annotationsOpt IMPORT MODULE qualifiedName
122123
;
123124

124125

@@ -1243,6 +1244,7 @@ identifier
12431244
| AWAIT
12441245
| DEFER
12451246
| IN
1247+
| MODULE
12461248
| PERMITS
12471249
| RECORD
12481250
| SEALED

src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
import org.codehaus.groovy.classgen.Verifier;
125125
import org.codehaus.groovy.control.CompilationFailedException;
126126
import org.codehaus.groovy.control.CompilePhase;
127+
import org.codehaus.groovy.control.ModuleImportHelper;
127128
import org.codehaus.groovy.control.SourceUnit;
128129
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
129130
import org.codehaus.groovy.transform.AsyncTransformHelper;
@@ -140,15 +141,18 @@
140141
import java.util.ArrayDeque;
141142
import java.util.ArrayList;
142143
import java.util.Arrays;
144+
import java.util.Collection;
143145
import java.util.Collections;
144146
import java.util.Deque;
147+
import java.util.HashSet;
145148
import java.util.Iterator;
146149
import java.util.LinkedHashMap;
147150
import java.util.LinkedList;
148151
import java.util.List;
149152
import java.util.Map;
150153
import java.util.Objects;
151154
import java.util.Optional;
155+
import java.util.Set;
152156
import java.util.function.Function;
153157
import java.util.stream.Collectors;
154158

@@ -338,6 +342,11 @@ public PackageNode visitPackageDeclaration(final PackageDeclarationContext ctx)
338342
public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) {
339343
List<AnnotationNode> annotations = this.visitAnnotationsOpt(ctx.annotationsOpt());
340344

345+
if (asBoolean(ctx.MODULE())) {
346+
String moduleName = this.visitQualifiedName(ctx.qualifiedName());
347+
return expandModuleImport(moduleName, annotations, ctx);
348+
}
349+
341350
boolean hasStatic = asBoolean(ctx.STATIC());
342351
boolean hasStar = asBoolean(ctx.MUL());
343352
boolean hasAlias = asBoolean(ctx.alias);
@@ -388,6 +397,55 @@ public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) {
388397
return configureAST(importNode, ctx);
389398
}
390399

400+
/**
401+
* Expands {@code import module java.base} into star imports for all
402+
* packages exported (unqualified) by the named module and, recursively,
403+
* by any modules it {@code requires transitive} (per JEP 476).
404+
* Packages already covered by Groovy's default imports or existing
405+
* star imports are skipped to avoid redundant resolution work.
406+
* <p>
407+
* Modules are located from system modules (JDK) first, then from
408+
* modular JARs on the compilation classpath. Automatic modules
409+
* (JARs with an {@code Automatic-Module-Name} manifest entry but no
410+
* {@code module-info.class}) are supported — all packages in the JAR
411+
* are imported since automatic modules have no explicit exports.
412+
* <p>
413+
* Known differences from Java's module import behavior:
414+
* <ul>
415+
* <li>Ambiguous class names from multiple module imports silently resolve
416+
* to the last match, consistent with Groovy's existing star import
417+
* semantics. Java reports a compile-time error for such ambiguities.</li>
418+
* <li>Explicit single-type imports take priority over module-expanded
419+
* star imports (same as Java).</li>
420+
* </ul>
421+
*/
422+
private ImportNode expandModuleImport(final String moduleName, final List<AnnotationNode> annotations, final ImportDeclarationContext ctx) {
423+
var finder = ModuleImportHelper.moduleFinder(sourceUnit);
424+
List<String> packageNames;
425+
try {
426+
packageNames = ModuleImportHelper.resolveModulePackages(moduleName, finder);
427+
} catch (IllegalArgumentException e) {
428+
throw createParsingFailedException(e.getMessage(), ctx);
429+
}
430+
Set<String> skip = new HashSet<>(Arrays.asList(
431+
org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS));
432+
moduleNode.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
433+
ImportNode lastImport = null;
434+
for (String pkg : packageNames) {
435+
String packageName = pkg + DOT_STR;
436+
if (!skip.contains(packageName)) {
437+
moduleNode.addStarImport(packageName, annotations);
438+
lastImport = last(moduleNode.getStarImports());
439+
skip.add(packageName);
440+
}
441+
}
442+
if (lastImport == null) {
443+
// All exported packages were already covered by existing imports
444+
lastImport = last(moduleNode.getStarImports());
445+
}
446+
return configureAST(lastImport, ctx);
447+
}
448+
391449
private static AnnotationNode makeAnnotationNode(final Class<? extends Annotation> type) {
392450
AnnotationNode node = new AnnotationNode(ClassHelper.make(type));
393451
// TODO: source offsets
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.control;
20+
21+
import java.lang.module.FindException;
22+
import java.lang.module.ModuleDescriptor;
23+
import java.lang.module.ModuleFinder;
24+
import java.lang.module.ModuleReference;
25+
import java.net.URISyntaxException;
26+
import java.net.URL;
27+
import java.net.URLClassLoader;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.util.ArrayList;
31+
import java.util.HashSet;
32+
import java.util.LinkedHashSet;
33+
import java.util.List;
34+
import java.util.Set;
35+
36+
/**
37+
* Shared utilities for expanding JPMS module imports into package-level
38+
* star imports. Used by both the parser ({@code AstBuilder}) and the
39+
* {@link org.codehaus.groovy.control.customizers.ImportCustomizer}.
40+
*
41+
* @since 6.0.0
42+
*/
43+
public final class ModuleImportHelper {
44+
45+
private ModuleImportHelper() { }
46+
47+
/**
48+
* Builds a {@link ModuleFinder} that combines the system module finder
49+
* with one scanning the compilation classpath for modular JARs.
50+
*
51+
* @param source the source unit providing classpath and classloader
52+
* @return a composite module finder
53+
*/
54+
public static ModuleFinder moduleFinder(final SourceUnit source) {
55+
return ModuleFinder.compose(ModuleFinder.ofSystem(), classpathModuleFinder(source));
56+
}
57+
58+
/**
59+
* Collects the exported package names from the given module and,
60+
* recursively, from any modules it {@code requires transitive}.
61+
* This implements the transitive readability semantics specified
62+
* by JEP 476.
63+
*
64+
* @param moduleRef the module to inspect
65+
* @param finder the finder used to resolve transitive dependencies
66+
* @param packageNames accumulator for discovered package names
67+
* @param visited set of already-visited module names (to avoid cycles)
68+
*/
69+
public static void collectModuleExports(final ModuleReference moduleRef, final ModuleFinder finder,
70+
final List<String> packageNames, final Set<String> visited) {
71+
ModuleDescriptor descriptor = moduleRef.descriptor();
72+
if (!visited.add(descriptor.name())) return;
73+
// Automatic modules have no exports — use packages() instead
74+
if (descriptor.isAutomatic()) {
75+
packageNames.addAll(descriptor.packages());
76+
} else {
77+
descriptor.exports().stream()
78+
.filter(e -> !e.isQualified())
79+
.map(ModuleDescriptor.Exports::source)
80+
.forEach(packageNames::add);
81+
}
82+
// Recursively process transitive dependencies (JEP 476)
83+
for (ModuleDescriptor.Requires req : descriptor.requires()) {
84+
if (req.modifiers().contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE)) {
85+
finder.find(req.name()).ifPresent(ref ->
86+
collectModuleExports(ref, finder, packageNames, visited));
87+
}
88+
}
89+
}
90+
91+
/**
92+
* Resolves a module by name and collects its exported packages
93+
* (including transitive dependencies).
94+
*
95+
* @param moduleName the JPMS module name
96+
* @param finder the module finder to use
97+
* @return the list of exported package names
98+
* @throws IllegalArgumentException if the module is not found
99+
*/
100+
public static List<String> resolveModulePackages(final String moduleName, final ModuleFinder finder) {
101+
ModuleReference moduleRef = finder.find(moduleName).orElse(null);
102+
if (moduleRef == null) {
103+
throw new IllegalArgumentException("Unknown module: " + moduleName);
104+
}
105+
List<String> packageNames = new ArrayList<>();
106+
collectModuleExports(moduleRef, finder, packageNames, new HashSet<>());
107+
return packageNames;
108+
}
109+
110+
/**
111+
* Builds a {@link ModuleFinder} that scans the compilation classpath
112+
* for modular JARs (those containing {@code module-info.class}).
113+
* Collects paths from both the compiler configuration classpath and
114+
* any URLClassLoaders in the classloader hierarchy.
115+
*/
116+
private static ModuleFinder classpathModuleFinder(final SourceUnit source) {
117+
Set<Path> paths = new LinkedHashSet<>();
118+
for (String entry : source.getConfiguration().getClasspath()) {
119+
Path p = Path.of(entry);
120+
if (Files.exists(p)) {
121+
paths.add(p);
122+
}
123+
}
124+
ClassLoader cl = source.getClassLoader();
125+
while (cl != null) {
126+
if (cl instanceof URLClassLoader) {
127+
for (URL url : ((URLClassLoader) cl).getURLs()) {
128+
try {
129+
Path p = Path.of(url.toURI());
130+
if (Files.exists(p)) {
131+
paths.add(p);
132+
}
133+
} catch (URISyntaxException ignore) {
134+
}
135+
}
136+
}
137+
cl = cl.getParent();
138+
}
139+
try {
140+
return ModuleFinder.of(paths.toArray(Path[]::new));
141+
} catch (FindException e) {
142+
return ModuleFinder.of();
143+
}
144+
}
145+
}

src/main/java/org/codehaus/groovy/control/customizers/ImportCustomizer.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,21 @@
2020

2121
import org.codehaus.groovy.ast.ClassHelper;
2222
import org.codehaus.groovy.ast.ClassNode;
23+
import org.codehaus.groovy.ast.ImportNode;
2324
import org.codehaus.groovy.ast.ModuleNode;
2425
import org.codehaus.groovy.classgen.GeneratorContext;
2526
import org.codehaus.groovy.control.CompilePhase;
27+
import org.codehaus.groovy.control.ModuleImportHelper;
28+
import org.codehaus.groovy.control.ResolveVisitor;
2629
import org.codehaus.groovy.control.SourceUnit;
2730

31+
import java.lang.module.ModuleFinder;
32+
import java.util.Arrays;
33+
import java.util.Collections;
34+
import java.util.HashSet;
2835
import java.util.LinkedList;
2936
import java.util.List;
37+
import java.util.Set;
3038

3139
/**
3240
* This compilation customizer allows adding various types of imports to the compilation unit. Supports adding:
@@ -35,6 +43,7 @@
3543
* <li>star imports via {@link #addStarImports(String...)}</li>
3644
* <li>static imports via {@link #addStaticImport(String, String)} or {@link #addStaticImport(String, String, String)}</li>
3745
* <li>static star imports via {@link #addStaticStars(String...)}</li>
46+
* <li>module imports via {@link #addModuleImports(String...)}</li>
3847
* </ul>
3948
*
4049
* @since 1.8.0
@@ -68,6 +77,24 @@ public void call(final SourceUnit source, final GeneratorContext context, final
6877
case star:
6978
ast.addStarImport(anImport.star);
7079
break;
80+
case moduleImport:
81+
expandModuleImport(source, ast, anImport.star);
82+
break;
83+
}
84+
}
85+
}
86+
87+
private static void expandModuleImport(final SourceUnit source, final ModuleNode ast, final String moduleName) {
88+
ModuleFinder finder = ModuleImportHelper.moduleFinder(source);
89+
List<String> packageNames = ModuleImportHelper.resolveModulePackages(moduleName, finder);
90+
Set<String> skip = new HashSet<>(Arrays.asList(ResolveVisitor.DEFAULT_IMPORTS));
91+
ast.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
92+
ast.getModuleStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
93+
for (String pkg : packageNames) {
94+
String packageName = pkg + ".";
95+
if (!skip.contains(packageName)) {
96+
ast.addModuleStarImport(packageName, Collections.emptyList());
97+
skip.add(packageName);
7198
}
7299
}
73100
}
@@ -108,6 +135,21 @@ public ImportCustomizer addStaticStars(final String... classNames) {
108135
return this;
109136
}
110137

138+
/**
139+
* Adds module imports. Each module name (e.g. {@code "java.sql"}) is expanded
140+
* at compilation time into star imports for all packages exported by that module,
141+
* including packages from transitively required modules (per JEP 476).
142+
*
143+
* @param moduleNames the JPMS module names to import
144+
* @since 6.0.0
145+
*/
146+
public ImportCustomizer addModuleImports(final String... moduleNames) {
147+
for (String moduleName : moduleNames) {
148+
imports.add(new Import(ImportType.moduleImport, moduleName));
149+
}
150+
return this;
151+
}
152+
111153
//
112154

113155
private void addImport(final String className) {
@@ -164,6 +206,7 @@ private enum ImportType {
164206
regular,
165207
staticImport,
166208
staticStar,
167-
star
209+
star,
210+
moduleImport
168211
}
169212
}

src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* <li><i>staticStar</i> for "static star" imports</li>
3737
* <li><i>alias</i> for imports with alias</li>
3838
* <li><i>staticMember</i> for static imports of individual members</li>
39+
* <li><i>module</i> for module imports (JEP 476)</li>
3940
* </ul>
4041
*
4142
* For example:
@@ -130,6 +131,10 @@ protected void staticMember(String alias, String name, String field) {
130131
customizer.addStaticImport(alias, name, field);
131132
}
132133

134+
protected void module(String... moduleNames) {
135+
customizer.addModuleImports(moduleNames);
136+
}
137+
133138
}
134139

135140
}

0 commit comments

Comments
 (0)