This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-compiler-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new 6b86381 Parser for `module-info-patch.maven` files (#963)
6b86381 is described below
commit 6b86381a61bf8a2b3da6be7a3948fad606d803ba
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Oct 14 18:39:18 2025 +0200
Parser for `module-info-patch.maven` files (#963)
The `module-info-patch.maven` files are intended to make easier to specify
`--add-exports` and similar options.
---
src/it/modular-sources/pom.xml | 8 +-
.../main => org.bar/main/java}/bar/App.java | 0
.../main => org.bar/main/java}/module-info.java | 0
.../test => org.bar/test/java}/bar/AppTest.java | 0
.../main => org.foo/main/java}/foo/App.java | 0
.../main => org.foo/main/java}/module-info.java | 0
.../test => org.foo/test/java}/foo/AppTest.java | 0
src/it/module-info-patch/invoker.properties | 19 +
.../{modular-sources => module-info-patch}/pom.xml | 12 +-
.../src/org.bar/main/java}/bar/App.java | 1 -
.../src/org.bar/main/java}/module-info.java | 0
.../src/org.bar/test/java}/bar/AppTest.java | 4 +
.../src/org.bar/test/java/module-info-patch.maven} | 6 +-
.../src/org.foo/main/java}/foo/App.java | 0
.../src/org.foo/main/java}/module-info.java | 2 +-
.../src/org.foo/test/java}/foo/AppTest.java | 0
.../src/org.foo/test/java/module-info-patch.maven} | 18 +-
.../verify.groovy} | 19 +-
.../maven/plugin/compiler/ModuleInfoPatch.java | 675 +++++++++++++++++++++
.../plugin/compiler/ModuleInfoPatchException.java | 50 ++
.../maven/plugin/compiler/TestCompilerMojo.java | 9 +-
.../apache/maven/plugin/compiler/ToolExecutor.java | 2 +-
.../maven/plugin/compiler/ToolExecutorForTest.java | 158 ++---
.../maven/plugin/compiler/ModuleInfoPatchTest.java | 94 +++
.../maven/plugin/compiler/module-info-patch.maven | 53 ++
25 files changed, 1015 insertions(+), 115 deletions(-)
diff --git a/src/it/modular-sources/pom.xml b/src/it/modular-sources/pom.xml
index 59b2452..a989ae2 100644
--- a/src/it/modular-sources/pom.xml
+++ b/src/it/modular-sources/pom.xml
@@ -51,20 +51,20 @@
<sources>
<source>
<module>org.foo</module>
- <directory>src/java/org.foo/main</directory>
+ <directory>src/org.foo/main/java</directory>
</source>
<source>
<module>org.foo</module>
- <directory>src/java/org.foo/test</directory>
+ <directory>src/org.foo/test/java</directory>
<scope>test</scope>
</source>
<source>
<module>org.bar</module>
- <directory>src/java/org.bar/main</directory>
+ <directory>src/org.bar/main/java</directory>
</source>
<source>
<module>org.bar</module>
- <directory>src/java/org.bar/test</directory>
+ <directory>src/org.bar/test/java</directory>
<scope>test</scope>
</source>
</sources>
diff --git a/src/it/modular-sources/src/java/org.bar/main/bar/App.java
b/src/it/modular-sources/src/org.bar/main/java/bar/App.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.bar/main/bar/App.java
copy to src/it/modular-sources/src/org.bar/main/java/bar/App.java
diff --git a/src/it/modular-sources/src/java/org.bar/main/module-info.java
b/src/it/modular-sources/src/org.bar/main/java/module-info.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.bar/main/module-info.java
copy to src/it/modular-sources/src/org.bar/main/java/module-info.java
diff --git a/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java
b/src/it/modular-sources/src/org.bar/test/java/bar/AppTest.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java
copy to src/it/modular-sources/src/org.bar/test/java/bar/AppTest.java
diff --git a/src/it/modular-sources/src/java/org.foo/main/foo/App.java
b/src/it/modular-sources/src/org.foo/main/java/foo/App.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.foo/main/foo/App.java
copy to src/it/modular-sources/src/org.foo/main/java/foo/App.java
diff --git a/src/it/modular-sources/src/java/org.foo/main/module-info.java
b/src/it/modular-sources/src/org.foo/main/java/module-info.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.foo/main/module-info.java
copy to src/it/modular-sources/src/org.foo/main/java/module-info.java
diff --git a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
b/src/it/modular-sources/src/org.foo/test/java/foo/AppTest.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
copy to src/it/modular-sources/src/org.foo/test/java/foo/AppTest.java
diff --git a/src/it/module-info-patch/invoker.properties
b/src/it/module-info-patch/invoker.properties
new file mode 100644
index 0000000..e1b3c9b
--- /dev/null
+++ b/src/it/module-info-patch/invoker.properties
@@ -0,0 +1,19 @@
+# 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.
+
+invoker.goals = clean compile test-compile
+invoker.buildResult = success
diff --git a/src/it/modular-sources/pom.xml b/src/it/module-info-patch/pom.xml
similarity index 87%
copy from src/it/modular-sources/pom.xml
copy to src/it/module-info-patch/pom.xml
index 59b2452..9c86d62 100644
--- a/src/it/modular-sources/pom.xml
+++ b/src/it/module-info-patch/pom.xml
@@ -20,10 +20,10 @@
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0
http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>org.apache.maven.plugins</groupId>
- <artifactId>modular-sources</artifactId>
+ <artifactId>module-info-patch</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
- <name>Modular project in Maven 4</name>
+ <name>Modular project with module-info-patch files</name>
<dependencies>
<dependency>
@@ -51,20 +51,20 @@
<sources>
<source>
<module>org.foo</module>
- <directory>src/java/org.foo/main</directory>
+ <directory>src/org.foo/main/java</directory>
</source>
<source>
<module>org.foo</module>
- <directory>src/java/org.foo/test</directory>
+ <directory>src/org.foo/test/java</directory>
<scope>test</scope>
</source>
<source>
<module>org.bar</module>
- <directory>src/java/org.bar/main</directory>
+ <directory>src/org.bar/main/java</directory>
</source>
<source>
<module>org.bar</module>
- <directory>src/java/org.bar/test</directory>
+ <directory>src/org.bar/test/java</directory>
<scope>test</scope>
</source>
</sources>
diff --git a/src/it/modular-sources/src/java/org.bar/main/bar/App.java
b/src/it/module-info-patch/src/org.bar/main/java/bar/App.java
similarity index 97%
rename from src/it/modular-sources/src/java/org.bar/main/bar/App.java
rename to src/it/module-info-patch/src/org.bar/main/java/bar/App.java
index c2521c8..90dd182 100644
--- a/src/it/modular-sources/src/java/org.bar/main/bar/App.java
+++ b/src/it/module-info-patch/src/org.bar/main/java/bar/App.java
@@ -20,7 +20,6 @@ package bar;
public class App {
public static void main(String[] args) {
- foo.App.main(args);
System.out.println("Bar");
}
}
diff --git a/src/it/modular-sources/src/java/org.bar/main/module-info.java
b/src/it/module-info-patch/src/org.bar/main/java/module-info.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.bar/main/module-info.java
copy to src/it/module-info-patch/src/org.bar/main/java/module-info.java
diff --git a/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java
b/src/it/module-info-patch/src/org.bar/test/java/bar/AppTest.java
similarity index 82%
rename from src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java
rename to src/it/module-info-patch/src/org.bar/test/java/bar/AppTest.java
index 2192369..8bcd5b9 100644
--- a/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java
+++ b/src/it/module-info-patch/src/org.bar/test/java/bar/AppTest.java
@@ -27,5 +27,9 @@ public class AppTest {
@Test
public void testMain() {
App.main(null);
+
+ // The following requires that the `foo` package from the `org.foo`
module is exported
+ // to this `org.bar` module, which is declared in the
`module-info-patch.maven` file.
+ foo.App.main(null);
}
}
diff --git a/src/it/modular-sources/src/java/org.bar/main/module-info.java
b/src/it/module-info-patch/src/org.bar/test/java/module-info-patch.maven
similarity index 89%
rename from src/it/modular-sources/src/java/org.bar/main/module-info.java
rename to src/it/module-info-patch/src/org.bar/test/java/module-info-patch.maven
index cf8da2f..7ab7e76 100644
--- a/src/it/modular-sources/src/java/org.bar/main/module-info.java
+++ b/src/it/module-info-patch/src/org.bar/test/java/module-info-patch.maven
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-module org.bar {
- requires org.foo;
+
+patch-module org.bar {
+ add-modules TEST-MODULE-PATH;
+ add-reads org.junit.jupiter.api;
}
diff --git a/src/it/modular-sources/src/java/org.foo/main/foo/App.java
b/src/it/module-info-patch/src/org.foo/main/java/foo/App.java
similarity index 100%
rename from src/it/modular-sources/src/java/org.foo/main/foo/App.java
rename to src/it/module-info-patch/src/org.foo/main/java/foo/App.java
diff --git a/src/it/modular-sources/src/java/org.foo/main/module-info.java
b/src/it/module-info-patch/src/org.foo/main/java/module-info.java
similarity index 92%
rename from src/it/modular-sources/src/java/org.foo/main/module-info.java
rename to src/it/module-info-patch/src/org.foo/main/java/module-info.java
index 27fa41c..ffa6d77 100644
--- a/src/it/modular-sources/src/java/org.foo/main/module-info.java
+++ b/src/it/module-info-patch/src/org.foo/main/java/module-info.java
@@ -17,5 +17,5 @@
* under the License.
*/
module org.foo {
- exports foo;
+ // Do not export `foo` as we want to test `--add-opens` options.
}
diff --git a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
b/src/it/module-info-patch/src/org.foo/test/java/foo/AppTest.java
similarity index 100%
copy from src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
copy to src/it/module-info-patch/src/org.foo/test/java/foo/AppTest.java
diff --git a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
b/src/it/module-info-patch/src/org.foo/test/java/module-info-patch.maven
similarity index 75%
copy from src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
copy to src/it/module-info-patch/src/org.foo/test/java/module-info-patch.maven
index db54dc7..efaa4ce 100644
--- a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
+++ b/src/it/module-info-patch/src/org.foo/test/java/module-info-patch.maven
@@ -16,16 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-package foo;
-import org.junit.jupiter.api.Test;
+patch-module org.foo {
+ add-modules TEST-MODULE-PATH;
-/**
- * Verifies that the compiler has access to JUnit and the main code.
- */
-public class AppTest {
- @Test
- public void testMain() {
- App.main(null);
- }
+ // Similar to `requires` in module-info.
+ // Accept also TEST-MODULE-PATH (Maven-specific).
+ add-reads org.junit.jupiter.api;
+
+ // Similar to `exports` in module-info.
+ add-exports foo to org.bar;
}
diff --git a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
b/src/it/module-info-patch/verify.groovy
similarity index 61%
rename from src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
rename to src/it/module-info-patch/verify.groovy
index db54dc7..211c143 100644
--- a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java
+++ b/src/it/module-info-patch/verify.groovy
@@ -16,16 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package foo;
-import org.junit.jupiter.api.Test;
+import java.util.jar.JarFile
-/**
- * Verifies that the compiler has access to JUnit and the main code.
- */
-public class AppTest {
- @Test
- public void testMain() {
- App.main(null);
- }
-}
+assert new File( basedir, "target/classes/org.foo/module-info.class").exists()
+assert new File( basedir, "target/classes/org.foo/foo/App.class").exists()
+assert new File( basedir,
"target/test-classes/org.foo/foo/AppTest.class").exists()
+
+assert new File( basedir, "target/classes/org.bar/module-info.class").exists()
+assert new File( basedir, "target/classes/org.bar/bar/App.class").exists()
+assert new File( basedir,
"target/test-classes/org.bar/bar/AppTest.class").exists()
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatch.java
b/src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatch.java
new file mode 100644
index 0000000..e2c3b3d
--- /dev/null
+++ b/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 null} 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);
+ }
+ addModules.clear(); // Add modules only once (this set is shared by
other instances).
+ limitModules.clear();
+ }
+}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatchException.java
b/src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatchException.java
new file mode 100644
index 0000000..a2fc33b
--- /dev/null
+++
b/src/main/java/org/apache/maven/plugin/compiler/ModuleInfoPatchException.java
@@ -0,0 +1,50 @@
+/*
+ * 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.StreamTokenizer;
+
+/**
+ * Thrown when a {@code module-info-patch.maven} file cannot be parsed.
+ *
+ * @author Martin Desruisseaux
+ */
+@SuppressWarnings("serial")
+public class ModuleInfoPatchException extends CompilationFailureException {
+ /**
+ * Creates a new exception with the given message.
+ *
+ * @param message the short message
+ */
+ public ModuleInfoPatchException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new exception with the given message followed by "at line"
and the line number.
+ * This is not in public API because the use of {@link StreamTokenizer} is
an implementation
+ * details that may change in any future version.
+ *
+ * @param message the short message
+ * @param reader the reader used for parsing the file
+ */
+ ModuleInfoPatchException(String message, StreamTokenizer reader) {
+ super(message + " at line " + reader.lineno());
+ }
+}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
index fa5ac6c..691b12f 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
@@ -201,7 +201,10 @@ public class TestCompilerMojo extends AbstractCompilerMojo
{
* <p>This field exists in this class only for transferring this
information
* to {@link ToolExecutorForTest#hasTestModuleInfo}, which is the class
that
* needs this information.</p>
+ *
+ * @deprecated Avoid {@code module-info.java} in tests.
*/
+ @Deprecated(since = "4.0.0")
transient boolean hasTestModuleInfo;
/**
@@ -398,10 +401,8 @@ public class TestCompilerMojo extends AbstractCompilerMojo
{
message.a("Overwriting the ")
.warning(MODULE_INFO + JAVA_FILE_SUFFIX)
.a(" file in the test directory is deprecated. Use ")
- .info("--add-reads")
- .a(", ")
- .info("--add-modules")
- .a(" and related options instead.");
+ .info(ModuleInfoPatch.FILENAME)
+ .a(" instead.");
logger.warn(message.toString());
if (SUPPORT_LEGACY) {
return useModulePath;
diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
index 62f40dc..1b9a20c 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
@@ -420,7 +420,7 @@ public class ToolExecutor {
}
} else if (key instanceof JavaPathType.Modular type) {
/*
- * Source code of test classes, handled as a "dependency".
+ * Main code to be tested by the test classes. This is handled
as a "dependency".
* Placed on: --patch-module-path.
*/
Optional<JavaFileManager.Location> location =
type.rawType().location();
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
index 8e4ba65..2c0fc91 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
@@ -22,21 +22,20 @@ import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.module.ModuleDescriptor;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.HashSet;
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.JavaPathType;
import org.apache.maven.api.PathType;
import org.apache.maven.api.ProjectScope;
@@ -69,15 +68,21 @@ class ToolExecutorForTest extends ToolExecutor {
* in which case the main classes are placed on the class path, but this
is deprecated.
* This flag may be removed in a future version if we remove support of
this practice.
*
+ * @deprecated Use {@code "claspath-jar"} dependency type instead, and
avoid {@code module-info.java} in tests.
+ *
* @see TestCompilerMojo#useModulePath
*/
+ @Deprecated(since = "4.0.0")
private final boolean useModulePath;
/**
* Whether a {@code module-info.java} file is defined in the test sources.
* In such case, it has precedence over the {@code module-info.java} in
main sources.
* This is defined for compatibility with Maven 3, but not recommended.
+ *
+ * @deprecated Avoid {@code module-info.java} in tests.
*/
+ @Deprecated(since = "4.0.0")
private final boolean hasTestModuleInfo;
/**
@@ -232,12 +237,15 @@ class ToolExecutorForTest extends ToolExecutor {
}
/**
- * Generates the {@code --add-modules} and {@code --add-reads} options for
the dependencies that are not
- * in the main compilation. This method is invoked only if {@code
hasModuleDeclaration} is {@code true}.
+ * Completes the given configuration with module options the first time
that this method is invoked.
+ * If at least one {@value ModuleInfoPatch#FILENAME} file is found in a
root directory of test sources,
+ * then these files are parsed and the options that they declare are added
to the given configuration.
+ * Otherwise, if {@link #hasModuleDeclaration} is {@code true}, then this
method generates the
+ * {@code --add-modules} and {@code --add-reads} options for dependencies
that are not in the main compilation.
+ * If this method is invoked more than once, all invocations after the
first one have no effect.
*
- * @param dependencyResolution the project dependencies
* @param configuration where to add the options
- * @throws IOException if the module information of a dependency cannot be
read
+ * @throws IOException if the module information of a dependency or the
module-info patch cannot be read
*/
@SuppressWarnings({"checkstyle:MissingSwitchDefault", "fallthrough"})
private void addModuleOptions(final Options configuration) throws
IOException {
@@ -245,79 +253,79 @@ class ToolExecutorForTest extends ToolExecutor {
return;
}
addedModuleOptions = true;
- if (!hasModuleDeclaration || dependencyResolution == null) {
- return;
- }
- if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo) {
- /*
- * Do not add any `--add-reads` parameters. The developers should
put
- * everything needed in the `module-info`, including test
dependencies.
- */
- return;
- }
- final var done = new HashSet<String>(); // Added modules and their
dependencies.
- final var addModules = new StringJoiner(",");
- StringJoiner addReads = null;
- boolean hasUnnamed = false;
- for (Map.Entry<Dependency, Path> entry :
- dependencyResolution.getDependencies().entrySet()) {
- boolean compile = false;
- switch (entry.getKey().getScope()) {
- case TEST:
- case TEST_ONLY:
- compile = true;
- // Fall through
- case TEST_RUNTIME:
- if (compile) {
- // Needs to be initialized even if `name` is null.
- if (addReads == null) {
- addReads = new StringJoiner(",");
- }
- }
- Path path = entry.getValue();
- String name =
dependencyResolution.getModuleName(path).orElse(null);
- if (name == null) {
- hasUnnamed = true;
- } else if (done.add(name)) {
- addModules.add(name);
- if (compile) {
- addReads.add(name);
- }
- /*
- * 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.
- */
-
dependencyResolution.getModuleDescriptor(path).ifPresent((descriptor) -> {
- for (ModuleDescriptor.Requires r :
descriptor.requires()) {
- done.add(r.name());
- }
- });
+ ModuleInfoPatch info = null;
+ ModuleInfoPatch defaultInfo = null;
+ final var patches = new LinkedHashMap<String, ModuleInfoPatch>();
+ for (SourceDirectory source : sourceDirectories) {
+ Path file = source.root.resolve(ModuleInfoPatch.FILENAME);
+ String module;
+ if (Files.notExists(file)) {
+ if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo &&
hasModuleDeclaration) {
+ /*
+ * Do not add any `--add-reads` parameters. The developers
should put
+ * everything needed in the `module-info`, including test
dependencies.
+ */
+ continue;
+ }
+ /*
+ * No `patch-module-info` file. Generate a default module
patch instance for the
+ * `--add-modules TEST-MODULE-PATH` and `--add-reads
TEST-MODULE-PATH` options.
+ * We generate that patch only for the first module. If there
is more modules
+ * without `patch-module-info`, we will copy the `defaultInfo`
instance.
+ */
+ module = source.moduleName;
+ if (module == null) {
+ module = getMainModuleName();
+ if (module.isEmpty()) {
+ continue;
}
- break;
+ }
+ if (defaultInfo != null) {
+ patches.putIfAbsent(module, null); // Remember that we
will need to compute a value later.
+ continue;
+ }
+ defaultInfo = new ModuleInfoPatch(module, info);
+ defaultInfo.setToDefaults();
+ info = defaultInfo;
+ } else {
+ info = new ModuleInfoPatch(getMainModuleName(), info);
+ try (BufferedReader reader = Files.newBufferedReader(file)) {
+ info.load(reader);
+ }
+ module = info.getModuleName();
}
- }
- if (!done.isEmpty()) {
- configuration.addIfNonBlank("--add-modules",
addModules.toString());
- }
- if (addReads != null) {
- if (hasUnnamed) {
- addReads.add("ALL-UNNAMED");
+ if (patches.put(module, info) != null) {
+ throw new ModuleInfoPatchException("\"module-info-patch " +
module + "\" is defined more than once.");
}
- String reads = addReads.toString();
- addReads(configuration, getMainModuleName(), reads);
- for (SourceDirectory root : sourceDirectories) {
- addReads(configuration, root.moduleName, reads);
+ }
+ /*
+ * Replace all occurrences of `TEST-MODULE-PATH` by the actual
dependency paths.
+ * Add `--add-modules` and `--add-reads` options with default values
equivalent to
+ * `TEST-MODULE-PATH` for every module that do not have a
`module-info-patch` file.
+ */
+ for (Map.Entry<String, ModuleInfoPatch> entry : patches.entrySet()) {
+ info = entry.getValue();
+ if (info != null) {
+ info.replaceProjectModules(sourceDirectories);
+ info.replaceTestModulePath(dependencyResolution);
+ } else {
+ // `defaultInfo` cannot be null if this `info` value is null.
+ entry.setValue(defaultInfo.patchWithSameReads(entry.getKey()));
}
}
- }
-
- /**
- * Adds an {@code --add-reads} compiler option if the given module name is
non-null and non-blank.
- */
- private static void addReads(Options configuration, String moduleName,
String reads) {
- if (moduleName != null && !moduleName.isBlank()) {
- configuration.addIfNonBlank("--add-reads", moduleName + '=' +
reads);
+ /*
+ * Write the runtime dependencies in the
`META-INF/maven/module-info-patch.args` file.
+ * Note that we unconditionally write in the root output directory,
not in the module directory,
+ * because a single option file applies to all modules.
+ */
+ if (!patches.isEmpty()) {
+ Path directory = // TODO: replace by Path.resolve(String,
String...) with JDK22.
+
Files.createDirectories(outputDirectory.resolve("META-INF").resolve("maven"));
+ try (BufferedWriter out =
Files.newBufferedWriter(directory.resolve("module-info-patch.args"))) {
+ for (ModuleInfoPatch m : patches.values()) {
+ m.writeTo(configuration, out);
+ }
+ }
}
}
diff --git
a/src/test/java/org/apache/maven/plugin/compiler/ModuleInfoPatchTest.java
b/src/test/java/org/apache/maven/plugin/compiler/ModuleInfoPatchTest.java
new file mode 100644
index 0000000..3d17e3a
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugin/compiler/ModuleInfoPatchTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 javax.tools.OptionChecker;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+
+import org.apache.maven.api.plugin.Log;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+/**
+ * Tests {@link ModuleInfoPatch}.
+ *
+ * @author Martin Desruisseaux
+ */
+public class ModuleInfoPatchTest implements OptionChecker {
+ /**
+ * Test reading a file.
+ *
+ * @throws IOException if an I/O error occurred while loading the file
+ */
+ @Test
+ public void testRead() throws IOException {
+ var info = new ModuleInfoPatch(null, null);
+ try (Reader r =
+ new
InputStreamReader(ModuleInfoPatchTest.class.getResourceAsStream("module-info-patch.maven")))
{
+ info.load(r);
+ }
+ var config = new Options(this, Mockito.mock(Log.class));
+ var out = new StringWriter();
+ try (var buffered = new BufferedWriter(out)) {
+ info.writeTo(config, buffered);
+ }
+ assertArrayEquals(
+ new String[] {
+ "--add-modules",
+ "ALL-MODULE-PATH",
+ "--limit-modules",
+ "org.junit.jupiter.api",
+ "--add-reads",
+ "org.mymodule=org.junit.jupiter.api",
+ "--add-exports",
+ "org.mymodule/org.mypackage=org.someone,org.another",
+ "--add-exports",
+ "org.mymodule/org.foo=TEST-MODULE-PATH"
+ },
+ config.options.toArray());
+
+ assertArrayEquals(
+ new String[] {
+ "--add-modules ALL-MODULE-PATH",
+ "--limit-modules org.junit.jupiter.api",
+ "--add-reads org.mymodule=org.junit.jupiter.api",
+ "--add-exports
org.mymodule/org.mypackage=org.someone,org.another",
+ "--add-exports org.mymodule/org.foo=TEST-MODULE-PATH",
+ "--add-opens org.mymodule/org.foo=org.junit.jupiter.api"
+ },
+ out.toString().split(System.lineSeparator()));
+ }
+
+ /**
+ * {@return the number of arguments the given option takes}.
+ *
+ * @param option an option
+ */
+ @Override
+ public int isSupportedOption(String option) {
+ return 1;
+ }
+}
diff --git
a/src/test/resources/org/apache/maven/plugin/compiler/module-info-patch.maven
b/src/test/resources/org/apache/maven/plugin/compiler/module-info-patch.maven
new file mode 100644
index 0000000..df2ef1a
--- /dev/null
+++
b/src/test/resources/org/apache/maven/plugin/compiler/module-info-patch.maven
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+/*
+ * Syntax: all keywords inside `patch-module` are Java compiler or Java
launcher options without
+ * the leading `--` characters. Each option value ends at the `;` character,
which is mandatory.
+ *
+ * Some options expect a value of the form
`module/package=other-module(,other-module)*`.
+ * For these options, the `module` part will be the name immediately after
`patch-module`
+ * and shall not be repeated inside the block. The `=` sign is replaced by the
`to` keyword,
+ * as in `module-info.java` files.
+ *
+ * This block accepts only options that do not require a path to source or
binary files.
+ * Options with path values should be handled as Maven dependencies or sources
instead.
+ */
+patch-module org.mymodule {
+
+ add-modules ALL-MODULE-PATH; // For testing purpose, but a valid value
would rather be TEST-MODULE-PATH.
+
+ limit-modules org.junit.jupiter.api;
+
+ // Similar to `requires` in module-info.
+ // Accept also TEST-MODULE-PATH (Maven-specific).
+ add-reads org.junit.jupiter.api;
+
+ // Similar to `exports` in module-info.
+ add-exports org.mypackage
+ to org.someone,
+ org.another;
+
+ add-exports org.foo
+ to TEST-MODULE-PATH; // Maven specific. Note: a standard
alternative is ALL-UNNAMED.
+
+ // Not used by the compiler, but useful for test executions.
+ add-opens org.foo
+ to org.junit.jupiter.api;
+}