Copilot commented on code in PR #963:
URL: 
https://github.com/apache/maven-compiler-plugin/pull/963#discussion_r2404644253


##########
src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatch.java:
##########
@@ -0,0 +1,675 @@
+/*
+ * 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.apache.maven.plugin.compiler;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.lang.module.ModuleDescriptor;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import org.apache.maven.api.Dependency;
+import org.apache.maven.api.services.DependencyResolverResult;
+
+/**
+ * Reader of {@value #FILENAME} files.
+ * The main options managed by this class are the options that are not defined 
by Maven dependencies.
+ * They are the options for opening or exporting packages to other modules, or 
reading more modules.
+ * The values of these options are module names or package names.
+ * This class does not manage the options for which the value is a path.
+ *
+ * <h2>Global options</h2>
+ * The {@code --add-modules} and {@code --limit-modules} options are global, 
not options defined on a per-module basis.
+ * The global aspect is handled by using shared maps for the {@link 
#addModules} and {@link #limitModules} fields.
+ * The value of {@code --add-modules} is usually controlled by the 
dependencies declared in the {@code pom.xml} file
+ * and rarely needs to be modified.
+ *
+ * @author Martin Desruisseaux
+ */
+final class ModuleInfoPatch {
+    /**
+     * Name of {@value} files that are parsed by this class.
+     */
+    public static final String FILENAME = "module-info-patch.maven";
+
+    /**
+     * Maven-specific keyword for meaning to export a package to all the test 
module path.
+     * Other keywords such as {@code "ALL-MODULE-PATH"} are understood by the 
Java compiler.
+     */
+    private static final String TEST_MODULE_PATH = "TEST-MODULE-PATH";
+
+    /**
+     * Maven-specific keyword for meaning to export a package to all other 
modules in the current Maven (sub)project.
+     * This is useful when a module contains a package of test fixtures also 
used for the tests in all other modules.
+     */
+    private static final String SUBPROJECT_MODULES = "SUBPROJECT-MODULES";
+
+    /**
+     * Special cases for the {@code --add-modules} option.
+     * The {@value #TEST_MODULE_PATH} keyword is specific to Maven.
+     * Other keywords in this set are recognized by the Java compiler.
+     */
+    private static final Set<String> ADD_MODULES_SPECIAL_CASES = 
Set.of("ALL-MODULE-PATH", TEST_MODULE_PATH);
+
+    /**
+     * Special cases for the {@code --add-exports} option.
+     * The {@value #TEST_MODULE_PATH} and {@value #SUBPROJECT_MODULES} 
keywords are specific to Maven.
+     * Other keywords in this set are recognized by the Java compiler.
+     */
+    private static final Set<String> ADD_EXPORTS_SPECIAL_CASES =
+            Set.of("ALL-UNNAMED", TEST_MODULE_PATH, SUBPROJECT_MODULES);
+
+    /**
+     * The name of the module to patch, or {@code null} if unspecified.
+     *
+     * @see #getModuleName()
+     */
+    private String moduleName;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code 
--add-modules} option.
+     * A unique set is shared by {@code ModuleInfoPatch} instances of a 
project, because there
+     * is only one {@code --add-module} option applying to all modules. The 
values will be the
+     * union of the values provided by all {@value #FILENAME} files.
+     */
+    private final Set<String> addModules;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code 
--limit-modules} option.
+     * A unique set is shared by all {@code ModuleInfoPatch} instances of a 
project in the
+     * same way as {@link #addModules}.
+     */
+    private final Set<String> limitModules;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code --add-reads} 
option.
+     * Option values will be prefixed by {@link #moduleName}.
+     */
+    private final Set<String> addReads;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code 
--add-exports} option.
+     * Option values will be prefixed by {@link #moduleName}.
+     * Keys are package names.
+     */
+    private final Map<String, Set<String>> addExports;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code --add-opens} 
option.
+     * Option values will be prefixed by {@link #moduleName}.
+     * Keys are package names.
+     */
+    private final Map<String, Set<String>> addOpens;
+
+    /**
+     * A clone of this {@code ModuleInfoPatch} but with runtime dependencies 
instead of compile-time dependencies.
+     * The information saved in this object are not used by the compiler 
plugin, because the runtime dependencies
+     * may differ from the runtime dependencies. But we need to save them for 
the needs of other plugins such as
+     * Surefire. If the compile and runtime dependencies are the same, then 
the value is {@code this}.
+     */
+    private ModuleInfoPatch runtimeDependencies;
+
+    /**
+     * Creates an initially empty module patch.
+     *
+     * @param defaultModule  the name of the default module if there is no 
{@value #FILENAME}
+     * @param previous       the previous instance (for sharing global 
options), or {@code null} if none.
+     */
+    ModuleInfoPatch(String defaultModule, ModuleInfoPatch previous) {
+        if (defaultModule != null && !defaultModule.isBlank()) {
+            moduleName = defaultModule;
+        }
+        if (previous != null) {
+            addModules = previous.addModules;
+            limitModules = previous.limitModules;
+        } else {
+            addModules = new LinkedHashSet<>();
+            limitModules = new LinkedHashSet<>();
+        }
+        addReads = new LinkedHashSet<>();
+        addExports = new LinkedHashMap<>();
+        addOpens = new LinkedHashMap<>();
+        runtimeDependencies = this;
+    }
+
+    /**
+     * Creates a deep clone of the given module info patch.
+     * This is used for initializing the {@link #runtimeDependencies} field.
+     *
+     * @param parent the module info patch to clone
+     */
+    private ModuleInfoPatch(ModuleInfoPatch parent) {
+        moduleName = parent.moduleName;
+        addModules = new LinkedHashSet<>(parent.addModules);
+        limitModules = new LinkedHashSet<>(parent.limitModules);
+        addReads = new LinkedHashSet<>(parent.addReads);
+        addExports = new LinkedHashMap<>(parent.addExports);
+        addOpens = new LinkedHashMap<>(parent.addOpens);
+        // Leave `runtimeDependencies` to null as it would be an error to use 
it a second time.
+    }
+
+    /**
+     * Creates a module patch with the specified {@code --add-reads} options 
and everything else empty.
+     *
+     * @param addReads the {@code --add-reads} option
+     * @param moduleName the name of the module to patch
+     *
+     * @see #patchWithSameReads(String)
+     */
+    private ModuleInfoPatch(Set<String> addReads, String moduleName) {
+        this.moduleName = moduleName;
+        this.addReads = addReads;
+        /*
+         * Really need `Collections.emptyFoo()` here, not `Set.of()` or 
`Map.of()`.
+         * A difference is that the former silently accept calls to `clear()` 
as
+         * no-operation, while the latter throw 
`UnsupportedOperationException`.
+         */
+        addModules = Collections.emptySet();
+        limitModules = Collections.emptySet();
+        addExports = Collections.emptyMap();
+        addOpens = Collections.emptyMap();
+        // `runtimeDependencies` to be initialized by the caller.
+    }
+
+    /**
+     * Sets this instance to the default configuration to use when no {@value 
#FILENAME} is present.
+     */
+    public void setToDefaults() {
+        addModules.add(TEST_MODULE_PATH);
+        addReads.add(TEST_MODULE_PATH);
+    }
+
+    /**
+     * Loads the content of the given stream of characters.
+     * This method does not close the given reader.
+     *
+     * @param source stream of characters to read
+     * @throws IOException if an I/O error occurred while loading the file
+     */
+    public void load(Reader source) throws IOException {
+        var reader = new StreamTokenizer(source);
+        reader.slashSlashComments(true);
+        reader.slashStarComments(true);
+        expectToken(reader, "patch-module");
+        moduleName = nextName(reader, true);
+        expectToken(reader, '{');
+        while (reader.nextToken() == StreamTokenizer.TT_WORD) {
+            switch (reader.sval) {
+                case "add-modules":
+                    readModuleList(reader, addModules, 
ADD_MODULES_SPECIAL_CASES);
+                    break;
+                case "limit-modules":
+                    readModuleList(reader, limitModules, Set.of());
+                    break;
+                case "add-reads":
+                    readModuleList(reader, addReads, Set.of(TEST_MODULE_PATH));
+                    break;
+                case "add-exports":
+                    readQualified(reader, addExports, 
ADD_EXPORTS_SPECIAL_CASES);
+                    break;
+                case "add-opens":
+                    readQualified(reader, addOpens, Set.of());
+                    break;
+                default:
+                    throw new ModuleInfoPatchException("Unknown keyword \"" + 
reader.sval + '"', reader);
+            }
+        }
+        if (reader.ttype != '}') {
+            throw new ModuleInfoPatchException("Not a token", reader);
+        }
+        if (reader.nextToken() != StreamTokenizer.TT_EOF) {
+            throw new ModuleInfoPatchException("Expected end of file but found 
\"" + reader.sval + '"', reader);
+        }
+    }
+
+    /**
+     * Skips a token which is expected to be equal to the given value.
+     *
+     * @param reader the reader from which to skip a token
+     * @param expected the expected token value
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token does not have the 
expected value
+     */
+    private static void expectToken(StreamTokenizer reader, String expected) 
throws IOException {
+        if (reader.nextToken() != StreamTokenizer.TT_WORD || 
!expected.equals(reader.sval)) {
+            throw new ModuleInfoPatchException("Expected \"" + expected + '"', 
reader);
+        }
+    }
+
+    /**
+     * Skips a token which is expected to be equal to the given value.
+     * The expected character must be flagged as an ordinary character in the 
reader.
+     *
+     * @param reader the reader from which to skip a token
+     * @param expected the expected character value
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token does not have the 
expected value
+     */
+    private static void expectToken(StreamTokenizer reader, char expected) 
throws IOException {
+        if (reader.nextToken() != expected) {
+            throw new ModuleInfoPatchException("Expected \"" + expected + '"', 
reader);
+        }
+    }
+
+    /**
+     * Returns the next package or module name.
+     * This method verifies that the name is non-empty and a valid Java 
identifier.
+     *
+     * @param reader the reader from which to get the package or module name
+     * @param module {@code true} is expecting a module name, {@code false} if 
expecting a package name
+     * @return the package or module name
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token is not a package or 
module name
+     */
+    private static String nextName(StreamTokenizer reader, boolean module) 
throws IOException {
+        if (reader.nextToken() != StreamTokenizer.TT_WORD) {
+            throw new ModuleInfoPatchException("Expected a " + (module ? 
"module" : "package") + " name", reader);
+        }
+        return ensureValidName(reader, reader.sval.strip(), module);
+    }
+
+    /**
+     * Verifies that the given name is a valid package or module identifier.
+     *
+     * @param reader the reader from which to get the line number if an 
exception needs to be thrown
+     * @param name the name to verify
+     * @param module {@code true} is expecting a module name, {@code false} if 
expecting a package name
+     * @throws ModuleInfoPatchException if the next token is not a package or 
module name
+     * @return the given name
+     */
+    private static String ensureValidName(StreamTokenizer reader, String name, 
boolean module) {
+        int length = name.length();
+        boolean expectFirstChar = true;
+        int c;
+        for (int i = 0; i < length; i += Character.charCount(c)) {
+            c = name.codePointAt(i);
+            if (expectFirstChar) {
+                if (Character.isJavaIdentifierStart(c)) {
+                    expectFirstChar = false;
+                } else {
+                    break; // Will throw exception because `expectFirstChar` 
is true.
+                }
+            } else if (!Character.isJavaIdentifierPart(c)) {
+                expectFirstChar = true;
+                if (c != '.') {
+                    break; // Will throw exception because `expectFirstChar` 
is true.
+                }
+            }
+        }
+        if (expectFirstChar) { // Also true if the name is empty
+            throw new ModuleInfoPatchException(
+                    "Invalid " + (module ? "module" : "package") + " name \"" 
+ name + '"', reader);
+        }
+        return name;
+    }
+
+    /**
+     * Reads a list of modules and stores the values in the given set.
+     *
+     * @param reader the reader from which to get the module names
+     * @param target where to store the module names
+     * @param specialCases special values to accept
+     * @return {@code target} or a new set if the target was initially null
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token is not a module name
+     */
+    private static void readModuleList(StreamTokenizer reader, Set<String> 
target, Set<String> specialCases)
+            throws IOException {
+        do {
+            while (reader.nextToken() == StreamTokenizer.TT_WORD) {
+                String module = reader.sval.strip();
+                if (!specialCases.contains(module)) {
+                    module = ensureValidName(reader, module, true);
+                }
+                target.add(module);
+            }
+        } while (reader.ttype == ',');
+        if (reader.ttype != ';') {
+            throw new ModuleInfoPatchException("Missing ';' character", 
reader);
+        }
+    }
+
+    /**
+     * Reads a package name followed by a list of modules names.
+     * Used for qualified exports or qualified opens.
+     *
+     * @param reader the reader from which to get the module names
+     * @param target where to store the module names
+     * @param specialCases special values to accept
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token is not a module name
+     */
+    private static void readQualified(StreamTokenizer reader, Map<String, 
Set<String>> target, Set<String> specialCases)
+            throws IOException {
+        String packageName = nextName(reader, false);
+        expectToken(reader, "to");
+        readModuleList(reader, modulesForPackage(target, packageName), 
specialCases);
+    }
+
+    /**
+     * {@return the set of modules associated to the given package name}.
+     *
+     * @param target the map where to store the set of modules
+     * @param packageName the package name for which to get a set of modules
+     */
+    private static Set<String> modulesForPackage(Map<String, Set<String>> 
target, String packageName) {
+        return target.computeIfAbsent(packageName, (key) -> new 
LinkedHashSet<>());
+    }
+
+    /**
+     * Bit mask for {@link #replaceTestModulePath(DependencyResolverResult)} 
internal usage.
+     */
+    private static final int COMPILE = 1;
+
+    /**
+     * Bit mask for {@link #replaceTestModulePath(DependencyResolverResult)} 
internal usage.
+     */
+    private static final int RUNTIME = 2;
+
+    /**
+     * Potentially adds the same value to compile and runtime sets.
+     * Whether to add a value is specified by the {@code scope} bitmask,
+     * which can contain a combination of {@link #COMPILE} and {@link 
#RUNTIME}.
+     *
+     * @param compile the collection where to add the value if the {@link 
#COMPILE} bit is set
+     * @param runtime the collection where to add the value if the {@link 
#RUNTIME} bit is set
+     * @param scope a combination of {@link #COMPILE} and {@link #RUNTIME} bits
+     * @param module the value to potentially add
+     * @return whether at least one collection has been modified
+     */
+    private static boolean addModuleName(Set<String> compile, Set<String> 
runtime, int scope, String module) {
+        boolean modified = false;
+        if ((scope & COMPILE) != 0) {
+            modified = compile.add(module);
+        }
+        if ((scope & RUNTIME) != 0 && compile != runtime) {
+            modified |= runtime.add(module);
+        }
+        return modified;
+    }
+
+    /**
+     * Potentially adds the same value to compile and runtime exports.
+     * Whether to add a value is specified by the {@code scope} bitmask,
+     * which can contain a combination of {@link #COMPILE} and {@link 
#RUNTIME}.
+     *
+     * @param packageName name of the package to export
+     * @param scope a combination of {@link #COMPILE} and {@link #RUNTIME} bits
+     * @param module the module for which to export a package
+     * @return whether at least one collection has been modified
+     */
+    private boolean addExport(String packageName, int scope, String module) {
+        Set<String> compile = modulesForPackage(addExports, packageName);
+        Set<String> runtime = compile;
+        if (runtimeDependencies != this) {
+            runtime = modulesForPackage(runtimeDependencies.addExports, 
packageName);
+        }
+        return addModuleName(compile, runtime, scope, module);
+    }
+
+    /**
+     * Replaces all occurrences of {@link #SUBPROJECT_MODULES} by the actual 
module names.
+     *
+     * @param sourceDirectories the test source directories for all modules in 
the project
+     */
+    public void replaceProjectModules(final List<SourceDirectory> 
sourceDirectories) {
+        for (Map.Entry<String, Set<String>> entry : addExports.entrySet()) {
+            if (entry.getValue().remove(SUBPROJECT_MODULES)) {
+                for (final SourceDirectory source : sourceDirectories) {
+                    final String module = source.moduleName;
+                    if (module != null && !module.equals(moduleName)) {
+                        addExport(entry.getKey(), COMPILE | RUNTIME, module);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Replaces all occurrences of {@link #TEST_MODULE_PATH} by the actual 
module names.
+     * These dependencies are automatically added to the {@code --add-modules} 
option once for all modules,
+     * then added to the {@code add-reads} option if the user specified the 
{@code TEST-MODULE-PATH} value.
+     * The latter is on a per-module basis. These options are also added 
implicitly if the user did not put
+     * a {@value #FILENAME} file in the test.
+     *
+     * @param dependencyResolution the result of resolving the dependencies, 
or {@code null} if none
+     * @throws IOException if an error occurred while reading information from 
a dependency
+     */
+    public void replaceTestModulePath(final DependencyResolverResult 
dependencyResolution) throws IOException {
+        final var exportsToTestModulePath = new LinkedHashSet<String>(); // 
Packages to export.
+        for (Map.Entry<String, Set<String>> entry : addExports.entrySet()) {
+            if (entry.getValue().remove(TEST_MODULE_PATH)) {
+                exportsToTestModulePath.add(entry.getKey());
+            }
+        }
+        final boolean addAllTestModulePath = 
addModules.remove(TEST_MODULE_PATH);
+        final boolean readAllTestModulePath = 
addReads.remove(TEST_MODULE_PATH);
+        if (!addAllTestModulePath && !readAllTestModulePath && 
exportsToTestModulePath.isEmpty()) {
+            return; // Nothing to do.
+        }
+        if (dependencyResolution == null) {
+            // Note: we could log a warning, but we would need to ensure that 
it is logged only once.
+            return;
+        }
+        /*
+         * At this point, all `TEST-MODULE-PATCH` special values have been 
removed, but the actual module names
+         * have not yet been added. The module names may be added in two 
different instances. This instance is
+         * used for compile-time dependencies, while the `runtime` instance is 
used for runtime dependencies.
+         * The latter is created only if at least one dependency is different.
+         */
+        final var done = new HashMap<String, Integer>(); // Added modules and 
their dependencies.
+        for (Map.Entry<Dependency, Path> entry :
+                dependencyResolution.getDependencies().entrySet()) {
+
+            final int scope; // As a bitmask.
+            switch (entry.getKey().getScope()) {
+                case TEST:
+                    scope = COMPILE | RUNTIME;
+                    break;
+                case TEST_ONLY:
+                    scope = COMPILE;
+                    if (runtimeDependencies == this) {
+                        runtimeDependencies = new ModuleInfoPatch(this);
+                    }
+                    break;
+                case TEST_RUNTIME:
+                    scope = RUNTIME;
+                    if (runtimeDependencies == this) {
+                        runtimeDependencies = new ModuleInfoPatch(this);
+                    }
+                    break;
+                default:
+                    continue; // Skip non-test dependencies because they 
should already be in the main module-info.
+            }
+            Path dependencyPath = entry.getValue();
+            String module = 
dependencyResolution.getModuleName(dependencyPath).orElse(null);
+            if (module == null) {
+                if (readAllTestModulePath) {
+                    addModuleName(addReads, runtimeDependencies.addReads, 
scope, "ALL-UNNAMED");
+                }
+            } else if (mergeBit(done, module, scope)) {
+                boolean modified = false;
+                if (addAllTestModulePath) {
+                    modified |= addModuleName(addModules, 
runtimeDependencies.addModules, scope, module);
+                }
+                if (readAllTestModulePath) {
+                    modified |= addModuleName(addReads, 
runtimeDependencies.addReads, scope, module);
+                }
+                for (String packageName : exportsToTestModulePath) {
+                    modified |= addExport(packageName, scope, module);
+                }
+                /*
+                 * For making the options simpler, we do not add 
`--add-modules` or `--add-reads`
+                 * options for modules that are required by a module that we 
already added. This
+                 * simplification is not necessary, but makes the command-line 
easier to read.
+                 */
+                if (modified) {
+                    
dependencyResolution.getModuleDescriptor(dependencyPath).ifPresent((descriptor) 
-> {
+                        for (ModuleDescriptor.Requires r : 
descriptor.requires()) {
+                            done.merge(r.name(), scope, (o, n) -> o | n);
+                        }
+                    });
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the given bit in a map of bit masks.
+     *
+     * @param map the map where to set a bit
+     * @param key key of the entry for which to set a bit
+     * @param bit the bit to set
+     * @return whether the map changed as a result of this operation
+     */
+    private static boolean mergeBit(final Map<String, Integer> map, final 
String key, final int bit) {
+        Integer mask = map.putIfAbsent(key, bit);
+        if (mask != null) {
+            if ((mask & bit) != 0) {
+                return false;
+            }
+            map.put(key, mask | bit);
+        }
+        return true;
+    }
+
+    /**
+     * Returns a patch for another module with the same {@code --add-reads} 
options. All other options are empty.
+     * This is used when a {@code ModuleInfoPatch} instance has been created 
for the implicit options and the
+     * caller wants to replicate these default values to other modules 
declared in the {@code <sources>}.
+     *
+     * <h4>Constraint</h4>
+     * This method should be invoked <em>after</em> {@link 
#replaceTestModulePath(DependencyResolverResult)},
+     * otherwise the runtime dependencies derived from {@code 
TEST-MODULE-PaTH} may not be correct.
+     *
+     * @param otherModule the other module to patch, or {@code null} or empty 
if none
+     * @return patch for the other module, or {@code nuoo} if {@code 
otherModule} was null or empty

Review Comment:
   Corrected spelling of 'nuoo' to 'null'.
   ```suggestion
        * @return patch for the other module, or {@code null} if {@code 
otherModule} was null or empty
   ```



##########
src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatch.java:
##########
@@ -0,0 +1,675 @@
+/*
+ * 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.apache.maven.plugin.compiler;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.lang.module.ModuleDescriptor;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import org.apache.maven.api.Dependency;
+import org.apache.maven.api.services.DependencyResolverResult;
+
+/**
+ * Reader of {@value #FILENAME} files.
+ * The main options managed by this class are the options that are not defined 
by Maven dependencies.
+ * They are the options for opening or exporting packages to other modules, or 
reading more modules.
+ * The values of these options are module names or package names.
+ * This class does not manage the options for which the value is a path.
+ *
+ * <h2>Global options</h2>
+ * The {@code --add-modules} and {@code --limit-modules} options are global, 
not options defined on a per-module basis.
+ * The global aspect is handled by using shared maps for the {@link 
#addModules} and {@link #limitModules} fields.
+ * The value of {@code --add-modules} is usually controlled by the 
dependencies declared in the {@code pom.xml} file
+ * and rarely needs to be modified.
+ *
+ * @author Martin Desruisseaux
+ */
+final class ModuleInfoPatch {
+    /**
+     * Name of {@value} files that are parsed by this class.
+     */
+    public static final String FILENAME = "module-info-patch.maven";
+
+    /**
+     * Maven-specific keyword for meaning to export a package to all the test 
module path.
+     * Other keywords such as {@code "ALL-MODULE-PATH"} are understood by the 
Java compiler.
+     */
+    private static final String TEST_MODULE_PATH = "TEST-MODULE-PATH";
+
+    /**
+     * Maven-specific keyword for meaning to export a package to all other 
modules in the current Maven (sub)project.
+     * This is useful when a module contains a package of test fixtures also 
used for the tests in all other modules.
+     */
+    private static final String SUBPROJECT_MODULES = "SUBPROJECT-MODULES";
+
+    /**
+     * Special cases for the {@code --add-modules} option.
+     * The {@value #TEST_MODULE_PATH} keyword is specific to Maven.
+     * Other keywords in this set are recognized by the Java compiler.
+     */
+    private static final Set<String> ADD_MODULES_SPECIAL_CASES = 
Set.of("ALL-MODULE-PATH", TEST_MODULE_PATH);
+
+    /**
+     * Special cases for the {@code --add-exports} option.
+     * The {@value #TEST_MODULE_PATH} and {@value #SUBPROJECT_MODULES} 
keywords are specific to Maven.
+     * Other keywords in this set are recognized by the Java compiler.
+     */
+    private static final Set<String> ADD_EXPORTS_SPECIAL_CASES =
+            Set.of("ALL-UNNAMED", TEST_MODULE_PATH, SUBPROJECT_MODULES);
+
+    /**
+     * The name of the module to patch, or {@code null} if unspecified.
+     *
+     * @see #getModuleName()
+     */
+    private String moduleName;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code 
--add-modules} option.
+     * A unique set is shared by {@code ModuleInfoPatch} instances of a 
project, because there
+     * is only one {@code --add-module} option applying to all modules. The 
values will be the
+     * union of the values provided by all {@value #FILENAME} files.
+     */
+    private final Set<String> addModules;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code 
--limit-modules} option.
+     * A unique set is shared by all {@code ModuleInfoPatch} instances of a 
project in the
+     * same way as {@link #addModules}.
+     */
+    private final Set<String> limitModules;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code --add-reads} 
option.
+     * Option values will be prefixed by {@link #moduleName}.
+     */
+    private final Set<String> addReads;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code 
--add-exports} option.
+     * Option values will be prefixed by {@link #moduleName}.
+     * Keys are package names.
+     */
+    private final Map<String, Set<String>> addExports;
+
+    /**
+     * Values parsed from the {@value #FILENAME} file for {@code --add-opens} 
option.
+     * Option values will be prefixed by {@link #moduleName}.
+     * Keys are package names.
+     */
+    private final Map<String, Set<String>> addOpens;
+
+    /**
+     * A clone of this {@code ModuleInfoPatch} but with runtime dependencies 
instead of compile-time dependencies.
+     * The information saved in this object are not used by the compiler 
plugin, because the runtime dependencies
+     * may differ from the runtime dependencies. But we need to save them for 
the needs of other plugins such as
+     * Surefire. If the compile and runtime dependencies are the same, then 
the value is {@code this}.
+     */
+    private ModuleInfoPatch runtimeDependencies;
+
+    /**
+     * Creates an initially empty module patch.
+     *
+     * @param defaultModule  the name of the default module if there is no 
{@value #FILENAME}
+     * @param previous       the previous instance (for sharing global 
options), or {@code null} if none.
+     */
+    ModuleInfoPatch(String defaultModule, ModuleInfoPatch previous) {
+        if (defaultModule != null && !defaultModule.isBlank()) {
+            moduleName = defaultModule;
+        }
+        if (previous != null) {
+            addModules = previous.addModules;
+            limitModules = previous.limitModules;
+        } else {
+            addModules = new LinkedHashSet<>();
+            limitModules = new LinkedHashSet<>();
+        }
+        addReads = new LinkedHashSet<>();
+        addExports = new LinkedHashMap<>();
+        addOpens = new LinkedHashMap<>();
+        runtimeDependencies = this;
+    }
+
+    /**
+     * Creates a deep clone of the given module info patch.
+     * This is used for initializing the {@link #runtimeDependencies} field.
+     *
+     * @param parent the module info patch to clone
+     */
+    private ModuleInfoPatch(ModuleInfoPatch parent) {
+        moduleName = parent.moduleName;
+        addModules = new LinkedHashSet<>(parent.addModules);
+        limitModules = new LinkedHashSet<>(parent.limitModules);
+        addReads = new LinkedHashSet<>(parent.addReads);
+        addExports = new LinkedHashMap<>(parent.addExports);
+        addOpens = new LinkedHashMap<>(parent.addOpens);
+        // Leave `runtimeDependencies` to null as it would be an error to use 
it a second time.
+    }
+
+    /**
+     * Creates a module patch with the specified {@code --add-reads} options 
and everything else empty.
+     *
+     * @param addReads the {@code --add-reads} option
+     * @param moduleName the name of the module to patch
+     *
+     * @see #patchWithSameReads(String)
+     */
+    private ModuleInfoPatch(Set<String> addReads, String moduleName) {
+        this.moduleName = moduleName;
+        this.addReads = addReads;
+        /*
+         * Really need `Collections.emptyFoo()` here, not `Set.of()` or 
`Map.of()`.
+         * A difference is that the former silently accept calls to `clear()` 
as
+         * no-operation, while the latter throw 
`UnsupportedOperationException`.
+         */
+        addModules = Collections.emptySet();
+        limitModules = Collections.emptySet();
+        addExports = Collections.emptyMap();
+        addOpens = Collections.emptyMap();
+        // `runtimeDependencies` to be initialized by the caller.
+    }
+
+    /**
+     * Sets this instance to the default configuration to use when no {@value 
#FILENAME} is present.
+     */
+    public void setToDefaults() {
+        addModules.add(TEST_MODULE_PATH);
+        addReads.add(TEST_MODULE_PATH);
+    }
+
+    /**
+     * Loads the content of the given stream of characters.
+     * This method does not close the given reader.
+     *
+     * @param source stream of characters to read
+     * @throws IOException if an I/O error occurred while loading the file
+     */
+    public void load(Reader source) throws IOException {
+        var reader = new StreamTokenizer(source);
+        reader.slashSlashComments(true);
+        reader.slashStarComments(true);
+        expectToken(reader, "patch-module");
+        moduleName = nextName(reader, true);
+        expectToken(reader, '{');
+        while (reader.nextToken() == StreamTokenizer.TT_WORD) {
+            switch (reader.sval) {
+                case "add-modules":
+                    readModuleList(reader, addModules, 
ADD_MODULES_SPECIAL_CASES);
+                    break;
+                case "limit-modules":
+                    readModuleList(reader, limitModules, Set.of());
+                    break;
+                case "add-reads":
+                    readModuleList(reader, addReads, Set.of(TEST_MODULE_PATH));
+                    break;
+                case "add-exports":
+                    readQualified(reader, addExports, 
ADD_EXPORTS_SPECIAL_CASES);
+                    break;
+                case "add-opens":
+                    readQualified(reader, addOpens, Set.of());
+                    break;
+                default:
+                    throw new ModuleInfoPatchException("Unknown keyword \"" + 
reader.sval + '"', reader);
+            }
+        }
+        if (reader.ttype != '}') {
+            throw new ModuleInfoPatchException("Not a token", reader);
+        }
+        if (reader.nextToken() != StreamTokenizer.TT_EOF) {
+            throw new ModuleInfoPatchException("Expected end of file but found 
\"" + reader.sval + '"', reader);
+        }
+    }
+
+    /**
+     * Skips a token which is expected to be equal to the given value.
+     *
+     * @param reader the reader from which to skip a token
+     * @param expected the expected token value
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token does not have the 
expected value
+     */
+    private static void expectToken(StreamTokenizer reader, String expected) 
throws IOException {
+        if (reader.nextToken() != StreamTokenizer.TT_WORD || 
!expected.equals(reader.sval)) {
+            throw new ModuleInfoPatchException("Expected \"" + expected + '"', 
reader);
+        }
+    }
+
+    /**
+     * Skips a token which is expected to be equal to the given value.
+     * The expected character must be flagged as an ordinary character in the 
reader.
+     *
+     * @param reader the reader from which to skip a token
+     * @param expected the expected character value
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token does not have the 
expected value
+     */
+    private static void expectToken(StreamTokenizer reader, char expected) 
throws IOException {
+        if (reader.nextToken() != expected) {
+            throw new ModuleInfoPatchException("Expected \"" + expected + '"', 
reader);
+        }
+    }
+
+    /**
+     * Returns the next package or module name.
+     * This method verifies that the name is non-empty and a valid Java 
identifier.
+     *
+     * @param reader the reader from which to get the package or module name
+     * @param module {@code true} is expecting a module name, {@code false} if 
expecting a package name
+     * @return the package or module name
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token is not a package or 
module name
+     */
+    private static String nextName(StreamTokenizer reader, boolean module) 
throws IOException {
+        if (reader.nextToken() != StreamTokenizer.TT_WORD) {
+            throw new ModuleInfoPatchException("Expected a " + (module ? 
"module" : "package") + " name", reader);
+        }
+        return ensureValidName(reader, reader.sval.strip(), module);
+    }
+
+    /**
+     * Verifies that the given name is a valid package or module identifier.
+     *
+     * @param reader the reader from which to get the line number if an 
exception needs to be thrown
+     * @param name the name to verify
+     * @param module {@code true} is expecting a module name, {@code false} if 
expecting a package name
+     * @throws ModuleInfoPatchException if the next token is not a package or 
module name
+     * @return the given name
+     */
+    private static String ensureValidName(StreamTokenizer reader, String name, 
boolean module) {
+        int length = name.length();
+        boolean expectFirstChar = true;
+        int c;
+        for (int i = 0; i < length; i += Character.charCount(c)) {
+            c = name.codePointAt(i);
+            if (expectFirstChar) {
+                if (Character.isJavaIdentifierStart(c)) {
+                    expectFirstChar = false;
+                } else {
+                    break; // Will throw exception because `expectFirstChar` 
is true.
+                }
+            } else if (!Character.isJavaIdentifierPart(c)) {
+                expectFirstChar = true;
+                if (c != '.') {
+                    break; // Will throw exception because `expectFirstChar` 
is true.
+                }
+            }
+        }
+        if (expectFirstChar) { // Also true if the name is empty
+            throw new ModuleInfoPatchException(
+                    "Invalid " + (module ? "module" : "package") + " name \"" 
+ name + '"', reader);
+        }
+        return name;
+    }
+
+    /**
+     * Reads a list of modules and stores the values in the given set.
+     *
+     * @param reader the reader from which to get the module names
+     * @param target where to store the module names
+     * @param specialCases special values to accept
+     * @return {@code target} or a new set if the target was initially null
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token is not a module name
+     */
+    private static void readModuleList(StreamTokenizer reader, Set<String> 
target, Set<String> specialCases)
+            throws IOException {
+        do {
+            while (reader.nextToken() == StreamTokenizer.TT_WORD) {
+                String module = reader.sval.strip();
+                if (!specialCases.contains(module)) {
+                    module = ensureValidName(reader, module, true);
+                }
+                target.add(module);
+            }
+        } while (reader.ttype == ',');
+        if (reader.ttype != ';') {
+            throw new ModuleInfoPatchException("Missing ';' character", 
reader);
+        }
+    }
+
+    /**
+     * Reads a package name followed by a list of modules names.
+     * Used for qualified exports or qualified opens.
+     *
+     * @param reader the reader from which to get the module names
+     * @param target where to store the module names
+     * @param specialCases special values to accept
+     * @throws IOException if an I/O error occurred while loading the file
+     * @throws ModuleInfoPatchException if the next token is not a module name
+     */
+    private static void readQualified(StreamTokenizer reader, Map<String, 
Set<String>> target, Set<String> specialCases)
+            throws IOException {
+        String packageName = nextName(reader, false);
+        expectToken(reader, "to");
+        readModuleList(reader, modulesForPackage(target, packageName), 
specialCases);
+    }
+
+    /**
+     * {@return the set of modules associated to the given package name}.
+     *
+     * @param target the map where to store the set of modules
+     * @param packageName the package name for which to get a set of modules
+     */
+    private static Set<String> modulesForPackage(Map<String, Set<String>> 
target, String packageName) {
+        return target.computeIfAbsent(packageName, (key) -> new 
LinkedHashSet<>());
+    }
+
+    /**
+     * Bit mask for {@link #replaceTestModulePath(DependencyResolverResult)} 
internal usage.
+     */
+    private static final int COMPILE = 1;
+
+    /**
+     * Bit mask for {@link #replaceTestModulePath(DependencyResolverResult)} 
internal usage.
+     */
+    private static final int RUNTIME = 2;
+
+    /**
+     * Potentially adds the same value to compile and runtime sets.
+     * Whether to add a value is specified by the {@code scope} bitmask,
+     * which can contain a combination of {@link #COMPILE} and {@link 
#RUNTIME}.
+     *
+     * @param compile the collection where to add the value if the {@link 
#COMPILE} bit is set
+     * @param runtime the collection where to add the value if the {@link 
#RUNTIME} bit is set
+     * @param scope a combination of {@link #COMPILE} and {@link #RUNTIME} bits
+     * @param module the value to potentially add
+     * @return whether at least one collection has been modified
+     */
+    private static boolean addModuleName(Set<String> compile, Set<String> 
runtime, int scope, String module) {
+        boolean modified = false;
+        if ((scope & COMPILE) != 0) {
+            modified = compile.add(module);
+        }
+        if ((scope & RUNTIME) != 0 && compile != runtime) {
+            modified |= runtime.add(module);
+        }
+        return modified;
+    }
+
+    /**
+     * Potentially adds the same value to compile and runtime exports.
+     * Whether to add a value is specified by the {@code scope} bitmask,
+     * which can contain a combination of {@link #COMPILE} and {@link 
#RUNTIME}.
+     *
+     * @param packageName name of the package to export
+     * @param scope a combination of {@link #COMPILE} and {@link #RUNTIME} bits
+     * @param module the module for which to export a package
+     * @return whether at least one collection has been modified
+     */
+    private boolean addExport(String packageName, int scope, String module) {
+        Set<String> compile = modulesForPackage(addExports, packageName);
+        Set<String> runtime = compile;
+        if (runtimeDependencies != this) {
+            runtime = modulesForPackage(runtimeDependencies.addExports, 
packageName);
+        }
+        return addModuleName(compile, runtime, scope, module);
+    }
+
+    /**
+     * Replaces all occurrences of {@link #SUBPROJECT_MODULES} by the actual 
module names.
+     *
+     * @param sourceDirectories the test source directories for all modules in 
the project
+     */
+    public void replaceProjectModules(final List<SourceDirectory> 
sourceDirectories) {
+        for (Map.Entry<String, Set<String>> entry : addExports.entrySet()) {
+            if (entry.getValue().remove(SUBPROJECT_MODULES)) {
+                for (final SourceDirectory source : sourceDirectories) {
+                    final String module = source.moduleName;
+                    if (module != null && !module.equals(moduleName)) {
+                        addExport(entry.getKey(), COMPILE | RUNTIME, module);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Replaces all occurrences of {@link #TEST_MODULE_PATH} by the actual 
module names.
+     * These dependencies are automatically added to the {@code --add-modules} 
option once for all modules,
+     * then added to the {@code add-reads} option if the user specified the 
{@code TEST-MODULE-PATH} value.
+     * The latter is on a per-module basis. These options are also added 
implicitly if the user did not put
+     * a {@value #FILENAME} file in the test.
+     *
+     * @param dependencyResolution the result of resolving the dependencies, 
or {@code null} if none
+     * @throws IOException if an error occurred while reading information from 
a dependency
+     */
+    public void replaceTestModulePath(final DependencyResolverResult 
dependencyResolution) throws IOException {
+        final var exportsToTestModulePath = new LinkedHashSet<String>(); // 
Packages to export.
+        for (Map.Entry<String, Set<String>> entry : addExports.entrySet()) {
+            if (entry.getValue().remove(TEST_MODULE_PATH)) {
+                exportsToTestModulePath.add(entry.getKey());
+            }
+        }
+        final boolean addAllTestModulePath = 
addModules.remove(TEST_MODULE_PATH);
+        final boolean readAllTestModulePath = 
addReads.remove(TEST_MODULE_PATH);
+        if (!addAllTestModulePath && !readAllTestModulePath && 
exportsToTestModulePath.isEmpty()) {
+            return; // Nothing to do.
+        }
+        if (dependencyResolution == null) {
+            // Note: we could log a warning, but we would need to ensure that 
it is logged only once.
+            return;
+        }
+        /*
+         * At this point, all `TEST-MODULE-PATCH` special values have been 
removed, but the actual module names
+         * have not yet been added. The module names may be added in two 
different instances. This instance is
+         * used for compile-time dependencies, while the `runtime` instance is 
used for runtime dependencies.
+         * The latter is created only if at least one dependency is different.
+         */
+        final var done = new HashMap<String, Integer>(); // Added modules and 
their dependencies.
+        for (Map.Entry<Dependency, Path> entry :
+                dependencyResolution.getDependencies().entrySet()) {
+
+            final int scope; // As a bitmask.
+            switch (entry.getKey().getScope()) {
+                case TEST:
+                    scope = COMPILE | RUNTIME;
+                    break;
+                case TEST_ONLY:
+                    scope = COMPILE;
+                    if (runtimeDependencies == this) {
+                        runtimeDependencies = new ModuleInfoPatch(this);
+                    }
+                    break;
+                case TEST_RUNTIME:
+                    scope = RUNTIME;
+                    if (runtimeDependencies == this) {
+                        runtimeDependencies = new ModuleInfoPatch(this);
+                    }
+                    break;
+                default:
+                    continue; // Skip non-test dependencies because they 
should already be in the main module-info.
+            }
+            Path dependencyPath = entry.getValue();
+            String module = 
dependencyResolution.getModuleName(dependencyPath).orElse(null);
+            if (module == null) {
+                if (readAllTestModulePath) {
+                    addModuleName(addReads, runtimeDependencies.addReads, 
scope, "ALL-UNNAMED");
+                }
+            } else if (mergeBit(done, module, scope)) {
+                boolean modified = false;
+                if (addAllTestModulePath) {
+                    modified |= addModuleName(addModules, 
runtimeDependencies.addModules, scope, module);
+                }
+                if (readAllTestModulePath) {
+                    modified |= addModuleName(addReads, 
runtimeDependencies.addReads, scope, module);
+                }
+                for (String packageName : exportsToTestModulePath) {
+                    modified |= addExport(packageName, scope, module);
+                }
+                /*
+                 * For making the options simpler, we do not add 
`--add-modules` or `--add-reads`
+                 * options for modules that are required by a module that we 
already added. This
+                 * simplification is not necessary, but makes the command-line 
easier to read.
+                 */
+                if (modified) {
+                    
dependencyResolution.getModuleDescriptor(dependencyPath).ifPresent((descriptor) 
-> {
+                        for (ModuleDescriptor.Requires r : 
descriptor.requires()) {
+                            done.merge(r.name(), scope, (o, n) -> o | n);
+                        }
+                    });
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the given bit in a map of bit masks.
+     *
+     * @param map the map where to set a bit
+     * @param key key of the entry for which to set a bit
+     * @param bit the bit to set
+     * @return whether the map changed as a result of this operation
+     */
+    private static boolean mergeBit(final Map<String, Integer> map, final 
String key, final int bit) {
+        Integer mask = map.putIfAbsent(key, bit);
+        if (mask != null) {
+            if ((mask & bit) != 0) {
+                return false;
+            }
+            map.put(key, mask | bit);
+        }
+        return true;
+    }
+
+    /**
+     * Returns a patch for another module with the same {@code --add-reads} 
options. All other options are empty.
+     * This is used when a {@code ModuleInfoPatch} instance has been created 
for the implicit options and the
+     * caller wants to replicate these default values to other modules 
declared in the {@code <sources>}.
+     *
+     * <h4>Constraint</h4>
+     * This method should be invoked <em>after</em> {@link 
#replaceTestModulePath(DependencyResolverResult)},
+     * otherwise the runtime dependencies derived from {@code 
TEST-MODULE-PaTH} may not be correct.
+     *
+     * @param otherModule the other module to patch, or {@code null} or empty 
if none
+     * @return patch for the other module, or {@code nuoo} if {@code 
otherModule} was null or empty
+     */
+    public ModuleInfoPatch patchWithSameReads(String otherModule) {
+        if (otherModule == null || otherModule.isBlank()) {
+            return null;
+        }
+        var other = new ModuleInfoPatch(addReads, otherModule);
+        other.runtimeDependencies =
+                (runtimeDependencies == this) ? other : new 
ModuleInfoPatch(runtimeDependencies.addReads, otherModule);
+        return other;
+    }
+
+    /**
+     * {@return the name of the module to patch, or null if unspecified and no 
default}.
+     */
+    public String getModuleName() {
+        return moduleName;
+    }
+
+    /**
+     * Writes the values of the given option if the values is is non-null.
+     *
+     * @param option the option for which to write the values
+     * @param prefix prefix to write, followed by {@code '='}, before the 
value, or empty if none
+     * @param compile the values to write for the compiler, or {@code null} if 
none
+     * @param runtime the values to write for the Java launcher
+     * @param configuration where to write the option values for the compiler
+     * @param out where to write the option values for the Java launcher
+     */
+    private static void write(
+            String option,
+            String prefix,
+            Set<String> compile,
+            Set<String> runtime,
+            Options configuration,
+            BufferedWriter out)
+            throws IOException {
+        Set<String> values = runtime;
+        do {
+            if (!values.isEmpty()) {
+                var buffer = new StringJoiner(",", (prefix != null) ? prefix + 
'=' : "", "");
+                for (String value : values) {
+                    buffer.add(value);
+                }
+                if (values == compile) {
+                    configuration.addIfNonBlank("--" + option, 
buffer.toString());
+                }
+                if (values == runtime) {
+                    out.append("--").append(option).append(' 
').append(buffer.toString());
+                    out.newLine();
+                }
+            }
+        } while (values != compile && (values = compile) != null);
+    }
+
+    /**
+     * Writes options that are qualified by module name and package name.
+     *
+     * @param option the option for which to write the values
+     * @param compile the values to write for the compiler, or {@code null} if 
none
+     * @param runtime the values to write for the Java launcher
+     * @param configuration where to write the option values for the compiler
+     * @param out where to write the option values for the Java launcher
+     */
+    private void write(
+            String option,
+            Map<String, Set<String>> compile,
+            Map<String, Set<String>> runtime,
+            Options configuration,
+            BufferedWriter out)
+            throws IOException {
+        Map<String, Set<String>> values = runtime;
+        do {
+            for (Map.Entry<String, Set<String>> entry : values.entrySet()) {
+                String prefix = moduleName + '/' + entry.getKey();
+                Set<String> otherModules = entry.getValue();
+                write(
+                        option,
+                        prefix,
+                        (values == compile) ? otherModules : null,
+                        (values == runtime) ? otherModules : Set.of(),
+                        configuration,
+                        out);
+            }
+        } while (values != compile && (values = compile) != null);
+    }
+
+    /**
+     * Writes the options.
+     *
+     * @param compile where to write the compile-time options
+     * @param runtime where to write the runtime options
+     */
+    public void writeTo(final Options compile, final BufferedWriter runtime) 
throws IOException {
+        write("add-modules", null, addModules, runtimeDependencies.addModules, 
compile, runtime);
+        write("limit-modules", null, limitModules, 
runtimeDependencies.limitModules, compile, runtime);
+        if (moduleName != null) {
+            write("add-reads", moduleName, addReads, 
runtimeDependencies.addReads, compile, runtime);
+            write("add-exports", addExports, runtimeDependencies.addExports, 
compile, runtime);
+            write("add-opens", null, runtimeDependencies.addOpens, compile, 
runtime);

Review Comment:
   The `add-opens` option should use `addOpens` for compile-time options, not 
`null`. This makes the compile and runtime options inconsistent.
   ```suggestion
               write("add-opens", addOpens, runtimeDependencies.addOpens, 
compile, runtime);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to