changeset 3372:2c1838c6e03c

8140767: adding -XaddReads option to javac
author jlahoda
date Tue, 10 Nov 2015 14:19:14 -0800
parents 76aeac001089
children 7c66438d4d8a 1669922f641d
files src/jdk.compiler/share/classes/com/sun/tools/javac/code/Directive.java src/jdk.compiler/share/classes/com/sun/tools/javac/code/ModuleFinder.java src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Modules.java src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/ClassWriter.java src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties test/tools/javac/modules/AddReadsTest.java
diffstat 8 files changed, 453 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Directive.java	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Directive.java	Tue Nov 10 14:19:14 2015 -0800
@@ -55,7 +55,8 @@
     public enum RequiresFlag {
         PUBLIC(0x0020),
         SYNTHETIC(0x1000),
-        MANDATED(0x8000);
+        MANDATED(0x8000),
+        EXTRA(0x10000);
 
         // overkill? move to ClassWriter?
         public static int value(Set<RequiresFlag> s) {
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/ModuleFinder.java	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/ModuleFinder.java	Tue Nov 10 14:19:14 2015 -0800
@@ -370,7 +370,7 @@
                 msym.requires = requires.toList();
                 msym.uses = List.nil();
                 msym.directives = directives.toList();
-                msym.flags_field |= Flags.AUTOMATIC_MODULE;
+                msym.flags_field |= Flags.AUTOMATIC_MODULE | Flags.ACYCLIC;
             } catch (IOException ex) {
                 throw new IllegalStateException(ex);
             }
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java	Tue Nov 10 14:19:14 2015 -0800
@@ -25,17 +25,14 @@
 
 package com.sun.tools.javac.code;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import javax.lang.model.element.ElementVisitor;
 import javax.tools.JavaFileObject;
 
-import com.sun.tools.javac.code.Directive.ExportsDirective;
 import com.sun.tools.javac.code.Scope.WriteableScope;
 import com.sun.tools.javac.code.Symbol.ClassSymbol;
 import com.sun.tools.javac.code.Symbol.Completer;
@@ -448,9 +445,14 @@
         Source source = Source.instance(context);
         Options options = Options.instance(context);
         boolean noModules = options.isSet("noModules");
-        java_base = source.allowModules() && !noModules //moved up here to avoid bootstraping issues related to DocEnv
-                ? enterModule(names.java_base)
-                : noModule;
+        if (source.allowModules() && !noModules) {
+            java_base = enterModule(names.java_base);
+            //avoid completing java.base during the Symtab initialization
+            java_base.completer = Completer.NULL_COMPLETER;
+            java_base.visiblePackages = Collections.emptySet();
+        } else {
+            java_base = noModule;
+        }
 
         // Get the initial completer for ModuleSymbols from Modules
         moduleCompleter = Modules.instance(context).getCompleter();
@@ -557,6 +559,10 @@
                            List.<Type>nil(), methodClass),
             arrayClass);
         arrayClass.members().enter(arrayCloneMethod);
