diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..7a494beaa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + assignees: + - "ascopes" + labels: + - "enhancement" + - "housekeeping" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f411d0fd8..84936babd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [windows-latest, ubuntu-latest] java-version: [ 11, 12, 13, 14, 15, 16, 17, 18 ] @@ -73,7 +74,7 @@ jobs: - name: Annotate test reports if: always() run: >- - .github/scripts/prepare-test-outputs-for-merge.sh + scripts/prepare-test-outputs-for-merge.sh ${{ matrix.java-version }} ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 9c27dd563..a3b80043f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ build/ out/ target/ -# IntelliJ -.idea/ +# IntelliJ junk *.iml +.idea/* +# Allow providing file templates for simplicity. +!.idea/fileTemplates # VSCode .vscode/ diff --git a/.idea/fileTemplates/internal/AnnotationType.java b/.idea/fileTemplates/internal/AnnotationType.java new file mode 100644 index 000000000..835407826 --- /dev/null +++ b/.idea/fileTemplates/internal/AnnotationType.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * Licensed 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * TODO(${USER}): update the documentation for this annotation. + * + *
Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public @interface ${NAME} { +} diff --git a/.idea/fileTemplates/internal/Class.java b/.idea/fileTemplates/internal/Class.java new file mode 100644 index 000000000..66759200b --- /dev/null +++ b/.idea/fileTemplates/internal/Class.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * Licensed 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * TODO(${USER}): update the documentation for this class. + * + *
Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public class ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Enum.java b/.idea/fileTemplates/internal/Enum.java new file mode 100644 index 000000000..8e5ee61f1 --- /dev/null +++ b/.idea/fileTemplates/internal/Enum.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * Licensed 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.Status; + +/** + * TODO(${USER}): update the documentation for this enum. + * + *
Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public enum ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Interface.java b/.idea/fileTemplates/internal/Interface.java new file mode 100644 index 000000000..40360e999 --- /dev/null +++ b/.idea/fileTemplates/internal/Interface.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * Licensed 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.Status; + +/** + * TODO(${USER}): update the documentation for this interface. + * + *
Also, change the {@code since} tag to hold the current library
+ * version.
+ *
+ * @author ${USER}
+ * @since XXX
+ */
+@API(since = "XXX", status = Status.EXPERIMENTAL)
+public interface ${NAME} {
+}
\ No newline at end of file
diff --git a/.idea/fileTemplates/internal/package-info.java b/.idea/fileTemplates/internal/package-info.java
new file mode 100644
index 000000000..aa1ea35e7
--- /dev/null
+++ b/.idea/fileTemplates/internal/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) ${YEAR} Ashley Scopes
+ *
+ * Licensed 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.
+ */
+
+/**
+ * TODO(${USER}): update the documentation for this package.
+ */
+package ${NAME};
diff --git a/.mvn/checkstyle/checkstyle.xml b/.mvn/checkstyle/checkstyle.xml
index 7200e8259..1411d388d 100644
--- a/.mvn/checkstyle/checkstyle.xml
+++ b/.mvn/checkstyle/checkstyle.xml
@@ -47,8 +47,13 @@
If warnings were treated as errors by the compiler, then this is identical to calling
+ * {@link #isSuccessful()}.
*
- * @return the assertions to perform.
+ * @return this assertion object.
*/
- public PathLocationManagerAssert classOutput() {
- return location(StandardLocation.CLASS_OUTPUT);
+ public CompilationAssert isSuccessfulWithoutWarnings() {
+ isSuccessful();
+ diagnostics().hasNoErrorsOrWarnings();
+ return myself;
}
/**
- * Perform assertions on the class output roots for a given module name.
+ * Assert that the compilation was a failure.
*
- * @param moduleName the name of the module.
- * @return the assertions to perform.
+ * @return this assertion object.
*/
- public PathLocationManagerAssert classOutput(String moduleName) {
- return location(StandardLocation.CLASS_OUTPUT, moduleName);
- }
+ public CompilationAssert isFailure() {
+ if (actual.isSuccessful()) {
+ var warnings = actual
+ .getDiagnostics()
+ .stream()
+ .filter(kind -> WARNING_DIAGNOSTIC_KINDS.contains(kind.getKind()))
+ .collect(Collectors.toUnmodifiableList());
- /**
- * Perform assertions on the native header outputs.
- *
- * @return the assertions to perform.
- */
- public PathLocationManagerAssert nativeHeaders() {
- return location(StandardLocation.NATIVE_HEADER_OUTPUT);
- }
+ failWithDiagnostics(warnings, "Expected compilation to fail, but it succeeded.");
+ }
- /**
- * Perform assertions on the native header outputs for a given module name.
- *
- * @param moduleName the name of the module.
- * @return the assertions to perform.
- */
- public PathLocationManagerAssert nativeHeaders(String moduleName) {
- return location(StandardLocation.NATIVE_HEADER_OUTPUT, moduleName);
+ return myself;
}
/**
- * Perform assertions on the generated source outputs.
+ * Get assertions for diagnostics.
*
- * @return the assertions to perform.
+ * @return assertions for the diagnostics.
*/
- public PathLocationManagerAssert generatedSources() {
- return location(StandardLocation.SOURCE_OUTPUT);
+ public DiagnosticListAssert diagnostics() {
+ return new DiagnosticListAssert(actual.getDiagnostics());
}
- /**
- * Perform assertions on the generated source outputs for a given module name.
- *
- * @param moduleName the name of the module.
- * @return the assertions to perform.
- */
- public PathLocationManagerAssert generatedSources(String moduleName) {
- return location(StandardLocation.SOURCE_PATH, moduleName);
- }
+ private void failWithDiagnostics(
+ List extends TraceDiagnostic>> diagnostics,
+ String message,
+ Object... args
+ ) {
+ if (diagnostics.isEmpty()) {
+ failWithMessage(message, args);
+ } else {
+ var fullMessage = String.join(
+ "\n\n",
+ args.length > 0
+ ? String.format(message, args)
+ : message,
+ "Diagnostics:",
+ DiagnosticListRepresentation.getInstance().toStringOf(diagnostics)
+ );
- /**
- * Create a new assertion object.
- *
- * @param compilation the compilation to assert on.
- * @param The value may be empty if no position was provided.
*
- * @return an assertion to perform on the thread name.
+ * @return the assertions for the position of the diagnostic.
*/
- public AbstractStringAssert> threadName() {
- return Assertions.assertThat(actual.getThreadName().orElse(null));
+ public OptionalLongAssert position() {
+ return assertPosition(actual.getPosition(), "position");
}
/**
- * Perform an assertion on the stack trace location that the diagnostic was reported from.
+ * Get assertions for the start position of the diagnostic.
*
- * @return an assertion to perform on the list of stack trace frames.
+ * The value may be empty if no position was provided.
+ *
+ * @return the assertions for the start position of the diagnostic.
*/
- public ListAssert The value may be empty if no position was provided.
*
- * @return an assertion to perform on the kind.
+ * @return the assertions for the end position of the diagnostic.
*/
- public AbstractComparableAssert, Kind> kind() {
- return Assertions.assertThat(actual.getKind());
+ public OptionalLongAssert endPosition() {
+ return assertPosition(actual.getEndPosition(), "endPosition");
}
/**
- * Perform an assertion on the source that the diagnostic was reported from.
+ * Get assertions for the line number of the diagnostic.
*
- * @return the assertion to perform on the source.
+ * The value may be empty if no position was provided.
+ *
+ * @return the assertions for the line number of the diagnostic.
*/
- public AbstractObjectAssert, JavaFileObject> source() {
- return Assertions.assertThat(actual.getSource());
+ public OptionalLongAssert lineNumber() {
+ return assertPosition(actual.getLineNumber(), "lineNumber");
}
/**
- * Perform an assertion on the position in the source that the diagnostic was reported from.
+ * Get assertions for the column number of the diagnostic.
+ *
+ * The value may be empty if no position was provided.
*
- * @return an assertion to perform on the position.
+ * @return the assertions for the column number of the diagnostic.
*/
- public AbstractLongAssert> position() {
- return Assertions.assertThat(actual.getPosition());
+ public OptionalLongAssert columnNumber() {
+ return assertPosition(actual.getColumnNumber(), "columnNumber");
}
/**
- * Perform an assertion on the start position in the source that the diagnostic was reported
- * from.
+ * Get assertions for the code of the diagnostic.
*
- * @return an assertion to perform on the start position.
+ * @return the assertions for the code of the diagnostic.
*/
- public AbstractLongAssert> startPosition() {
- return Assertions.assertThat(actual.getStartPosition());
+ public StringAssert code() {
+ return new StringAssert(actual.getCode());
}
/**
- * Perform an assertion on the end position in the source that the diagnostic was reported from.
+ * Get assertions for the message of the diagnostic, assuming the default locale.
*
- * @return an assertion to perform on the end position.
+ * @return the assertions for the message of the diagnostic.
*/
- public AbstractLongAssert> endPosition() {
- return Assertions.assertThat(actual.getEndPosition());
+ public StringAssert message() {
+ return new StringAssert(actual.getMessage(null));
}
/**
- * Perform an assertion on the line number in the source that the diagnostic was reported from.
+ * Get assertions for the message of the diagnostic.
*
- * @return an assertion to perform on the line number.
+ * @param locale the locale to use.
+ * @return the assertions for the message of the diagnostic.
*/
- public AbstractLongAssert> lineNumber() {
- return Assertions.assertThat(actual.getLineNumber());
+ public StringAssert message(Locale locale) {
+ requireNonNull(locale, "locale");
+ return new StringAssert(actual.getMessage(locale));
}
/**
- * Perform an assertion on the column number in the source that the diagnostic was reported from.
+ * Get assertions for the timestamp of the diagnostic.
*
- * @return an assertion to perform on the column number.
+ * @return the assertions for the timestamp of the diagnostic.
*/
- public AbstractLongAssert> columnNumber() {
- return Assertions.assertThat(actual.getColumnNumber());
+ public InstantAssert timestamp() {
+ return new InstantAssert(actual.getTimestamp());
}
/**
- * Perform an assertion on the diagnostic code.
- *
- * This is usually only present for compiler-provided diagnostics, and is not able to be
- * specified in the standard annotation processing API (where this will be null).
+ * Get assertions for the thread ID of the thread that reported the diagnostic to the compiler.
*
- * @return an assertion to perform on the diagnostic code.
+ * @return the assertions for the thread ID.
*/
- public AbstractStringAssert> code() {
- return Assertions.assertThat(actual.getCode());
+ public LongAssert threadId() {
+ return new LongAssert(actual.getThreadId());
}
/**
- * Perform an assertion on the diagnostic message, assuming a root locale.
+ * Get assertions for the thread name of the thread that reported the diagnostic. This may not be
+ * present in some situations.
*
- * @return an assertion to perform on the message, assuming a root locale.
+ * @return the assertions for the optional thread name.
*/
- public AbstractStringAssert> message() {
- return message(Locale.ROOT);
+ public OptionalAssert If no file is found, the assertion will apply to a null value.
- *
- * @param path the path to look for.
- * @return an assertion for the path of the matching file.
- */
- public OptionalPathAssert file(String path) {
- var actualFile = actual
- .findFile(path)
- .orElse(null);
-
- return OptionalPathAssert.assertThatPath(actual, path, actualFile);
- }
-
- /**
- * Create a new set of assertions for a path location manager.
- *
- * @param pathLocationManager the manager of paths to assert on for a specific location.
- * @return the assertions.
- */
- public static PathLocationManagerAssert assertThatLocation(
- PathLocationManager pathLocationManager
- ) {
- return new PathLocationManagerAssert(pathLocationManager);
- }
-}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceAssert.java
new file mode 100644
index 000000000..120a47d82
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceAssert.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.assertions;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.assertj.core.api.AbstractListAssert;
+
+/**
+ * Assertions for a list of {@link StackTraceElement stack trace frames}.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public final class StackTraceAssert
+ extends AbstractListAssert The line number may be empty if the method is a {@link #nativeMethod() native method}.
+ *
+ * @return the assertions for the line number.
+ */
+ public OptionalIntAssert lineNumber() {
+ // Null for irrelevant values is less surprising than a negative value.
+ return new OptionalIntAssert(
+ actual.getLineNumber() < 0 ? OptionalInt.empty() : OptionalInt.of(actual.getLineNumber())
+ ) {}.describedAs("line number %s", actual.getLineNumber());
+ }
+
+ /**
+ * Get assertions for the module name of the stack trace frame.
+ *
+ * The value may be null if not present.
+ *
+ * @return the assertions for the module name.
+ */
+ public StringAssert moduleName() {
+ return new StringAssert(actual.getModuleName());
+ }
+
+ /**
+ * Get assertions for the module version of the stack trace frame.
+ *
+ * The value may be null if not present.
+ *
+ * @return the assertions for the module version.
+ */
+ public StringAssert moduleVersion() {
+ return new StringAssert(actual.getModuleVersion());
+ }
+
+ /**
+ * Get assertions for the name of the classloader of the class in the stack trace frame.
+ *
+ * @return the assertions for the classloader name.
+ */
+ public StringAssert classLoaderName() {
+ return new StringAssert(actual.getClassLoaderName());
+ }
+
+ /**
+ * Get assertions for the class name of the stack trace frame.
+ *
+ * @return the assertions for the class name.
+ */
+ public StringAssert className() {
+ return new StringAssert(actual.getClassName());
+ }
+
+ /**
+ * Get assertions for the method name of the stack trace frame.
+ *
+ * @return the assertions for the method name.
+ */
+ public StringAssert methodName() {
+ return new StringAssert(actual.getMethodName());
+ }
+
+ /**
+ * Get assertions for whether the frame is for a native (JNI) method or not.
+ *
+ * @return the assertions for the method nativity.
+ */
+ public BooleanAssert nativeMethod() {
+ return new BooleanAssert(actual.isNativeMethod());
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceRepresentation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceRepresentation.java
new file mode 100644
index 000000000..fe6fd1a00
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceRepresentation.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.assertions;
+
+import java.util.List;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.assertj.core.presentation.Representation;
+
+/**
+ * Representation of a {@link List list} of {@link StackTraceElement stack trace frames}.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public final class StackTraceRepresentation implements Representation {
+
+ private static final StackTraceRepresentation INSTANCE
+ = new StackTraceRepresentation();
+
+ /**
+ * Get an instance of this stack trace representation.
+ *
+ * @return the instance.
+ */
+ public static StackTraceRepresentation getInstance() {
+ return INSTANCE;
+ }
+
+
+ private StackTraceRepresentation() {
+ // Nothing to see here, move along now!
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public String toStringOf(Object object) {
+ var trace = (List extends StackTraceElement>) object;
+ var builder = new StringBuilder("Stacktrace:");
+ for (var frame : trace) {
+ builder.append("\n\tat ").append(frame);
+ }
+ return builder.toString();
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AnnotationProcessorDiscovery.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AnnotationProcessorDiscovery.java
new file mode 100644
index 000000000..c0b99ba5e
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AnnotationProcessorDiscovery.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.compilers;
+
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * Mode for annotation processor discovery when no explicit processors are provided.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public enum AnnotationProcessorDiscovery {
+ /**
+ * Discovery is enabled, and will also scan any dependencies in the classpath or module path.
+ */
+ INCLUDE_DEPENDENCIES,
+
+ /**
+ * Discovery is enabled using the provided processor paths.
+ */
+ ENABLED,
+
+ /**
+ * Discovery is disabled.
+ */
+ DISABLED,
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilable.java
similarity index 54%
rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compiler.java
rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilable.java
index 3bd891216..70e5c64aa 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compiler.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilable.java
@@ -16,14 +16,13 @@
package io.github.ascopes.jct.compilers;
-import io.github.ascopes.jct.intern.IterableUtils;
-import io.github.ascopes.jct.paths.PathLocationRepository;
-import io.github.ascopes.jct.paths.RamPath;
+import io.github.ascopes.jct.paths.NioPath;
+import io.github.ascopes.jct.paths.PathLike;
+import io.github.ascopes.jct.utils.IterableUtils;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
-import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -46,7 +45,7 @@
* @since 0.0.1
*/
@API(since = "0.0.1", status = Status.EXPERIMENTAL)
-public interface Compiler This is mutable, and care should be taken if using this interface directly.
+ * The location must not be
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*
- * @return the path location repository.
- */
- PathLocationRepository getPathLocationRepository();
-
- // !!! BUG REGRESSION WARNING FOR THIS API !!!:
- // DO NOT REPLACE COLLECTION The path can be one of:
*
- * @param location the location to add paths to.
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- C addPaths(Location location, Collection extends Path> paths);
-
- /**
- * Add paths to the given location.
+ * The location must not be
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*
- * @param path1 the first path to add.
- * @param paths additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addClassOutputPaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.CLASS_OUTPUT, path1, paths);
- }
-
- /**
- * Add paths to the source output path.
+ * The path can be one of:
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSourceOutputPaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.SOURCE_OUTPUT, paths);
- }
-
- /**
- * Add paths to the source output path.
+ * The location must be
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addClassPaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.CLASS_PATH, path1, paths);
- }
-
- /**
- * Add paths to the source path.
+ * The path can be one of:
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSourcePaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.SOURCE_PATH, paths);
- }
-
- /**
- * Add paths to the source path.
+ * The location must be
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addAnnotationProcessorPaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, path1, paths);
- }
-
- /**
- * Add paths to the annotation processor path.
+ * The path can be one of:
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addAnnotationProcessorModulePaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, paths);
- }
-
- /**
- * Add paths to the annotation processor module path.
+ * The path can be one of:
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addPlatformClassPaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.PLATFORM_CLASS_PATH, path1, paths);
- }
-
- /**
- * Add paths to the module source path.
+ * Note: to add modules, consider using
+ * {@link #addModulePath(String, PathLike)} instead. The path can be one of:
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addUpgradeModulePaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.UPGRADE_MODULE_PATH, path1, paths);
- }
-
- /**
- * Add paths to the system module path.
+ * Note: to add modules, consider using
+ * {@link #addModulePath(String, Path)} instead. The path can be one of:
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addModulePaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.MODULE_PATH, path1, paths);
- }
-
- /**
- * Add paths to the patch module path.
+ * Note: to add regular packages, consider using
+ * {@link #addClassPath(PathLike)} instead. Note that this will take ownership of the path in the underlying file repository.
+ * The path can be one of:
*
- * @param location the location to add.
- * @param paths the in-memory directories to add.
- * @return this compiler object for further call chaining.
- */
- C addRamPaths(Location location, Collection extends RamPath> paths);
-
- /**
- * Add multiple in-memory directories to the paths for a given location.
+ * Note that this will take ownership of the path in the underlying file repository.
+ * Note: to add regular packages, consider using
+ * {@link #addClassPath(Path)} instead. Anything placed in here will be treated as a compilation unit by default.
*
- * @param path1 the first path to add.
- * @param paths additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addClassOutputRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.CLASS_OUTPUT, path1, paths);
- }
-
- /**
- * Add paths to the source output path.
+ * The path can be one of:
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSourceOutputRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.SOURCE_OUTPUT, paths);
- }
-
- /**
- * Add paths to the source output path.
+ * Note: to add modules, consider using
+ * {@link #addModuleSourcePath(String, PathLike)} instead. You will not be able to mix this
+ * method and that method together. Anything placed in here will be treated as a compilation unit by default.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSourceRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.SOURCE_PATH, paths);
- }
-
- /**
- * Add paths to the source path.
+ * The path can be one of:
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSourceRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.SOURCE_PATH, path1, paths);
- }
-
- /**
- * Add paths to the annotation processor path.
+ * Note: to add modules, consider using
+ * {@link #addModuleSourcePath(String, PathLike)} instead. You will not be able to mix this
+ * method and that method together. Anything placed in here will be treated as a compilation unit by default.
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addAnnotationProcessorModuleRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, path1, paths);
- }
-
- /**
- * Add paths to the platform class path.
+ * The path can be one of:
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addPlatformClassRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.PLATFORM_CLASS_PATH, paths);
- }
-
- /**
- * Add paths to the platform class path.
+ * Note: to add non-modules, consider using
+ * {@link #addSourcePath(PathLike)} instead. You will not be able to mix this
+ * method and that method together. Anything placed in here will be treated as a compilation unit by default.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addNativeHeaderOutputRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.NATIVE_HEADER_OUTPUT, paths);
- }
-
- /**
- * Add paths to the native header output path.
+ * The path can be one of:
*
- * @param path1 the first path to add.
- * @param paths additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addNativeHeaderOutputRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.NATIVE_HEADER_OUTPUT, path1, paths);
- }
-
- /**
- * Add paths to the module source path.
+ * Note: to add non-modules, consider using
+ * {@link #addSourcePath(Path)} instead. You will not be able to mix this
+ * method and that method together. This will be used for annotation processor discovery.
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addUpgradeModuleRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.UPGRADE_MODULE_PATH, path1, paths);
- }
-
- /**
- * Add paths to the system module path.
+ * The path can be one of:
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSystemModuleRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.SYSTEM_MODULES, paths);
- }
-
- /**
- * Add paths to the system module path.
+ * This will be used for annotation processor discovery.
+ *
+ * The path can be one of:
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
+ * This will be used for annotation processor discovery.
+ *
+ * The path can be one of:
+ *
+ * This will be used for annotation processor discovery.
+ *
+ * The path can be one of:
+ *
+ * This bypasses the discovery process of annotation processors provided in
- * {@link #addAnnotationProcessorPaths}.
+ * {@link #addAnnotationProcessorPath}.
*
* @param annotationProcessors the processors to invoke.
* @return this compiler object for further call chaining.
@@ -797,7 +560,7 @@ default C addAnnotationProcessorOptions(
* Add annotation processors to invoke.
*
* This bypasses the discovery process of annotation processors provided in
- * {@link #addAnnotationProcessorPaths}.
+ * {@link #addAnnotationProcessorPath}.
*
* @param annotationProcessor the first processor to invoke.
* @param annotationProcessors additional processors to invoke.
@@ -864,27 +627,6 @@ default C addRuntimeOptions(String runtimeOption, String... runtimeOptions) {
return addRuntimeOptions(IterableUtils.combineOneOrMore(runtimeOption, runtimeOptions));
}
- /**
- * Get the charset being used to read and write files with.
- *
- * Unless otherwise changed or specified, implementations should default to
- * {@link #DEFAULT_LOG_CHARSET}.
- *
- * @return the charset.
- */
- Charset getFileCharset();
-
- /**
- * Set the charset being used to read and write files with.
- *
- * Unless otherwise changed or specified, implementations should default to
- * {@link #DEFAULT_LOG_CHARSET}.
- *
- * @param fileCharset the charset to use.
- * @return this compiler for further call chaining.
- */
- C fileCharset(Charset fileCharset);
-
/**
* Determine whether verbose logging is enabled or not.
*
@@ -1003,6 +745,15 @@ default C addRuntimeOptions(String runtimeOption, String... runtimeOptions) {
*/
C failOnWarnings(boolean enabled);
+ /**
+ * Get the default release to use if no release or target version is specified.
+ *
+ * This can not be configured.
+ *
+ * @return the default release version to use.
+ */
+ String getDefaultRelease();
+
/**
* Get the current release version that is set, or an empty optional if left to the compiler
* default.
@@ -1314,43 +1065,43 @@ default C target(SourceVersion target) {
* Get the current file manager logging mode.
*
* Unless otherwise changed or specified, implementations should default to
- * {@link #DEFAULT_FILE_MANAGER_LOGGING}.
+ * {@link #DEFAULT_FILE_MANAGER_LOGGING_MODE}.
*
* @return the current file manager logging mode.
*/
- Logging getFileManagerLogging();
+ LoggingMode getFileManagerLoggingMode();
/**
* Set how to handle logging calls to underlying file managers.
*
* Unless otherwise changed or specified, implementations should default to
- * {@link #DEFAULT_FILE_MANAGER_LOGGING}.
+ * {@link #DEFAULT_FILE_MANAGER_LOGGING_MODE}.
*
- * @param fileManagerLogging the mode to use for file manager logging.
+ * @param fileManagerLoggingMode the mode to use for file manager logging.
* @return this compiler for further call chaining.
*/
- C fileManagerLogging(Logging fileManagerLogging);
+ C fileManagerLoggingMode(LoggingMode fileManagerLoggingMode);
/**
* Get the current diagnostic logging mode.
*
* Unless otherwise changed or specified, implementations should default to
- * {@link #DEFAULT_DIAGNOSTIC_LOGGING}.
+ * {@link #DEFAULT_DIAGNOSTIC_LOGGING_MODE}.
*
* @return the current diagnostic logging mode.
*/
- Logging getDiagnosticLogging();
+ LoggingMode getDiagnosticLoggingMode();
/**
* Set how to handle diagnostic capture.
*
* Unless otherwise changed or specified, implementations should default to
- * {@link #DEFAULT_DIAGNOSTIC_LOGGING}.
+ * {@link #DEFAULT_DIAGNOSTIC_LOGGING_MODE}.
*
- * @param diagnosticLogging the mode to use for diagnostic capture.
+ * @param diagnosticLoggingMode the mode to use for diagnostic capture.
* @return this compiler for further call chaining.
*/
- C diagnosticLogging(Logging diagnosticLogging);
+ C diagnosticLoggingMode(LoggingMode diagnosticLoggingMode);
/**
* Get how to perform annotation processor discovery.
@@ -1394,74 +1145,4 @@ default C target(SourceVersion target) {
*/
R compile();
- /**
- * Options for how to handle logging on special internal components.
- *
- * @author Ashley Scopes
- * @since 0.0.1
- */
- @API(since = "0.0.1", status = Status.EXPERIMENTAL)
- enum Logging {
- /**
- * Enable basic logging.
- */
- ENABLED,
-
- /**
- * Enable logging and include stacktraces in the logs for each entry.
- */
- STACKTRACES,
-
- /**
- * Do not log anything.
- */
- DISABLED,
- }
-
- /**
- * Mode for annotation processor discovery when no explicit processors are provided.
- *
- * @author Ashley Scopes
- * @since 0.0.1
- */
- @API(since = "0.0.1", status = Status.EXPERIMENTAL)
- enum AnnotationProcessorDiscovery {
- /**
- * Discovery is enabled, and will also scan any dependencies in the classpath or module path.
- */
- INCLUDE_DEPENDENCIES,
-
- /**
- * Discovery is enabled using the provided processor paths.
- */
- ENABLED,
-
- /**
- * Discovery is disabled.
- */
- DISABLED,
- }
-
- /**
- * Function representing a configuration operation that can be applied to a compiler.
- *
- * This can allow encapsulating common configuration logic across tests into a single place.
- *
- * @param This can allow encapsulating common configuration logic across tests into a single place.
+ *
+ * @param This is bundled with this toolkit.
- *
- * Note: the ECJ implementation does not currently work correctly with
- * JPMS modules.
- *
- * @return the ECJ instance.
- */
- public static Compiler, ?> ecj() {
- return new EcjCompiler(new EclipseCompiler());
- }
-}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/LoggingMode.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/LoggingMode.java
new file mode 100644
index 000000000..4ef19bbf3
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/LoggingMode.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.compilers;
+
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * Options for how to handle logging on special internal components.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public enum LoggingMode {
+ /**
+ * Enable basic logging.
+ */
+ ENABLED,
+
+ /**
+ * Enable logging and include stacktraces in the logs for each entry.
+ */
+ STACKTRACES,
+
+ /**
+ * Do not log anything.
+ */
+ DISABLED,
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java
index 0f2747783..c2f50f516 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java
@@ -16,11 +16,12 @@
package io.github.ascopes.jct.compilers;
-import static io.github.ascopes.jct.intern.IterableUtils.nonNullUnmodifiableList;
-import static io.github.ascopes.jct.intern.IterableUtils.nonNullUnmodifiableSet;
+import static io.github.ascopes.jct.utils.IterableUtils.nonNullUnmodifiableList;
+import static io.github.ascopes.jct.utils.IterableUtils.nonNullUnmodifiableSet;
import static java.util.Objects.requireNonNull;
-import io.github.ascopes.jct.paths.PathLocationRepository;
+import io.github.ascopes.jct.jsr199.FileManager;
+import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic;
import java.util.List;
import java.util.Set;
import javax.tools.JavaFileObject;
@@ -42,7 +43,7 @@ public final class SimpleCompilation implements Compilation {
private final List Logging will be applied to this via
- * {@link #applyLoggingToFileManager(Compiler, JavaFileManager)}, which will be handled by
- * {@link #compile(Compiler, JavaCompiler, FlagBuilder)}.
+ * LoggingMode will be applied to this via
+ * {@link #applyLoggingToFileManager(Compilable, FileManager)}, which will be handled by
+ * {@link #compile(Compilable, SimpleFileManagerTemplate, JavaCompiler, FlagBuilder)}.
*
* @param compiler the compiler to use.
* @return the file manager to use.
*/
- protected JavaFileManager buildJavaFileManager(A compiler) {
- ensureClassOutputPathExists(compiler);
- registerClassPath(compiler);
- registerPlatformClassPath(compiler);
- registerSystemModulePath(compiler);
- return new PathJavaFileManager(compiler.getPathLocationRepository());
+ protected FileManager buildFileManager(A compiler, SimpleFileManagerTemplate template) {
+ var release = compiler.getRelease()
+ .or(compiler::getTarget)
+ .orElseGet(compiler::getDefaultRelease);
+
+ var fileManager = template.createFileManager(release);
+ ensureClassOutputPathExists(fileManager);
+ registerClassPath(compiler, fileManager);
+ registerPlatformClassPath(compiler, fileManager);
+ registerSystemModulePath(compiler, fileManager);
+ registerAnnotationProcessorPaths(compiler, fileManager);
+ return fileManager;
}
/**
@@ -190,26 +200,26 @@ protected List extends JavaFileObject> findCompilationUnits(
/**
* Apply the logging level to the file manager provided by
- * {@link #buildJavaFileManager(Compiler)}.
+ * {@link #buildFileManager(Compilable, SimpleFileManagerTemplate)}.
*
* The default implementation will wrap the given {@link JavaFileManager} in a
- * {@link LoggingJavaFileManagerProxy} if the {@link Compiler#getFileManagerLogging()} field is
- * not set to {@link Logging#DISABLED}. In the latter scenario, the input
+ * {@link LoggingFileManagerProxy} if the {@link Compilable#getFileManagerLoggingMode()} field is
+ * not set to {@link LoggingMode#DISABLED}. In the latter scenario, the input
* will be returned to the caller with no other modifications.
*
* @param compiler the compiler to use.
* @param fileManager the file manager to apply to.
* @return the file manager to use for future operations.
*/
- protected JavaFileManager applyLoggingToFileManager(
+ protected FileManager applyLoggingToFileManager(
A compiler,
- JavaFileManager fileManager
+ FileManager fileManager
) {
- switch (compiler.getFileManagerLogging()) {
+ switch (compiler.getFileManagerLoggingMode()) {
case STACKTRACES:
- return LoggingJavaFileManagerProxy.wrap(fileManager, true);
+ return LoggingFileManagerProxy.wrap(fileManager, true);
case ENABLED:
- return LoggingJavaFileManagerProxy.wrap(fileManager, false);
+ return LoggingFileManagerProxy.wrap(fileManager, false);
default:
return fileManager;
}
@@ -224,11 +234,11 @@ protected JavaFileManager applyLoggingToFileManager(
* @return the diagnostics listener.
*/
protected TracingDiagnosticListener File manager creation is deferred until as late as possible as to enable the specification of
+ * the version to use when opening JARs that may be multi-release compatible. We have to do this to
+ * ensure the behaviour for opening JARs matches the release version the code is compiled against.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public class SimpleFileManagerTemplate {
+
+ private final Map Windows cannot create symbolic links without root, but can create hard links.
- *
- * Hard links will often not work across separate mount points on Linux.
- *
- * Some operating systems will support neither hard nor symbolic links, so we fall back
- * to copying the target file to the link location in these cases.
- *
- * @author Ashley Scopes
- * @since 0.0.1
- */
-@API(since = "0.0.1", status = Status.INTERNAL)
-public final class PlatformLinkStrategy {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(PlatformLinkStrategy.class);
- private final Call impl;
- private final String name;
-
- /**
- * Initialize this strategy.
- */
- public PlatformLinkStrategy(Properties systemProperties) {
- var windows = systemProperties
- .getProperty("os.name", "")
- .toLowerCase(Locale.ROOT)
- .startsWith("windows");
-
- if (windows) {
- // Windows cannot create symbolic links without root.
- impl = Files::createLink;
- name = "hard link";
- } else {
- // /tmp on UNIX is usually a separate device, so hard links won't work.
- impl = Files::createSymbolicLink;
- name = "symbolic link";
- }
- }
-
- /**
- * Attempt to create a link, falling back to just copying the file if this is not possible.
- *
- * @param link the link to create.
- * @param target the target to link to.
- * @return the path to the created link or file.
- * @throws IOException if something goes wrong.
- */
- public Path createLinkOrCopy(Path link, Path target) throws IOException {
- try {
- var result = impl.createLink(link, target);
- LOGGER.trace("Created {} from {} to {}", name, link, target);
- return result;
- } catch (UnsupportedOperationException | FileSystemException ex) {
- LOGGER.trace(
- "Failed to create {} from {} to {}, falling back to copying files",
- name,
- link,
- target,
- ex
- );
- return Files.copy(target, link);
- }
- }
-
- @FunctionalInterface
- private interface Call {
-
- /**
- * Create the link.
- *
- * @param link the link to create.
- * @param target the target to link to.
- * @return the link.
- * @throws IOException if the link fails to be created.
- */
- Path createLink(Path link, Path target) throws IOException;
- }
-}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/FileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/FileManager.java
new file mode 100644
index 000000000..2b31dd1be
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/FileManager.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.jsr199;
+
+import io.github.ascopes.jct.jsr199.containers.ModuleOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.OutputOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.PackageOrientedContainerGroup;
+import io.github.ascopes.jct.paths.PathLike;
+import java.util.Optional;
+import javax.tools.JavaFileManager;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * Extension around a {@link JavaFileManager} that allows adding of {@link PathLike} objects to the
+ * manager.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public interface FileManager extends JavaFileManager {
+
+ /**
+ * Add a path to a given location.
+ *
+ * @param location the location to use.
+ * @param path the path to add.
+ */
+ void addPath(Location location, PathLike path);
+
+ /**
+ * Register an empty container for the given location to indicate to the compiler that the feature
+ * exists, but has no configured paths.
+ *
+ * This is needed to coerce the behaviour for annotation processing in some cases.
+ *
+ * If the location already exists, then do not do anything.
+ *
+ * @param location the location to apply an empty container for.
+ */
+ void ensureEmptyLocationExists(Location location);
+
+ /**
+ * Copy all containers from the first location to the second location.
+ *
+ * @param from the first location.
+ * @param to the second location.
+ */
+ void copyContainers(Location from, Location to);
+
+ /**
+ * Get the container group for the given package-oriented location.
+ *
+ * @param location the package oriented location.
+ * @return the container group, or an empty optional if one does not exist.
+ */
+ Optional This is useful for diagnosing difficult-to-find errors being produced by {@code javac}
@@ -40,14 +40,14 @@
* @since 0.0.1
*/
@API(since = "0.0.1", status = Status.EXPERIMENTAL)
-public class LoggingJavaFileManagerProxy implements InvocationHandler {
+public class LoggingFileManagerProxy implements InvocationHandler {
- private static final Logger LOGGER = LoggerFactory.getLogger(LoggingJavaFileManagerProxy.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFileManagerProxy.class);
- private final JavaFileManager inner;
+ private final FileManager inner;
private final boolean stackTraces;
- private LoggingJavaFileManagerProxy(JavaFileManager inner, boolean stackTraces) {
+ private LoggingFileManagerProxy(FileManager inner, boolean stackTraces) {
this.inner = inner;
this.stackTraces = stackTraces;
}
@@ -89,9 +89,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
LOGGER.info("<<< {}({}) returns {}", method.getName(), argsStr, result);
}
return result;
- } catch (Throwable ex) {
- LOGGER.error("!!! {}({}) throws exception", method.getName(), argsStr, ex);
- throw ex;
+ } catch (InvocationTargetException ex) {
+ var cause = ex.getCause() == null
+ ? ex
+ : ex.getCause();
+
+ LOGGER.error("!!! {}({}) throws exception", method.getName(), argsStr, cause);
+ throw cause;
}
}
@@ -104,18 +108,18 @@ public String toString() {
}
/**
- * Wrap the given {@link JavaFileManager} in a proxy that logs any calls.
+ * Wrap the given {@link FileManager} in a proxy that logs any calls.
*
* @param manager the manager to wrap.
* @param stackTraces {@code true} to dump stacktraces on each interception, or {@code false} to
* omit them.
- * @return the proxy {@link JavaFileManager} to use.
+ * @return the proxy {@link FileManager} to use.
*/
- public static JavaFileManager wrap(JavaFileManager manager, boolean stackTraces) {
- return (JavaFileManager) Proxy.newProxyInstance(
- JavaFileManager.class.getClassLoader(),
- new Class>[]{JavaFileManager.class},
- new LoggingJavaFileManagerProxy(manager, stackTraces)
+ public static FileManager wrap(FileManager manager, boolean stackTraces) {
+ return (FileManager) Proxy.newProxyInstance(
+ FileManager.class.getClassLoader(),
+ new Class>[]{FileManager.class},
+ new LoggingFileManagerProxy(manager, stackTraces)
);
}
}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ModuleLocation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/ModuleLocation.java
similarity index 97%
rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ModuleLocation.java
rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/ModuleLocation.java
index 7cba80ea0..74e0c0229 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ModuleLocation.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/ModuleLocation.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package io.github.ascopes.jct.paths;
+package io.github.ascopes.jct.jsr199;
-import io.github.ascopes.jct.intern.StringUtils;
+import io.github.ascopes.jct.utils.StringUtils;
import java.util.Objects;
import javax.tools.JavaFileManager.Location;
import org.apiguardian.api.API;
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObject.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/PathFileObject.java
similarity index 66%
rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObject.java
rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/PathFileObject.java
index 0d3675f63..29854f110 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObject.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/PathFileObject.java
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package io.github.ascopes.jct.paths;
+package io.github.ascopes.jct.jsr199;
import static java.util.Objects.requireNonNull;
-import io.github.ascopes.jct.intern.StringUtils;
+import io.github.ascopes.jct.utils.FileUtils;
+import io.github.ascopes.jct.utils.StringUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
@@ -32,10 +33,11 @@
import java.io.Writer;
import java.net.URI;
import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.lang.model.element.Modifier;
@@ -48,61 +50,62 @@
import org.slf4j.LoggerFactory;
/**
- * File object that can be used with paths, and that is bound to a specific charset.
- *
- * All inputs and outputs are buffered automatically.
+ * A very simple {@link JavaFileObject} that points to a path on some {@link FileSystem} somewhere.
*
* @author Ashley Scopes
* @since 0.0.1
*/
@API(since = "0.0.1", status = Status.EXPERIMENTAL)
-public class PathJavaFileObject implements JavaFileObject {
+public class PathFileObject implements JavaFileObject {
- private static final Logger LOGGER = LoggerFactory.getLogger(PathJavaFileObject.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(PathFileObject.class);
private static final long NOT_MODIFIED = 0L;
private final Location location;
- private final Path path;
+ private final Path root;
+ private final Path relativePath;
+ private final Path fullPath;
private final String name;
private final URI uri;
private final Kind kind;
- private final Charset charset;
/**
- * Initialize the file object.
+ * Initialize this file object.
*
- * @param location the location of the object.
- * @param path the path to the object.
- * @param name the given name to use to identify the file.
- * @param kind the kind of the file.
- * @param charset the charset to use to read the file textually. If the file is binary then this
- * can be any value.
+ * @param root the root directory that the path is a package within.
+ * @param relativePath the path to point to.
*/
- public PathJavaFileObject(Location location, Path path, String name, Kind kind, Charset charset) {
- this.location = requireNonNull(location);
- this.path = requireNonNull(path);
- this.name = requireNonNull(name);
- uri = path.toUri();
- this.kind = requireNonNull(kind);
- this.charset = requireNonNull(charset);
+ public PathFileObject(Location location, Path root, Path relativePath) {
+ this.location = requireNonNull(location, "location");
+ this.root = requireNonNull(root, "root");
+ this.relativePath = root.relativize(requireNonNull(relativePath, "relativePath"));
+ fullPath = root.resolve(relativePath);
+ name = relativePath.toString();
+ uri = fullPath.toUri();
+ kind = FileUtils.pathToKind(relativePath);
}
- /**
- * Get the location of the file.
- *
- * @return the location.
- */
- public Location getLocation() {
- return location;
+ @Override
+ public boolean delete() {
+ try {
+ return Files.deleteIfExists(relativePath);
+ } catch (IOException ex) {
+ LOGGER.warn("Ignoring error deleting {}", uri, ex);
+ return false;
+ }
}
- /**
- * Get the path to the file.
- *
- * @return the path.
- */
- public Path getPath() {
- return path;
+ @Override
+ public Modifier getAccessLevel() {
+ // Null implies that the access level is unknown.
+ return null;
+ }
+
+ @Override
+ public String getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ return decoder(ignoreEncodingErrors)
+ .decode(ByteBuffer.wrap(Files.readAllBytes(fullPath)))
+ .toString();
}
@Override
@@ -111,36 +114,56 @@ public Kind getKind() {
}
@Override
- public boolean isNameCompatible(String simpleName, Kind kind) {
- return path.getFileName().toString().endsWith(simpleName + kind.extension);
+ public long getLastModified() {
+ try {
+ return Files.getLastModifiedTime(relativePath).toMillis();
+ } catch (IOException ex) {
+ LOGGER.warn("Ignoring error reading last modified time for {}", uri, ex);
+ return NOT_MODIFIED;
+ }
+ }
+
+ /**
+ * Get the location of this path file object.
+ *
+ * @return the location.
+ */
+ public Location getLocation() {
+ return location;
}
@Override
- public NestingKind getNestingKind() {
- // We do not have access to this info, so return null to
- // indicate this to JSR-199.
- return null;
+ public String getName() {
+ return name;
}
@Override
- public Modifier getAccessLevel() {
- // We do not have access to this info, so return null to
- // indicate this to JSR-199.
+ public NestingKind getNestingKind() {
+ // Null implies that the nesting kind is unknown.
return null;
}
- @Override
- public URI toUri() {
- // TODO(ascopes): mitigate bug where URI from path in JAR starts with
- // jar://file:/// rather than jar:///, causing filesystem resolution
- // to fail. This might be an issue if JARs are added from non-root file
- // systems though, so I don't know the best way of working around this.
- return uri;
+ /**
+ * Get the full path of this file object.
+ *
+ * @return the full path.
+ */
+ public Path getFullPath() {
+ return fullPath;
+ }
+
+ /**
+ * Get the relative path of this file object.
+ *
+ * @return the path of this file object.
+ */
+ public Path getRelativePath() {
+ return relativePath;
}
@Override
- public String getName() {
- return name;
+ public boolean isNameCompatible(String simpleName, Kind kind) {
+ return relativePath.getFileName().toString().equals(simpleName + kind.extension);
}
@Override
@@ -158,55 +181,29 @@ public BufferedReader openReader(boolean ignoreEncodingErrors) throws IOExceptio
return new BufferedReader(openUnbufferedReader(ignoreEncodingErrors));
}
- @Override
- public String getCharContent(boolean ignoreEncodingErrors) throws IOException {
- return decoder(ignoreEncodingErrors)
- .decode(ByteBuffer.wrap(Files.readAllBytes(path)))
- .toString();
- }
-
@Override
public BufferedWriter openWriter() throws IOException {
return new BufferedWriter(openUnbufferedWriter());
}
@Override
- public long getLastModified() {
- try {
- return Files.getLastModifiedTime(path).toMillis();
- } catch (IOException ex) {
- LOGGER.warn("Ignoring error reading last modified time for {}", uri, ex);
- return NOT_MODIFIED;
- }
- }
-
- @Override
- public boolean delete() {
- try {
- return Files.deleteIfExists(path);
- } catch (IOException ex) {
- LOGGER.warn("Ignoring error deleting {}", uri, ex);
- return false;
- }
+ public URI toUri() {
+ return uri;
}
@Override
public String toString() {
- return getClass().getSimpleName()
- + "{location=" + StringUtils.quoted(location.getName()) + ", "
- + "uri=" + StringUtils.quoted(uri) + ", "
- + "kind=" + kind
- + "}";
+ return getClass().getSimpleName() + "{uri=" + StringUtils.quoted(uri) + "}";
}
private InputStream openUnbufferedInputStream() throws IOException {
- return Files.newInputStream(path);
+ return Files.newInputStream(fullPath);
}
private OutputStream openUnbufferedOutputStream() throws IOException {
// Ensure parent directories exist first.
- Files.createDirectories(path.getParent());
- return Files.newOutputStream(path);
+ Files.createDirectories(fullPath.getParent());
+ return Files.newOutputStream(fullPath);
}
private Reader openUnbufferedReader(boolean ignoreEncodingErrors) throws IOException {
@@ -222,14 +219,14 @@ private CharsetDecoder decoder(boolean ignoreEncodingErrors) {
? CodingErrorAction.IGNORE
: CodingErrorAction.REPORT;
- return charset
+ return StandardCharsets.UTF_8
.newDecoder()
.onUnmappableCharacter(action)
.onMalformedInput(action);
}
private CharsetEncoder encoder() {
- return charset
+ return StandardCharsets.UTF_8
.newEncoder()
.onUnmappableCharacter(CodingErrorAction.REPORT)
.onMalformedInput(CodingErrorAction.REPORT);
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/SimpleFileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/SimpleFileManager.java
new file mode 100644
index 000000000..a7fa323e4
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/SimpleFileManager.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.jsr199;
+
+import static java.util.Objects.requireNonNull;
+
+import io.github.ascopes.jct.jsr199.containers.ContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.ModuleOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.OutputOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.PackageOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.SimpleModuleOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.SimpleOutputOrientedContainerGroup;
+import io.github.ascopes.jct.jsr199.containers.SimplePackageOrientedContainerGroup;
+import io.github.ascopes.jct.paths.PathLike;
+import io.github.ascopes.jct.paths.SubPath;
+import io.github.ascopes.jct.utils.Nullable;
+import java.io.IOException;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * Simple implementation of a {@link FileManager}.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public class SimpleFileManager implements FileManager {
+
+ private final String release;
+ private final Map This mechanism enables the ability to have locations with more than one path in them,
+ * which is needed to facilitate the Java compiler's distributed class path, module handling, and
+ * other important features.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public abstract class AbstractPackageOrientedContainerGroup
+ implements PackageOrientedContainerGroup {
+
+ private static final Set If the file does not exist, an empty optional is returned.
+ *
+ * @param packageName the package name of the file.
+ * @param relativeName the relative name of the file in the package.
+ * @return the file object, or an empty optional if it does not exist.
+ */
+ Optional If the container is read-only, an empty optional is returned.
+ *
+ * @param packageName the package name of the file.
+ * @param relativeName the relative name of the file in the package.
+ * @return the file object, or an empty optional if this container is read-only.
+ */
+ Optional If the file does not exist, an empty optional is returned.
+ *
+ * @param className the binary name of the class to open.
+ * @param kind the kind of file to open.
+ * @return the file object, or an empty optional if it does not exist.
+ */
+ Optional If the container is read-only, an empty optional is returned.
+ *
+ * @param className the binary name of the class to open.
+ * @param kind the kind of file to open.
+ * @return the file object, or an empty optional if this container is read-only.
+ */
+ Optional If the resource does not exist, then return an empty optional.
+ *
+ * @param resourcePath the path to the resource.
+ * @return the URL to the resource if it exists, or an empty optional if it does not exist.
+ * @throws IOException if an IO error occurs looking for the resource.
+ */
+ Optional Note that adding additional containers to this group after accessing this class loader
+ * may result in the class loader being destroyed or re-created.
+ *
+ * @return the class loader.
+ */
+ ClassLoader getClassLoader();
+
+ /**
+ * Get the location of this container group.
+ *
+ * @return the location.
+ */
+ Location getLocation();
+
+ /**
+ * Get a service loader for the given service class.
+ *
+ * @param service the service class to get.
+ * @param Unlike the regular JAR file system provider, more than one of these
+ * containers can exist pointing to the same physical JAR at once without concurrency issues
+ * occurring.
+ *
+ * The JAR will be opened lazily when needed, and then kept open until {@link #close() closed}
+ * explicitly.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public final class JarContainer implements Container {
+
+ private final Location location;
+ private final PathLike jarPath;
+ private final String release;
+ private final Lazy These modules can be accessed by their module name. Each one holds a separate group of
+ * containers, and has the ability to produce a custom class loader.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public interface ModuleOrientedContainerGroup extends ContainerGroup {
+
+ /**
+ * Add a container to this group.
+ *
+ * @param module the module that the container is for.
+ * @param container the container to add.
+ */
+ void addModule(String module, Container container);
+
+ /**
+ * Add a path to this group for a module.
+ *
+ * @param module the name of the module that this is for.
+ * @param path the path to add.
+ */
+ void addModule(String module, PathLike path);
+
+ /**
+ * Get the {@link PackageOrientedContainerGroup} for a given module name, creating it if it does
+ * not yet exist.
+ *
+ * @param moduleName the module name to look up.
+ * @return the container group.
+ */
+ PackageOrientedContainerGroup forModule(String moduleName);
+
+ /**
+ * Get the module-oriented location.
+ *
+ * @return the module-oriented location.
+ */
+ @Override
+ Location getLocation();
+
+ /**
+ * Get all locations that are modules.
+ *
+ * @return the locations that are modules.
+ */
+ List These can behave as if they are module-oriented, or non-module-oriented.
+ * It is down to the implementation to mediate access between modules and their files.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public interface OutputOrientedContainerGroup
+ extends PackageOrientedContainerGroup, ModuleOrientedContainerGroup {
+
+ /**
+ * Get the output-oriented location.
+ *
+ * @return the output-oriented location.
+ */
+ @Override
+ Location getLocation();
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/PackageOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/PackageOrientedContainerGroup.java
new file mode 100644
index 000000000..cd386298e
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/PackageOrientedContainerGroup.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.jsr199.containers;
+
+import io.github.ascopes.jct.jsr199.PathFileObject;
+import io.github.ascopes.jct.paths.PathLike;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * Interface describing a group of containers.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public interface PackageOrientedContainerGroup extends ContainerGroup {
+
+ /**
+ * Add a container to this group.
+ *
+ * @param container the container to add.
+ */
+ void addPackage(Container container);
+
+ /**
+ * Add a path to this group.
+ *
+ * Note that this will destroy the {@link #getClassLoader() classloader} if one is already
+ * allocated.
+ *
+ * @param path the path to add.
+ */
+ void addPackage(PathLike path);
+
+ /**
+ * Find the first occurrence of a given path to a file.
+ *
+ * @param path the path to the file to find.
+ * @return the first occurrence of the path in this group, or an empty optional if not found.
+ */
+ Optional This will return an empty optional if no file is found.
+ *
+ * @param packageName the package name of the file to read.
+ * @param relativeName the relative name of the file to read.
+ * @return the file object, or an empty optional if the file is not found.
+ */
+ Optional This will attempt to write to the first writeable path in this group. An empty optional
+ * will be returned if no writeable paths exist in this group.
+ *
+ * @param packageName the name of the package the file is in.
+ * @param relativeName the relative name of the file within the package.
+ * @return the {@link FileObject} to write to, or an empty optional if this group has no paths
+ * that can be written to.
+ */
+ Optional This will attempt to write to the first writeable path in this group. An empty optional
+ * will be returned if no writeable paths exist in this group.
+ *
+ * @param className the binary name of the class to read.
+ * @param kind the kind of file to read.
+ * @return the {@link JavaFileObject} to write to, or an empty optional if this group has no paths
+ * that can be written to.
+ */
+ Optional This will attempt to write to the first writeable path in this group. An empty optional
+ * will be returned if no writeable paths exist in this group.
+ *
+ * @param className the name of the class.
+ * @param kind the kind of the class file.
+ * @return the {@link JavaFileObject} to write to, or an empty optional if this group has no paths
+ * that can be written to.
+ */
+ Optional These can contain packages and modules of packages together, and thus
+ * are slightly more complicated internally as a result.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public class SimpleOutputOrientedContainerGroup
+ extends AbstractPackageOrientedContainerGroup
+ implements OutputOrientedContainerGroup {
+
+ private final Location location;
+ private final Map This mechanism enables the ability to have locations with more than one path in them,
+ * which is needed to facilitate the Java compiler's distributed class path, module handling, and
+ * other important features.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public class SimplePackageOrientedContainerGroup extends AbstractPackageOrientedContainerGroup {
+
+ private final Location location;
+
+ /**
+ * Initialize this group.
+ *
+ * @param location the location of the group.
+ * @param release the release version to use for handling {@code Multi-Release} JARs in this
+ * location.
+ */
+ public SimplePackageOrientedContainerGroup(Location location, String release) {
+ super(release);
+
+ this.location = requireNonNull(location, "location");
+
+ if (location.isOutputLocation()) {
+ throw new UnsupportedOperationException(
+ "Cannot use output locations such as "
+ + StringUtils.quoted(location.getName())
+ + " with this container"
+ );
+ }
+
+ if (location.isModuleOrientedLocation()) {
+ throw new UnsupportedOperationException(
+ "Cannot use module-oriented locations such as "
+ + StringUtils.quoted(location.getName())
+ + " with this container"
+ );
+ }
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/package-info.java
new file mode 100644
index 000000000..d2f23b8a1
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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.
+ */
+
+/**
+ * Implementations of containers and container-groups, which provide a means of mapping a
+ * {@link javax.tools.JavaFileManager file manager} to a group of distributed paths in multiple
+ * {@link javax.tools.JavaFileManager.Location locations}.
+ */
+package io.github.ascopes.jct.jsr199.containers;
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ForwardingDiagnostic.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/ForwardingDiagnostic.java
similarity index 81%
rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ForwardingDiagnostic.java
rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/ForwardingDiagnostic.java
index daff8f468..2bf40be7b 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ForwardingDiagnostic.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/ForwardingDiagnostic.java
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package io.github.ascopes.jct.compilers;
+package io.github.ascopes.jct.jsr199.diagnostics;
+import static java.util.Objects.requireNonNull;
+
+import io.github.ascopes.jct.utils.Nullable;
import java.util.Locale;
-import java.util.Objects;
import javax.tools.Diagnostic;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
@@ -43,7 +45,7 @@ public abstract class ForwardingDiagnostic Note: this representation may vary depending on the compiler that
+ * initialized it. Setting this to true will skip any versions of the compiler that do not support JPMS
+ * modules.
+ *
+ * @return {@code true} if we need to support modules, or {@code false} if we do not.
+ */
+ boolean modules() default false;
+
+ /**
+ * Argument provider for the {@link EcjCompilers} annotation.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+ @API(since = "0.0.1", status = Status.INTERNAL)
+ final class EcjCompilersProvider extends AbstractCompilersProvider implements
+ AnnotationConsumer Setting this to true will skip any versions of the compiler that do not support JPMS
+ * modules.
+ *
+ * @return {@code true} if we need to support modules, or {@code false} if we do not.
+ */
+ boolean modules() default false;
+
+ /**
+ * Argument provider for the {@link JavacCompilers} annotation.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+ @API(since = "0.0.1", status = Status.INTERNAL)
+ final class JavacCompilersProvider
+ extends AbstractCompilersProvider
+ implements AnnotationConsumer This charset can be adjusted once initialized as needed.
- *
- * @author Ashley Scopes
- * @since 0.0.1
- */
-public class PathJavaFileObjectFactory {
-
- // Determine this on startup to allow inclusion of new extensions that may be added in the
- // future, potentially.
- private static final SortedMap Not only does this enable us to have different types of path with varying behaviour, but
+ * we can also use this to enforce that other references related to the internal path are kept alive
+ * for as long as the path-like object itself is kept alive.
+ *
+ * This becomes very useful for {@link RamPath}, which keeps a RAM-based {@link FileSystem}
+ * alive until it is garbage collected, or the {@link RamPath#close()} operation is called. The
+ * mechanism enables cleaning up of resources implicitly without resource-tidying logic polluting
+ * the user's test cases.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public interface PathLike {
+
+ /**
+ * Get the {@link Path Java NIO Path} for this path-like object.
+ *
+ * @return the path.
+ */
+ Path getPath();
+
+ /**
+ * Get a URI representation of this path-like object.
+ *
+ * @return the URI.
+ */
+ URI getUri();
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationManager.java
deleted file mode 100644
index b2c7a3e4c..000000000
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationManager.java
+++ /dev/null
@@ -1,678 +0,0 @@
-/*
- * Copyright (C) 2022 Ashley Scopes
- *
- * Licensed 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 io.github.ascopes.jct.paths;
-
-import static io.github.ascopes.jct.intern.IoExceptionUtils.uncheckedIo;
-import static io.github.ascopes.jct.intern.IoExceptionUtils.wrapWithUncheckedIoException;
-import static java.util.Objects.requireNonNull;
-
-import io.github.ascopes.jct.intern.AsyncResourceCloser;
-import io.github.ascopes.jct.intern.Lazy;
-import io.github.ascopes.jct.intern.PlatformLinkStrategy;
-import io.github.ascopes.jct.intern.RecursiveDeleter;
-import io.github.ascopes.jct.intern.StringSlicer;
-import io.github.ascopes.jct.intern.StringUtils;
-import java.io.Closeable;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.lang.module.ModuleFinder;
-import java.lang.ref.Cleaner;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystemNotFoundException;
-import java.nio.file.FileVisitOption;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.spi.FileSystemProvider;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.ServiceLoader;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.stream.Stream;
-import javax.tools.FileObject;
-import javax.tools.JavaFileManager.Location;
-import javax.tools.JavaFileObject;
-import javax.tools.JavaFileObject.Kind;
-import org.apiguardian.api.API;
-import org.apiguardian.api.API.Status;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-/**
- * Manager of paths for a specific compiler location.
- *
- * This provides access to file objects within any of the paths, as well as the ability
- * to construct classloaders for the paths as needed.
- *
- * @author Ashley Scopes
- * @since 0.0.1
- */
-@API(since = "0.0.1", status = Status.EXPERIMENTAL)
-public class PathLocationManager implements Iterable This will destroy the existing classloader, if it already exists.
- *
- * @param path the path to add.
- */
- public void addPath(Path path) {
- LOGGER.debug(
- "Adding paths {} to {} for location {}",
- path,
- getClass().getSimpleName(),
- location
- );
- registerPath(path);
- destroyClassLoader();
- }
-
- // !!! BUG REGRESSION WARNING FOR THIS API !!!:
- // DO NOT REPLACE COLLECTION This will destroy the existing classloader, if it already exists.
- *
- * @param paths the paths to add.
- */
- public void addPaths(Collection extends Path> paths) {
- // Don't expand paths if this was incorrectly called with a single path, since paths themselves
- // are iterables of paths.
- LOGGER.debug(
- "Adding paths {} to {} for location {}",
- paths,
- getClass().getSimpleName(),
- location
- );
- for (var path : paths) {
- registerPath(path);
- }
- destroyClassLoader();
- }
-
- /**
- * Add an in-memory directory to the manager, if it has not already been added.
- *
- * This will destroy the existing classloader, if it already exists.
- *
- * @param path the path to add.
- */
- public void addRamPath(RamPath path) {
- LOGGER.debug(
- "Registering {} to {} for location {}",
- path,
- getClass().getSimpleName(),
- location
- );
- registerPath(path.getPath());
-
- // Keep the reference alive.
- inMemoryDirectories.add(path);
- destroyClassLoader();
- }
-
- /**
- * Add in-memory directories to the manager, if it has not already been added.
- *
- * This will destroy the existing classloader, if it already exists.
- *
- * @param paths the paths to add.
- */
- public void addRamPaths(Collection extends RamPath> paths) {
- LOGGER.debug(
- "Registering {} to {} for location {}",
- paths,
- getClass().getSimpleName(),
- location
- );
- for (var path : paths) {
- registerPath(path.getPath());
- // Keep the reference alive.
- inMemoryDirectories.add(path);
- }
- destroyClassLoader();
- }
-
- /**
- * Determine if this manager contains the given file object anywhere.
- *
- * @param fileObject the file object to look for.
- * @return {@code true} if present, {@code false} otherwise.
- */
- public boolean contains(FileObject fileObject) {
- // TODO(ascopes): can we get non-path file objects here?
- var path = ((PathJavaFileObject) fileObject).getPath();
-
- // While we could just return `Files.isRegularFile` from the start,
- // we need to make sure the path is one of the roots in the location.
- // Otherwise, we could give a false-positive.
- for (var root : roots) {
- if (path.startsWith(root)) {
- return Files.isRegularFile(path);
- }
- }
-
- return false;
- }
-
- /**
- * Get the full path for a given string path to a file by finding the first occurrence where the
- * given path exists as a file.
- *
- * @param path the path to resolve.
- * @return the first full path that ends with the given path that is an existing file, or an empty
- * optional if no results were found.
- * @throws IllegalArgumentException if an absolute-style path is provided.
- */
- public Optional extends Path> findFile(String path) {
- for (var root : roots) {
- var fullPath = root.resolve(path);
-
- if (Files.exists(fullPath)) {
- return Optional.of(fullPath);
- }
- }
-
- return Optional.empty();
- }
-
- /**
- * Get a classloader for this location manager.
- *
- * This will initialize a classloader if it has not been initialized since the last path was
- * added.
- *
- * @return the class loader.
- */
- public ClassLoader getClassLoader() {
- return classLoader.access();
- }
-
- /**
- * Find an existing file object with the given package and relative file name.
- *
- * @param packageName the package name.
- * @param relativeName the relative file name.
- * @return the file, or an empty optional if not found.
- */
- public Optional This will always use the first path that was registered.
- *
- * @param packageName the package to create the file in.
- * @param relativeName the relative name of the file to create.
- * @return the file object for output, or an empty optional if no paths existed to place it in.
- */
- public Optional This will always use the first path that was registered.
- *
- * @param className the class name of the file to create.
- * @param kind the kind of file to create.
- * @return the file object for output, or an empty optional if no paths existed to place it in.
- */
- public Optional This will attempt to find the corresponding root path for the file object, and then
- * output the relative path, converted to a class name-like string. If the file object does not
- * have a root in this manager, expect an empty optional to be returned instead.
- *
- * @param fileObject the file object to infer the binary name for.
- * @return the binary name of the object, or an empty optional if unknown.
- */
- public Optional the implementation type.
+ * @param , E extends Enum {
+
+ private final String friendlyTypeName;
+
+ /**
+ * Initialize this enum assertion.
+ *
+ * @param value the value to assert upon.
+ * @param selfType the type of this assertion implementation.
+ * @param friendlyTypeName the friendly type name to use.
+ */
+ protected AbstractEnumAssert(
+ E value,
+ Class> selfType,
+ String friendlyTypeName
+ ) {
+ super(value, selfType);
+ this.friendlyTypeName = requireNonNull(friendlyTypeName, "friendlyTypeName");
+ }
+
+ /**
+ * Assert that the value is one of the given values.
+ *
+ * @param first the first value to check for.
+ * @param more any additional values to check for.
+ * @return this assertion object.
+ */
+ @SafeVarargs
+ public final S isOneOf(E first, E... more) {
+ requireNonNull(first, "first");
+ requireNonNullValues(more, "more");
+
+ var all = IterableUtils.asList(first, more);
+ if (!all.contains(actual)) {
+ var actualStr = reprName(actual);
+
+ String expectedStr;
+ if (all.size() > 1) {
+ expectedStr = "one of " + all
+ .stream()
+ .map(this::reprName)
+ .collect(Collectors.joining(", "));
+ } else {
+ expectedStr = reprName(first);
+ }
+
+ throw failureWithActualExpected(
+ actualStr,
+ expectedStr,
+ "Expected %s to be %s, but it was %s",
+ friendlyTypeName,
+ expectedStr,
+ actualStr
+ );
+ }
+
+ return myself;
+ }
+
+ private String reprName(E e) {
+ return e == null
+ ? "null"
+ : "<" + e.name() + ">";
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractFileObjectAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractFileObjectAssert.java
new file mode 100644
index 000000000..df28bd2c3
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractFileObjectAssert.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.assertions;
+
+import io.github.ascopes.jct.utils.IoExceptionUtils;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import javax.tools.FileObject;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.assertj.core.api.AbstractAssert;
+import org.assertj.core.api.ByteArrayAssert;
+import org.assertj.core.api.InstantAssert;
+import org.assertj.core.api.StringAssert;
+import org.assertj.core.api.UriAssert;
+
+/**
+ * Abstract assertions for {@link FileObject file objects}.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public abstract class AbstractFileObjectAssert, A extends FileObject>
+ extends AbstractAssert {
+
+ /**
+ * Initialize this assertion.
+ *
+ * @param actual the actual value to assert on.
+ * @param selfType the type of the assertion implementation.
+ */
+ protected AbstractFileObjectAssert(A actual, Class> selfType) {
+ super(actual, selfType);
+ }
+
+ /**
+ * Get an assertion object on the URI of the file.
+ *
+ * @return the URI assertion.
+ */
+ public UriAssert uri() {
+ return new UriAssert(actual.toUri());
+ }
+
+ /**
+ * Get an assertion object on the name of the file.
+ *
+ * @return the string assertion.
+ */
+ public StringAssert name() {
+ return new StringAssert(actual.getName());
+ }
+
+ /**
+ * Get an assertion object on the binary content of the file.
+ *
+ * @return the byte array assertion.
+ */
+ public ByteArrayAssert binaryContent() {
+ return new ByteArrayAssert(rawContent());
+ }
+
+ /**
+ * Get an assertion object on the content of the file, using {@link StandardCharsets#UTF_8 UTF-8}
+ * encoding.
+ *
+ * @return the string assertion.
+ */
+ public StringAssert content() {
+ return content(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Get an assertion object on the content of the file.
+ *
+ * @param charset the charset to decode the file with.
+ * @return the string assertion.
+ */
+ public StringAssert content(Charset charset) {
+ return content(charset.newDecoder());
+ }
+
+ /**
+ * Get an assertion object on the content of the file.
+ *
+ * @param charsetDecoder the charset decoder to use to decode the file to a string.
+ * @return the string assertion.
+ */
+ public StringAssert content(CharsetDecoder charsetDecoder) {
+ var content = IoExceptionUtils.uncheckedIo(() -> charsetDecoder
+ .decode(ByteBuffer.wrap(rawContent()))
+ .toString());
+
+ return new StringAssert(content);
+ }
+
+ /**
+ * Get an assertion object on the last modified timestamp.
+ *
+ * @return the instant assertion.
+ */
+ public InstantAssert lastModified() {
+ var instant = Instant.ofEpochMilli(actual.getLastModified());
+ return new InstantAssert(instant);
+ }
+
+ private byte[] rawContent() {
+ return IoExceptionUtils.uncheckedIo(() -> {
+ var baos = new ByteArrayOutputStream();
+ try (var is = actual.openInputStream()) {
+ is.transferTo(baos);
+ }
+ return baos.toByteArray();
+ });
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java
index a0692a170..e65a6e61a 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java
@@ -17,283 +17,135 @@
package io.github.ascopes.jct.assertions;
import io.github.ascopes.jct.compilers.Compilation;
-import io.github.ascopes.jct.compilers.TraceDiagnostic;
-import io.github.ascopes.jct.paths.ModuleLocation;
+import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic;
+import java.util.List;
+import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.tools.Diagnostic.Kind;
-import javax.tools.JavaFileManager.Location;
-import javax.tools.JavaFileObject;
-import javax.tools.StandardLocation;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
-import org.assertj.core.api.AbstractObjectAssert;
-import org.assertj.core.api.Assertions;
-import org.assertj.core.api.ListAssert;
+import org.assertj.core.api.AbstractAssert;
/**
- * Assertions to apply to compilation output.
+ * Assertions that apply to a {@link Compilation}.
*
- * @param
+ *
*
- * @param location the location to add paths to.
- * @param path1 the first path to add.
- * @param paths additional paths to add.
+ * @param location the location to add.
+ * @param pathLike the path-like object to add.
* @return this compiler object for further call chaining.
+ * @throws IllegalArgumentException if the location is
+ * {@link Location#isModuleOrientedLocation() module-oriented} or
+ * an {@link Location#isOutputLocation()} output location}.
*/
- default C addPaths(Location location, Path path1, Path... paths) {
- return addPaths(location, IterableUtils.combineOneOrMore(path1, paths));
- }
+ C addPath(Location location, PathLike pathLike);
/**
- * Add paths to the class output path.
+ * Add a {@link Path NIO Path} object to a given location.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addClassOutputPaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.CLASS_OUTPUT, paths);
- }
-
- /**
- * Add paths to the class output path.
+ *
+ *
*
- * @param path1 the first path to add.
- * @param paths additional paths to add.
+ * @param location the location to add.
+ * @param path the path to add.
* @return this compiler object for further call chaining.
+ * @throws IllegalArgumentException if the location is not
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*/
- default C addSourceOutputPaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.SOURCE_OUTPUT, path1, paths);
+ default C addPath(Location location, Path path) {
+ return addPath(location, new NioPath(path));
}
/**
- * Add paths to the class path.
+ * Add a path-like object within a module to a given location.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addClassPaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.CLASS_PATH, paths);
- }
-
- /**
- * Add paths to the class path.
+ *
+ *
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
+ * @param location the location to add.
+ * @param pathLike the path-like object to add.
* @return this compiler object for further call chaining.
+ * @throws IllegalArgumentException if the location is not
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*/
- default C addSourcePaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.SOURCE_PATH, path1, paths);
- }
+ C addPath(Location location, String moduleName, PathLike pathLike);
/**
- * Add paths to the annotation processor path.
+ * Add a {@link Path NIO Path} object within a module to a given location.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addAnnotationProcessorPaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, paths);
- }
-
- /**
- * Add paths to the annotation processor path.
+ *
+ *
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
+ * @param location the location to add.
+ * @param path the path to add.
* @return this compiler object for further call chaining.
+ * @throws IllegalArgumentException if the location is not
+ * {@link Location#isModuleOrientedLocation() module-oriented}.
*/
- default C addAnnotationProcessorModulePaths(Path path1, Path... paths) {
- return addPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, path1, paths);
+ default C addPath(Location location, String moduleName, Path path) {
+ return addPath(location, moduleName, new NioPath(path));
}
/**
- * Add paths to the platform class path.
+ * Add a path-like object that contains a package to the class path.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addPlatformClassPaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.PLATFORM_CLASS_PATH, paths);
- }
-
- /**
- * Add paths to the platform class path.
+ *
+ *
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addModuleSourcePaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.MODULE_SOURCE_PATH, paths);
- }
-
- /**
- * Add paths to the module source path.
+ *
+ *
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSystemModulePaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.SYSTEM_MODULES, paths);
- }
-
- /**
- * Add paths to the system module path.
+ *
+ *
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addPatchModulePaths(Collection extends Path> paths) {
- return addPaths(StandardLocation.PATCH_MODULE_PATH, paths);
- }
-
- /**
- * Add paths to the patch module path.
+ *
+ *
*
- *
+ *
*
- * @param path1 the first path to add.
- * @param paths additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addSourceOutputRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.SOURCE_OUTPUT, path1, paths);
- }
-
- /**
- * Add paths to the class path.
+ *
+ *
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addAnnotationProcessorRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, paths);
- }
-
- /**
- * Add paths to the annotation processor path.
+ *
+ *
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addPlatformClassRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.PLATFORM_CLASS_PATH, path1, paths);
- }
-
- /**
- * Add paths to the native header output path.
+ *
+ *
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addModuleSourceRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.MODULE_SOURCE_PATH, paths);
- }
-
- /**
- * Add paths to the module source path.
+ *
+ *
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
+ * @param pathLike the path-like object to add.
* @return this compiler object for further call chaining.
*/
- default C addSystemModuleRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.SYSTEM_MODULES, path1, paths);
+ default C addAnnotationProcessorPath(PathLike pathLike) {
+ return addPath(StandardLocation.ANNOTATION_PROCESSOR_PATH, pathLike);
}
/**
- * Add paths to the module path.
+ * Add a {@link Path NIO Path} that contains a package to compiled annotation processors.
*
- * @param paths the paths to add.
- * @return this compiler object for further call chaining.
- */
- default C addModuleRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.MODULE_PATH, paths);
- }
-
- /**
- * Add paths to the module path.
+ *
+ *
+ *
+ * @param path the path to add.
* @return this compiler object for further call chaining.
*/
- default C addModuleRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.MODULE_PATH, path1, paths);
+ default C addAnnotationProcessorPath(Path path) {
+ return addPath(StandardLocation.ANNOTATION_PROCESSOR_PATH, path);
}
/**
- * Add paths to the patch module path.
+ * Add a path-like object that contains a module package to compiled annotation processors.
+ *
+ *
+ *
*
- * @param paths the paths to add.
+ * @param moduleName the name of the module.
+ * @param pathLike the path-like object to add.
* @return this compiler object for further call chaining.
*/
- default C addPatchModuleRamPaths(Collection extends RamPath> paths) {
- return addRamPaths(StandardLocation.PATCH_MODULE_PATH, paths);
+ default C addAnnotationProcessorModulePath(String moduleName, PathLike pathLike) {
+ return addPath(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, moduleName, pathLike);
}
/**
- * Add paths to the patch module path.
+ * Add a {@link Path NIO Path} that contains a module package to compiled annotation processors.
*
- * @param path1 the first path to add.
- * @param paths any additional paths to add.
+ *
+ *
+ *
+ * @param moduleName the name of the module.
+ * @param path the path to add.
* @return this compiler object for further call chaining.
*/
- default C addPatchModuleRamPaths(RamPath path1, RamPath... paths) {
- return addRamPaths(StandardLocation.PATCH_MODULE_PATH, path1, paths);
+ default C addAnnotationProcessorModulePath(String moduleName, Path path) {
+ return addPath(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, moduleName, path);
}
/**
@@ -786,7 +549,7 @@ default C addAnnotationProcessorOptions(
* Add annotation processors to invoke.
*
* ServiceLoader getServiceLoader(
+ Location location,
+ Class service
+ ) {
+ return getGroup(location)
+ .flatMap(group -> group.getServiceLoader(service))
+ .orElseThrow(() -> new NoSuchElementException(
+ "No service for " + service.getName() + " exists"
+ ));
+ }
+
+ @Nullable
+ @Override
+ public String inferModuleName(Location location) {
+ requirePackageOrientedLocation(location);
+
+ return location instanceof ModuleLocation
+ ? ((ModuleLocation) location).getModuleName()
+ : null;
+ }
+
+ @Override
+ public Iterable Optional service) {
+ var location = getLocation();
+
+ if (location instanceof ModuleLocation) {
+ throw new UnsupportedOperationException("Cannot load services from specific modules");
+ }
+
+ return Optional.of(ServiceLoader.load(service, classLoaderLazy.access()));
+ }
+
+ @Override
+ public Optional the service class type.
+ * @return the service loader, if this location supports loading plugins. If not, an empty
+ * optional is returned instead.
+ * @throws UnsupportedOperationException if the container group does not provide this
+ * functionality.
+ */
+ Optional service);
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/DirectoryContainer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/DirectoryContainer.java
new file mode 100644
index 000000000..6a49b32e0
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/DirectoryContainer.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.jsr199.containers;
+
+import static java.util.Objects.requireNonNull;
+
+import io.github.ascopes.jct.jsr199.PathFileObject;
+import io.github.ascopes.jct.paths.PathLike;
+import io.github.ascopes.jct.utils.FileUtils;
+import io.github.ascopes.jct.utils.StringUtils;
+import java.io.IOException;
+import java.lang.module.ModuleFinder;
+import java.net.URL;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject.Kind;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * A container that wraps a known directory of files.
+ *
+ * @author Ashley Scopes
+ * @since 0.0.1
+ */
+@API(since = "0.0.1", status = Status.EXPERIMENTAL)
+public class DirectoryContainer implements Container {
+
+ private final Location location;
+ private final PathLike root;
+ private final String name;
+
+ /**
+ * Initialize this container.
+ *
+ * @param location the location.
+ * @param root the root directory to hold.
+ */
+ public DirectoryContainer(Location location, PathLike root) {
+ this.location = requireNonNull(location, "location");
+ this.root = requireNonNull(root, "root");
+ name = root.toString();
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Do nothing for this implementation. We have nothing to close.
+ }
+
+ @Override
+ public boolean contains(PathFileObject fileObject) {
+ var path = fileObject.getFullPath();
+ return path.startsWith(root.getPath()) && Files.isRegularFile(path);
+ }
+
+ @Override
+ public Optional Optional service) {
+ getClass().getModule().addUses(service);
+
+ var finders = modules
+ .values()
+ .stream()
+ .map(SimpleModuleOrientedModuleContainerGroup::getPackages)
+ .flatMap(List::stream)
+ .map(Container::getModuleFinder)
+ .toArray(ModuleFinder[]::new);
+
+ var composedFinder = ModuleFinder.compose(finders);
+ var bootLayer = ModuleLayer.boot();
+ var config = bootLayer
+ .configuration()
+ .resolveAndBind(ModuleFinder.of(), composedFinder, Collections.emptySet());
+
+ var layer = bootLayer
+ .defineModulesWithOneLoader(config, ClassLoader.getSystemClassLoader());
+
+ return Optional.of(ServiceLoader.load(layer, service));
+ }
+
+ @Override
+ public PackageOrientedContainerGroup forModule(String moduleName) {
+ return modules
+ .computeIfAbsent(
+ new ModuleLocation(location, moduleName),
+ SimpleModuleOrientedModuleContainerGroup::new
+ );
+ }
+
+ private ContainerClassLoader createClassLoader() {
+ var moduleMapping = modules
+ .entrySet()
+ .stream()
+ .collect(Collectors.toUnmodifiableMap(
+ entry -> entry.getKey().getModuleName(),
+ entry -> entry.getValue().getPackages()
+ ));
+
+ return new ContainerClassLoader(location, moduleMapping);
+ }
+
+ /**
+ * Wrapper around a location that lacks the constraints that
+ * {@link SimplePackageOrientedContainerGroup} imposes.
+ */
+ private class SimpleModuleOrientedModuleContainerGroup
+ extends AbstractPackageOrientedContainerGroup {
+
+ private final Location location;
+
+ private SimpleModuleOrientedModuleContainerGroup(Location location) {
+ super(SimpleModuleOrientedContainerGroup.this.release);
+ this.location = requireNonNull(location, "location");
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleOutputOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleOutputOrientedContainerGroup.java
new file mode 100644
index 000000000..b136ebe82
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleOutputOrientedContainerGroup.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2022 Ashley Scopes
+ *
+ * Licensed 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 io.github.ascopes.jct.jsr199.containers;
+
+import static java.util.Objects.requireNonNull;
+
+import io.github.ascopes.jct.jsr199.ModuleLocation;
+import io.github.ascopes.jct.jsr199.PathFileObject;
+import io.github.ascopes.jct.paths.PathLike;
+import io.github.ascopes.jct.paths.SubPath;
+import io.github.ascopes.jct.utils.IoExceptionUtils;
+import io.github.ascopes.jct.utils.ModulePrefix;
+import io.github.ascopes.jct.utils.StringUtils;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject.Kind;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * A group of containers that relate to a specific output location.
+ *
+ * implements Diagnostic {
* @param original the original diagnostic to delegate to.
*/
protected ForwardingDiagnostic(Diagnostic extends S> original) {
- this.original = Objects.requireNonNull(original);
+ this.original = requireNonNull(original, "original");
}
@Override
@@ -51,6 +53,7 @@ public Kind getKind() {
return original.getKind();
}
+ @Nullable
@Override
public S getSource() {
return original.getSource();
@@ -81,16 +84,25 @@ public long getColumnNumber() {
return original.getColumnNumber();
}
+ @Nullable
@Override
public String getCode() {
return original.getCode();
}
@Override
- public String getMessage(Locale locale) {
+ public String getMessage(@Nullable Locale locale) {
return original.getMessage(locale);
}
+ /**
+ * {@inheritDoc}
+ *
+ * ServiceLoader getServiceLoader(Location location, Class service)
- throws IOException {
- return repository
- .getManager(location)
- .map(manager -> manager.getServiceLoader(service))
- .orElseThrow(() -> formatException(
- NoSuchElementException::new,
- "Cannot find a service loader for %s in location %s (%s)",
- service.getName(),
- location.getName(),
- location.getClass().getName()
- ));
- }
-
- @Override
- public FileObject getFileForInput(Location location, String packageName, String relativeName)
- throws IOException {
- assertKnownLocation(location);
-
- return repository
- .getManager(location)
- .flatMap(manager -> manager.getFileForInput(packageName, relativeName))
- .orElse(null);
- }
-
- @Override
- public FileObject getFileForOutput(
- Location location,
- String packageName,
- String relativeName,
- FileObject sibling
- ) throws IOException {
- assertOutputLocation(location);
- assertKnownLocation(location);
-
- return repository
- .getOrCreateManager(location)
- .getFileForOutput(packageName, relativeName)
- .orElse(null);
- }
-
- @Override
- public JavaFileObject getJavaFileForInput(
- Location location,
- String className,
- Kind kind
- ) throws IOException {
- assertKnownLocation(location);
-
- return repository
- .getManager(location)
- .flatMap(manager -> manager.getJavaFileForInput(className, kind))
- .orElse(null);
- }
-
- @Override
- public JavaFileObject getJavaFileForOutput(
- Location location,
- String className,
- Kind kind,
- FileObject sibling
- ) throws IOException {
- assertOutputLocation(location);
-
- return repository
- .getOrCreateManager(location)
- .getJavaFileForOutput(className, kind)
- .orElse(null);
- }
-
- @Override
- public Location getLocationForModule(Location location, String moduleName) throws IOException {
- assertOpen();
- assertModuleOrientedOrOutputLocation(location);
- assertKnownLocation(location);
-
- return new ModuleLocation(location, moduleName);
- }
-
- @Override
- public Location getLocationForModule(Location location, JavaFileObject fileObject)
- throws IOException {
- assertOpen();
- assertModuleOrientedOrOutputLocation(location);
-
- return repository
- .getManager(location)
- .map(ParentPathLocationManager.class::cast)
- .orElseThrow(() -> unknownLocation(location))
- .getModuleLocationFor(fileObject)
- .orElseThrow(() -> formatException(
- IllegalArgumentException::new,
- "File %s is not known to any modules within location %s (%s)",
- fileObject.toUri(),
- location.getName(),
- location.getClass().getName()
- ));
- }
-
- @Override
- public boolean handleOption(String current, Iterator the service type.
- * @return the service loader.
- * @throws UnsupportedOperationException if this manager is for a module location.
- */
- public ServiceLoader getServiceLoader(Class service) {
- if (location instanceof ModuleLocation) {
- throw new UnsupportedOperationException("Cannot load services from specific modules");
- }
-
- getClass().getModule().addUses(service);
- if (location.isModuleOrientedLocation()) {
- var finder = ModuleFinder.of(roots.toArray(new Path[0]));
- var bootLayer = ModuleLayer.boot();
- var config = bootLayer
- .configuration()
- .resolveAndBind(ModuleFinder.of(), finder, Collections.emptySet());
- var layer = bootLayer
- .defineModulesWithOneLoader(config, ClassLoader.getSystemClassLoader());
- return ServiceLoader.load(layer, service);
- } else {
- return ServiceLoader.load(service, classLoader.access());
- }
- }
-
- /**
- * Infer the binary name of the given file object.
- *
- *