|
136 | 136 | import java.io.BufferedReader; |
137 | 137 | import java.io.IOException; |
138 | 138 | import java.lang.annotation.Annotation; |
| 139 | +import java.lang.module.FindException; |
| 140 | +import java.lang.module.ModuleDescriptor; |
| 141 | +import java.lang.module.ModuleFinder; |
| 142 | +import java.lang.module.ModuleReference; |
| 143 | +import java.net.URISyntaxException; |
| 144 | +import java.net.URL; |
| 145 | +import java.net.URLClassLoader; |
| 146 | +import java.nio.file.Files; |
| 147 | +import java.nio.file.Path; |
139 | 148 | import java.util.ArrayDeque; |
140 | 149 | import java.util.ArrayList; |
141 | 150 | import java.util.Arrays; |
| 151 | +import java.util.Collection; |
142 | 152 | import java.util.Collections; |
143 | 153 | import java.util.Deque; |
| 154 | +import java.util.HashSet; |
144 | 155 | import java.util.Iterator; |
145 | 156 | import java.util.LinkedHashMap; |
| 157 | +import java.util.LinkedHashSet; |
146 | 158 | import java.util.LinkedList; |
147 | 159 | import java.util.List; |
148 | 160 | import java.util.Map; |
149 | 161 | import java.util.Objects; |
150 | 162 | import java.util.Optional; |
| 163 | +import java.util.Set; |
151 | 164 | import java.util.function.Function; |
152 | 165 | import java.util.stream.Collectors; |
153 | 166 |
|
@@ -337,6 +350,11 @@ public PackageNode visitPackageDeclaration(final PackageDeclarationContext ctx) |
337 | 350 | public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) { |
338 | 351 | List<AnnotationNode> annotations = this.visitAnnotationsOpt(ctx.annotationsOpt()); |
339 | 352 |
|
| 353 | + if (asBoolean(ctx.MODULE())) { |
| 354 | + String moduleName = this.visitQualifiedName(ctx.qualifiedName()); |
| 355 | + return expandModuleImport(moduleName, annotations, ctx); |
| 356 | + } |
| 357 | + |
340 | 358 | boolean hasStatic = asBoolean(ctx.STATIC()); |
341 | 359 | boolean hasStar = asBoolean(ctx.MUL()); |
342 | 360 | boolean hasAlias = asBoolean(ctx.alias); |
@@ -387,6 +405,97 @@ public ImportNode visitImportDeclaration(final ImportDeclarationContext ctx) { |
387 | 405 | return configureAST(importNode, ctx); |
388 | 406 | } |
389 | 407 |
|
| 408 | + /** |
| 409 | + * Expands {@code import module java.base} into star imports for all |
| 410 | + * packages exported (unqualified) by the named module. |
| 411 | + * Packages already covered by Groovy's default imports or existing |
| 412 | + * star imports are skipped to avoid redundant resolution work. |
| 413 | + * <p> |
| 414 | + * Modules are located from system modules (JDK) first, then from |
| 415 | + * modular JARs on the compilation classpath. Automatic modules |
| 416 | + * (JARs with an {@code Automatic-Module-Name} manifest entry but no |
| 417 | + * {@code module-info.class}) are supported — all packages in the JAR |
| 418 | + * are imported since automatic modules have no explicit exports. |
| 419 | + * <p> |
| 420 | + * Known differences from Java's module import behavior: |
| 421 | + * <ul> |
| 422 | + * <li>Ambiguous class names from multiple module imports silently resolve |
| 423 | + * to the last match, consistent with Groovy's existing star import |
| 424 | + * semantics. Java reports a compile-time error for such ambiguities.</li> |
| 425 | + * <li>Explicit single-type imports take priority over module-expanded |
| 426 | + * star imports (same as Java).</li> |
| 427 | + * </ul> |
| 428 | + */ |
| 429 | + private ImportNode expandModuleImport(final String moduleName, final List<AnnotationNode> annotations, final ImportDeclarationContext ctx) { |
| 430 | + ModuleFinder finder = ModuleFinder.compose( |
| 431 | + ModuleFinder.ofSystem(), classpathModuleFinder()); |
| 432 | + ModuleReference moduleRef = finder.find(moduleName).orElse(null); |
| 433 | + if (moduleRef == null) { |
| 434 | + throw createParsingFailedException("Unknown module: " + moduleName, ctx); |
| 435 | + } |
| 436 | + ModuleDescriptor descriptor = moduleRef.descriptor(); |
| 437 | + Set<String> skip = new HashSet<>(Arrays.asList( |
| 438 | + org.codehaus.groovy.control.ResolveVisitor.DEFAULT_IMPORTS)); |
| 439 | + moduleNode.getStarImports().stream().map(ImportNode::getPackageName).forEach(skip::add); |
| 440 | + ImportNode lastImport = null; |
| 441 | + // Automatic modules have no exports — use packages() instead |
| 442 | + Collection<String> packageNames = descriptor.isAutomatic() |
| 443 | + ? descriptor.packages() |
| 444 | + : descriptor.exports().stream() |
| 445 | + .filter(e -> !e.isQualified()) |
| 446 | + .map(ModuleDescriptor.Exports::source) |
| 447 | + .collect(Collectors.toList()); |
| 448 | + for (String pkg : packageNames) { |
| 449 | + String packageName = pkg + DOT_STR; |
| 450 | + if (!skip.contains(packageName)) { |
| 451 | + moduleNode.addStarImport(packageName, annotations); |
| 452 | + lastImport = last(moduleNode.getStarImports()); |
| 453 | + skip.add(packageName); |
| 454 | + } |
| 455 | + } |
| 456 | + if (lastImport == null) { |
| 457 | + // All exported packages were already covered by existing imports |
| 458 | + lastImport = last(moduleNode.getStarImports()); |
| 459 | + } |
| 460 | + return configureAST(lastImport, ctx); |
| 461 | + } |
| 462 | + |
| 463 | + /** |
| 464 | + * Builds a {@link java.lang.module.ModuleFinder} that scans the compilation |
| 465 | + * classpath for modular JARs (those containing {@code module-info.class}). |
| 466 | + * Collects paths from both the compiler configuration classpath and any |
| 467 | + * URLClassLoaders in the classloader hierarchy. |
| 468 | + */ |
| 469 | + private ModuleFinder classpathModuleFinder() { |
| 470 | + Set<Path> paths = new LinkedHashSet<>(); |
| 471 | + for (String entry : sourceUnit.getConfiguration().getClasspath()) { |
| 472 | + Path p = Path.of(entry); |
| 473 | + if (java.nio.file.Files.exists(p)) { |
| 474 | + paths.add(p); |
| 475 | + } |
| 476 | + } |
| 477 | + ClassLoader cl = sourceUnit.getClassLoader(); |
| 478 | + while (cl != null) { |
| 479 | + if (cl instanceof URLClassLoader) { |
| 480 | + for (URL url : ((URLClassLoader) cl).getURLs()) { |
| 481 | + try { |
| 482 | + Path p = Path.of(url.toURI()); |
| 483 | + if (java.nio.file.Files.exists(p)) { |
| 484 | + paths.add(p); |
| 485 | + } |
| 486 | + } catch (URISyntaxException ignore) { |
| 487 | + } |
| 488 | + } |
| 489 | + } |
| 490 | + cl = cl.getParent(); |
| 491 | + } |
| 492 | + try { |
| 493 | + return ModuleFinder.of(paths.toArray(Path[]::new)); |
| 494 | + } catch (java.lang.module.FindException e) { |
| 495 | + return ModuleFinder.of(); |
| 496 | + } |
| 497 | + } |
| 498 | + |
390 | 499 | private static AnnotationNode makeAnnotationNode(final Class<? extends Annotation> type) { |
391 | 500 | AnnotationNode node = new AnnotationNode(ClassHelper.make(type)); |
392 | 501 | // TODO: source offsets |
|
0 commit comments