+
+        if (java_base != noModule)
+            java_base.completer = sym -> moduleCompleter.complete(sym); //bootstrap issues
+
     }
 
     /** Define a new class given its name and owner.
@@ -601,24 +607,50 @@
             return msym.unnamedPackage;
         }
 
-        if (msym != noModule && msym != java_base) { //avoid completing java_base too early
-            msym.complete();
+        if (msym == noModule) {
+            return enterPackage(msym, flatName);
+        }
 
-            for (PackageSymbol pack : msym.visiblePackages) {
-                if (pack.fullname == flatName) {
-                    return pack;
-                }
+        msym.complete();
+
+        for (PackageSymbol pack : msym.visiblePackages) {
+            if (pack.fullname == flatName) {
+                return pack;
+            }
+        }
+
+        PackageSymbol pack = getPackage(msym, flatName);
+
+        if (pack != null && pack.exists())
+            return pack;
+
+        boolean dependsOnUnnamed = msym.requires != null &&
+                                   msym.requires.stream()
+                                                .map(rd -> rd.module)
+                                                .anyMatch(mod -> mod == unnamedModule);
+
+        if (dependsOnUnnamed) {
+            //msyms depends on the unnamed module, for which we generally don't know
+            //the list of packages it "exports" ahead of time. So try to lookup the package in the
+            //current module, and in the unnamed module and see if it exists in one of them
+            PackageSymbol unnamedPack = getPackage(unnamedModule, flatName);
+
+            if (unnamedPack != null && unnamedPack.exists())
+                return unnamedPack;
+
+            pack = enterPackage(msym, flatName);
+            pack.complete();
+            if (pack.exists())
+                return pack;
+
+            unnamedPack = enterPackage(unnamedModule, flatName);
+            unnamedPack.complete();
+            if (unnamedPack.exists()) {
+                msym.visiblePackages.add(unnamedPack);
+                return unnamedPack;
             }
 
-            if ((msym.flags() & AUTOMATIC_MODULE) != 0) {
-                //automatic modules depend on the unname module, for which we generally don't know
-                //the list of packages it "exports" ahead of time. So try to lookup the package in the
-                //unnamed module, and use it if it exists:
-                PackageSymbol pack = enterPackage(unnamedModule, flatName);
-                pack.complete();
-                if (pack.exists())
-                    return pack;
-            }
+            return pack;
         }
 
         return enterPackage(msym, flatName);
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Modules.java	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Modules.java	Tue Nov 10 14:19:14 2015 -0800
@@ -28,6 +28,7 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -83,6 +84,8 @@
 import static com.sun.tools.javac.code.Flags.UNATTRIBUTED;
 import static com.sun.tools.javac.code.Kinds.Kind.MDL;
 import static com.sun.tools.javac.code.TypeTag.CLASS;
+import com.sun.tools.javac.tree.JCTree.JCDirective;
+import com.sun.tools.javac.tree.JCTree.Tag;
 import static com.sun.tools.javac.tree.JCTree.Tag.MODULEDEF;
 
 /**
@@ -112,6 +115,8 @@
 
     private final String addExportsOpt;
     private Map<ModuleSymbol, Set<ExportsDirective>> addExports;
+    private final String addReadsOpt;
+    private Map<ModuleSymbol, Set<RequiresDirective>> addReads;
 
     public static Modules instance(Context context) {
         Modules instance = context.get(Modules.class);
@@ -145,6 +150,7 @@
         jniWriter.multiModuleMode = multiModuleMode;
 
         addExportsOpt = options.get(Option.XADDEXPORTS);
+        addReadsOpt = options.get(Option.XADDREADS);
     }
 
     int depth = -1;
@@ -287,9 +293,11 @@
                     log.useSource(prev);
                 }
             }
-            syms.unnamedModule.completer = getUnnamedModuleCompleter();
-            syms.unnamedModule.sourceLocation = StandardLocation.SOURCE_PATH;
-            syms.unnamedModule.classLocation = StandardLocation.CLASS_PATH;
+            if (syms.unnamedModule.sourceLocation == null) {
+                syms.unnamedModule.completer = getUnnamedModuleCompleter();
+                syms.unnamedModule.sourceLocation = StandardLocation.SOURCE_PATH;
+                syms.unnamedModule.classLocation = StandardLocation.CLASS_PATH;
+            }
             defaultModule = syms.unnamedModule;
         } else {
             if (defaultModule == null) {
@@ -331,6 +339,12 @@
                 Assert.check(rootModules.isEmpty());
             }
 
+            if (defaultModule != syms.unnamedModule) {
+                syms.unnamedModule.completer = getUnnamedModuleCompleter();
+                syms.unnamedModule.sourceLocation = StandardLocation.SOURCE_PATH;
+                syms.unnamedModule.classLocation = StandardLocation.CLASS_PATH;
+            }
+
             for (JCCompilationUnit tree: trees) {
                 tree.modle = defaultModule;
             }
@@ -416,6 +430,7 @@
                 try {
                     tree.defs.head.accept(v);
                     completeModule(msym);
+                    checkCyclicDependencies((JCModuleDecl) tree.defs.head);
                 } finally {
                     log.useSource(prev);
                     msym.flags_field &= ~UNATTRIBUTED;
@@ -459,8 +474,6 @@
             ModuleSymbol msym = lookupModule(tree.moduleName);
             if (msym.kind != MDL) {
                 log.error(tree.moduleName.pos(), "module.not.found", msym);
-            } else if ((msym.flags_field & UNATTRIBUTED) != 0) {
-                log.error(tree.moduleName.pos(), "cyclic.requires", msym);
             } else if (allRequires.contains(msym)) {
                 log.error(tree.moduleName.pos(), "duplicate.requires", msym);
             } else {
@@ -567,6 +580,7 @@
                 l.head.accept(this);
         }
 
+        @SuppressWarnings("unchecked")
         public void visitModuleDef(JCModuleDecl tree) {
             msym.directives = List.nil();
             msym.provides = List.nil();
@@ -578,6 +592,8 @@
 
             if (msym.requires.nonEmpty() && msym.requires.head.flags.contains(RequiresFlag.MANDATED))
                 msym.directives = msym.directives.prepend(msym.requires.head);
+
+            msym.directives = msym.directives.appendList(List.from(addReads.getOrDefault(msym, Collections.emptySet())));
         }
 
         public void visitExports(JCExports tree) {
@@ -635,6 +651,11 @@
 
     private void completeModule(ModuleSymbol msym) {
         Assert.checkNonNull(msym.requires);
+
+        initAddReads();
+
+        msym.requires = msym.requires.appendList(List.from(addReads.getOrDefault(msym, Collections.emptySet())));
+
         Set<ModuleSymbol> readable = new HashSet<>();
         Set<ModuleSymbol> requiresPublic = new HashSet<>();
         if ((msym.flags() & Flags.AUTOMATIC_MODULE) == 0) {
@@ -657,10 +678,8 @@
             readable.addAll(s);
             requiresPublic.addAll(s);
 
-            //ensure unnamed modules are added (these are not requires public):
-            msym.requires.stream()
-                    .map(rd -> { rd.module.complete(); return rd.module;})
-                    .forEach(readable::add);
+            //ensure the unnamed module is added (it is not requires public):
+            readable.add(syms.unnamedModule);
         }
         requiresPublicCache.put(msym, requiresPublic);
         initVisiblePackages(msym, readable);
@@ -674,9 +693,7 @@
         Set<ModuleSymbol> requiresPublic = requiresPublicCache.get(msym);
 
         if (requiresPublic == null) {
-            Assert.check((msym.flags() & Flags.AUTOMATIC_MODULE) != 0, () -> "no entry in cache for " + msym);
-
-            //the module graph may contain cycles involving automatic modules
+            //the module graph may contain cycles involving automatic modules or -XaddReads edges
             requiresPublic = new HashSet<>();
 
             Set<ModuleSymbol> seen = new HashSet<>();
@@ -689,10 +706,18 @@
                     continue;
                 requiresPublic.add(current);
                 current.complete();
-                Assert.checkNonNull(current.requires, () -> current + ".requires == null; " + msym);
-                for (RequiresDirective rd : current.requires) {
-                    if (rd.isPublic())
-                        todo = todo.prepend(rd.module);
+                Iterable<? extends RequiresDirective> requires;
+                if (current != syms.unnamedModule) {
+                    Assert.checkNonNull(current.requires, () -> current + ".requires == null; " + msym);
+                    requires = current.requires;
+                    for (RequiresDirective rd : requires) {
+                        if (rd.isPublic())
+                            todo = todo.prepend(rd.module);
+                    }
+                } else {
+                    for (ModuleSymbol mod : syms.getAllModules()) {
+                        todo = todo.prepend(mod);
+                    }
                 }
             }
 
@@ -783,6 +808,68 @@
         }
     }
 
+    private void initAddReads() {
+        if (addReads != null)
+            return;
+
+        addReads = new LinkedHashMap<>();
+
+        if (addReadsOpt == null)
+            return;
+
+        for (String s : addReadsOpt.split(",")) {
+            if (s.isEmpty())
+                continue;
+            int equals = s.indexOf('=');
+            if (equals == -1) {
+                // TODO: error: invalid target
+                continue;
+            }
+            String targetName = s.substring(0, equals);
+            ModuleSymbol msym = syms.enterModule(names.fromString(targetName));
+            String source = s.substring(equals + 1);
+            ModuleSymbol sourceModule;
+            if (source.equals("ALL-UNNAMED")) {
+                sourceModule = syms.unnamedModule;
+            } else {
+                if (!SourceVersion.isName(source)) {
+                    // TODO: error: invalid module name
+                    continue;
+                }
+                sourceModule = syms.enterModule(names.fromString(source));
+            }
+            addReads.computeIfAbsent(msym, m -> new HashSet<>())
+                    .add(new RequiresDirective(sourceModule, EnumSet.of(RequiresFlag.EXTRA)));
+        }
+    }
+
+    private void checkCyclicDependencies(JCModuleDecl mod) {
+        for (JCDirective d : mod.directives) {
+            if (!d.hasTag(Tag.REQUIRES))
+                continue;
+            JCRequires rd = (JCRequires) d;
+            Set<ModuleSymbol> nonSyntheticDeps = new HashSet<>();
+            List<ModuleSymbol> queue = List.of(rd.directive.module);
+            while (queue.nonEmpty()) {
+                ModuleSymbol current = queue.head;
+                queue = queue.tail;
+                if (!nonSyntheticDeps.add(current))
+                    continue;
+                if ((current.flags() & Flags.ACYCLIC) != 0)
+                    continue;
+                Assert.checkNonNull(current.requires, () -> current.toString());
+                for (RequiresDirective dep : current.requires) {
+                    if (!dep.flags.contains(RequiresFlag.EXTRA))
+                        queue = queue.prepend(dep.module);
+                }
+            }
+            if (nonSyntheticDeps.contains(mod.sym)) {
+                log.error(rd.moduleName.pos(), "cyclic.requires", rd.directive.module);
+            }
+            mod.sym.flags_field |= Flags.ACYCLIC;
+        }
+    }
+
     // DEBUG
     private String toString(ModuleSymbol msym) {
         return msym.name + "["
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/ClassWriter.java	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/ClassWriter.java	Tue Nov 10 14:19:14 2015 -0800
@@ -983,7 +983,11 @@
         ModuleSymbol m = c.modle;
 
         int alenIdx = writeAttr(names.Module);
-        List<RequiresDirective> requires = m.requires;
+        ListBuffer<RequiresDirective> requires = new ListBuffer<>();
+        for (RequiresDirective r: m.requires) {
+            if (!r.flags.contains(RequiresFlag.EXTRA))
+                requires.add(r);
+        }
         databuf.appendChar(requires.size());
         for (RequiresDirective r: requires) {
             databuf.appendChar(pool.put(r.module.name));
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java	Tue Nov 10 14:19:14 2015 -0800
@@ -550,6 +550,19 @@
         }
     },
 
+    XADDREADS("-XaddReads:", "opt.arg.addReads", "opt.addReads", EXTENDED, BASIC) {
+        @Override
+        public boolean process(OptionHelper helper, String option) {
+            String prev = helper.get(XADDREADS);
+            if (prev != null) {
+                helper.error("err.option.too.many", XADDREADS.text);
+            }
+            String p = option.substring(option.indexOf(':') + 1);
+            helper.put(XADDREADS.text, p);
+            return false;
+        }
+    },
+
     XMODULE("-Xmodule:", "opt.arg.module", "opt.module", EXTENDED, BASIC) {
         @Override
         public boolean process(OptionHelper helper, String option) {
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties	Thu Nov 05 10:49:43 2015 -0800
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties	Tue Nov 10 14:19:14 2015 -0800
@@ -286,6 +286,12 @@
     <exports> is a comma-separated list of <module-name>/<package-name>
 javac.opt.arg.addExports=\
     <exports>
+javac.opt.addReads=\
+    Specify additional modules to be considered as required by given modules, where \
+    <additional-reads> is a comma-separated list of <target-module>/<required-module>. \
+    <required-module> may be ALL-UNNAMED to require the unnamed module.
+javac.opt.arg.addReads=\
+    <additional-reads>
 javac.opt.module=\
     Specify a module to which the classes being compiled belong
 javac.opt.arg.module=\
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/tools/javac/modules/AddReadsTest.java	Tue Nov 10 14:19:14 2015 -0800
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Test the -XaddReads option
+ * @library /tools/lib
+ * @modules
+ *      jdk.compiler/com.sun.tools.javac.api
+ *      jdk.compiler/com.sun.tools.javac.main
+ * @build ToolBox ModuleTestBase
+ * @run main AddReadsTest
+ */
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.ModuleElement;
+import javax.lang.model.element.ModuleElement.RequiresDirective;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementFilter;
+
+public class AddReadsTest extends ModuleTestBase {
+
+    public static void main(String... args) throws Exception {
+        new AddReadsTest().runTests();
+    }
+
+    @Test
+    void testAddReads(Path base) throws Exception {
+        Path src = base.resolve("src");
+        Path src_m1 = src.resolve("m1");
+        tb.writeJavaFiles(src_m1,
+                          "module m1 { exports api; }",
+                          "package api; public class Api { }");
+        Path src_m2 = src.resolve("m2");
+        tb.writeJavaFiles(src_m2,
+                          "module m2 { }",
+                          "package test; public class Test extends api.Api { }");
+        Path classes = base.resolve("classes");
+        tb.createDirectories(classes);
+
+        String log = tb.new JavacTask()
+                .options("-XDrawDiagnostics",
+                         "-modulesourcepath", src.toString())
+                .outdir(classes)
+                .files(findJavaFiles(src))
+                .run(ToolBox.Expect.FAIL)
+                .writeAll()
+                .getOutput(ToolBox.OutputKind.DIRECT);
+
+        if (!log.contains("Test.java:1:44: compiler.err.not.def.access.package.cant.access: api.Api, api"))
+            throw new Exception("expected output not found");
+
+        //test add dependencies:
+        tb.new JavacTask()
+                .options("-XaddReads:m2=m1",
+                         "-modulesourcepath", src.toString(),
+                         "-processor", VerifyRequires.class.getName())
+                .outdir(classes)
+                .files(findJavaFiles(src))
+                .run()
+                .writeAll();
+
+        String decompiled = tb.new JavapTask()
+                .options("-verbose", classes.resolve("m2").resolve("module-info.class").toString())
+                .run()
+                .getOutput(ToolBox.OutputKind.DIRECT);
+
+        if (decompiled.contains("m1")) {
+            throw new Exception("Incorrectly refers to m1 module.");
+        }
+
+        //cyclic dependencies OK when created through addReads:
+        tb.new JavacTask()
+                .options("-XaddReads:m2=m1,m1=m2",
+                         "-modulesourcepath", src.toString())
+                .outdir(classes)
+                .files(findJavaFiles(src))
+                .run()
+                .writeAll();
+
+        tb.writeJavaFiles(src_m2,
+                          "module m2 { requires m1; }");
+
+        tb.new JavacTask()
+                .options("-XaddReads:m1=m2",
+                         "-modulesourcepath", src.toString())
+                .outdir(classes)
+                .files(findJavaFiles(src))
+                .run()
+                .writeAll();
+    }
+
+    @SupportedAnnotationTypes("*")
+    public static final class VerifyRequires extends AbstractProcessor {
+
+        @Override
+        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+            ModuleElement m2Module = processingEnv.getElementUtils().getModuleElement("m2");
+            if (m2Module == null) {
+                throw new AssertionError("Cannot find the m2 module!");
+            }
+            boolean foundM1 = false;
+            for (RequiresDirective rd : ElementFilter.requiresIn(m2Module.getDirectives())) {
+                foundM1 |= rd.getDependency().getSimpleName().contentEquals("m1");
+            }
+            if (!foundM1) {
+                throw new AssertionError("Cannot find the dependency on m1 module!");
+            }
+            return false;
+        }
+
+        @Override
+        public SourceVersion getSupportedSourceVersion() {
+            return SourceVersion.latest();
+        }
+
+    }
+
+    @Test
+    void testAddReadsUnnamedModule(Path base) throws Exception {
+        Path jar = prepareTestJar(base);
+
+        Path moduleSrc = base.resolve("module-src");
+        Path m1 = moduleSrc.resolve("m1");
+
+        Path classes = base.resolve("classes");
+
+        Files.createDirectories(classes);
+
+        tb.writeJavaFiles(m1,
+                          "module m1 { }",
+                          "package impl; public class Impl { api.Api api; }");
+
+        tb.new JavacTask()
+          .options("-classpath", jar.toString(),
+                   "-XaddReads:m1=ALL-UNNAMED",
+                   "-XDrawDiagnostics")
+          .outdir(classes)
+          .files(findJavaFiles(moduleSrc))
+          .run()
+          .writeAll();
+    }
+
+    @Test
+    void testAddReadsUnnamedModulePackageConflict(Path base) throws Exception {
+        Path jar = prepareTestJar(base);
+
+        Path moduleSrc = base.resolve("module-src");
+        Path m1 = moduleSrc.resolve("m1");
+
+        Path classes = base.resolve("classes");
+
+        Files.createDirectories(classes);
+
+        tb.writeJavaFiles(m1,
+                          "module m1 { }",
+                          "package api; public class Api { public static void test() { } }",
+                          "package impl; public class Impl { { api.Api.test(); } }");
+
+        tb.new JavacTask()
+          .options("-classpath", jar.toString(),
+                   "-modulesourcepath", moduleSrc.toString(),
+                   "-XaddReads:m1=ALL-UNNAMED",
+                   "-XDrawDiagnostics")
+          .outdir(classes)
+          .files(m1.resolve("impl").resolve("Impl.java"))
+          .run()
+          .writeAll();
+    }
+
+    @Test
+    void testAddReadsUnnamedToJavaBase(Path base) throws Exception {
+        Path jar = prepareTestJar(base);
+        Path src = base.resolve("src");
+        Path classes = base.resolve("classes");
+
+        Files.createDirectories(classes);
+
+        tb.writeJavaFiles(src,
+                          "package impl; public class Impl { api.Api a; }");
+
+        tb.new JavacTask()
+          .options("-classpath", jar.toString(),
+                   "-XaddReads:java.base=ALL-UNNAMED",
+                   "-Xmodule:java.base")
+          .outdir(classes)
+          .files(src.resolve("impl").resolve("Impl.java"))
+          .run()
+          .writeAll();
+    }
+
+    @Test
+    void testAddReadsToJavaBase(Path base) throws Exception {
+        Path src = base.resolve("src");
+        Path classes = base.resolve("classes");
+
+        Files.createDirectories(classes);
+
+        tb.writeJavaFiles(src,
+                          "package impl; public class Impl { javax.swing.JButton b; }");
+
+        tb.new JavacTask()
+          .options("-XaddReads:java.base=java.desktop",
+                   "-Xmodule:java.base")
+          .outdir(classes)
+          .files(findJavaFiles(src))
+          .run()
+          .writeAll();
+    }
+
+    private Path prepareTestJar(Path base) throws Exception {
+        Path legacySrc = base.resolve("legacy-src");
+        tb.writeJavaFiles(legacySrc,
+                          "package api; public abstract class Api {}");
+        Path legacyClasses = base.resolve("legacy-classes");
+        Files.createDirectories(legacyClasses);
+
+        String log = tb.new JavacTask()
+                .options()
+                .outdir(legacyClasses)
+                .files(findJavaFiles(legacySrc))
+                .run()
+                .writeAll()
+                .getOutput(ToolBox.OutputKind.DIRECT);
+
+        if (!log.isEmpty()) {
+            throw new Exception("unexpected output: " + log);
+        }
+
+        Path lib = base.resolve("lib");
+
+        Files.createDirectories(lib);
+
+        Path jar = lib.resolve("test-api-1.0.jar");
+
+        tb.new JarTask(jar)
+          .baseDir(legacyClasses)
+          .files("api/Api.class")
+          .run();
+
+        return jar;
+    }
+}