Skip to content

Commit 67a0aa6

Browse files
committed
GROOVY-11896: Support module import declarations
1 parent 2a7d1e9 commit 67a0aa6

4 files changed

Lines changed: 107 additions & 1 deletion

File tree

src/antlr/GroovyLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ LONG : 'long';
456456

457457
NATIVE : 'native';
458458
NEW : 'new';
459+
MODULE : 'module';
459460
NON_SEALED : 'non-sealed';
460461
PACKAGE : 'package';
461462
PERMITS : 'permits';

src/antlr/GroovyParser.g4

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ packageDeclaration
118118
;
119119

120120
importDeclaration
121-
: annotationsOpt IMPORT STATIC? qualifiedName (DOT MUL | AS alias=identifier)?
121+
: annotationsOpt IMPORT (STATIC | MODULE)? qualifiedName (DOT MUL | AS alias=identifier)?
122122
;
123123

124124

@@ -1230,6 +1230,7 @@ identifier
12301230
| CapitalizedIdentifier
12311231
| AS
12321232
| IN
1233+
| MODULE
12331234
| PERMITS
12341235
| RECORD
12351236
| SEALED

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,15 @@ public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) {
338338
List<AnnotationNode> annotations = this.visitAnnotationsOpt(ctx.annotationsOpt());
339339

340340
boolean hasStatic = asBoolean(ctx.STATIC());
341+
boolean hasModule = asBoolean(ctx.MODULE());
341342
boolean hasStar = asBoolean(ctx.MUL());
342343
boolean hasAlias = asBoolean(ctx.alias);
343344

345+
if (hasModule) {
346+
String moduleName = this.visitQualifiedName(ctx.qualifiedName());
347+
return expandModuleImport(moduleName, annotations, ctx);
348+
}
349+
344350
ImportNode importNode;
345351

346352
if (hasStatic) {
@@ -387,6 +393,50 @@ public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) {
387393
return configureAST(importNode, ctx);
388394
}
389395

396+
/**
397+
* Expands {@code import module java.base} into star imports for all
398+
* packages exported (unqualified) by the named module.
399+
* Packages already covered by Groovy's default imports or existing
400+
* star imports are skipped to avoid redundant resolution work.
401+
* <p>
402+
* Known differences from Java's module import behavior:
403+
* <ul>
404+
* <li>Ambiguous class names from multiple module imports silently resolve
405+
* to the last match, consistent with Groovy's existing star import
406+
* semantics. Java reports a compile-time error for such ambiguities.</li>
407+
* <li>Explicit single-type imports take priority over module-expanded
408+
* star imports (same as Java).</li>
409+
* </ul>
410+
*/
411+
private ImportNode expandModuleImport(final String moduleName, final List<AnnotationNode> annotations, final ImportDeclarationContext ctx) {
412+
java.lang.module.ModuleFinder finder = java.lang.module.ModuleFinder.ofSystem();
413+
java.lang.module.ModuleReference moduleRef = finder.find(moduleName).orElse(null);
414+
if (moduleRef == null) {
415+
throw createParsingFailedException("Unknown module: " + moduleName, ctx);
416+
}
417+
java.lang.module.ModuleDescriptor descriptor = moduleRef.descriptor();
418+
java.util.Set<String> skip = new java.util.HashSet<>(java.util.Arrays.asList(
419+
org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS));
420+
moduleNode.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add);
421+
ImportNode lastImport = null;
422+
for (java.lang.module.ModuleDescriptor.Exports export : descriptor.exports()) {
423+
if (!export.isQualified()) {
424+
String packageName = export.source() + DOT_STR;
425+
if (!skip.contains(packageName)) {
426+
moduleNode.addStarImport(packageName, annotations);
427+
lastImport = last(moduleNode.getStarImports());
428+
skip.add(packageName);
429+
}
430+
}
431+
}
432+
if (lastImport == null) {
433+
// All exported packages were already imported — add a no-op marker so AST is valid
434+
moduleNode.addStarImport(org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS[0], annotations);
435+
lastImport = last(moduleNode.getStarImports());
436+
}
437+
return configureAST(lastImport, ctx);
438+
}
439+
390440
private static AnnotationNode makeAnnotationNode(final Class<? extends Annotation> type) {
391441
AnnotationNode node = new AnnotationNode(ClassHelper.make(type));
392442
// TODO: source offsets

src/test/groovy/groovy/ImportTest.groovy

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,58 @@ final class ImportTest {
163163
Entry entry = [foo:'bar'].entrySet().first()
164164
'''
165165
}
166+
167+
// module imports
168+
169+
@Test
170+
void testImportModuleJavaBase() {
171+
assertScript '''
172+
import module java.base
173+
174+
// java.util
175+
List list = [1, 2, 3]
176+
Map map = [a: 1]
177+
178+
// java.io
179+
File f = new File('.')
180+
181+
// java.net
182+
URI uri = new URI('http://example.com')
183+
184+
// java.time
185+
LocalDate date = LocalDate.now()
186+
187+
// java.util.concurrent
188+
CountDownLatch latch = new CountDownLatch(1)
189+
190+
assert list.size() == 3
191+
'''
192+
}
193+
194+
@Test
195+
void testImportModuleJavaSql() {
196+
assertScript '''
197+
import module java.sql
198+
199+
// java.sql package
200+
assert Connection != null
201+
assert DriverManager != null
202+
'''
203+
}
204+
205+
@Test
206+
void testImportModuleUnknown() {
207+
shouldFail '''
208+
import module no.such.module
209+
'''
210+
}
211+
212+
@Test
213+
void testModuleAsIdentifier() {
214+
// 'module' can still be used as a variable name
215+
assertScript '''
216+
def module = 'hello'
217+
assert module.toUpperCase() == 'HELLO'
218+
'''
219+
}
166220
}

0 commit comments

Comments
 (0)