From 5f3ed2aaea546bc8a3cb26978aeaf5bc237cfcff Mon Sep 17 00:00:00 2001 From: Ashley Scopes <73482956+ascopes@users.noreply.github.com> Date: Thu, 26 Dec 2024 15:08:10 +0000 Subject: [PATCH] Port across ECJ support This is still provisional and an active work in progress, please track https://github.com/eclipse-jdt/eclipse.jdt.core/issues/958 for this work. --- java-compiler-testing/pom.xml | 5 + .../ascopes/jct/compilers/JctCompilers.java | 11 + .../compilers/impl/EcjJctCompilerImpl.java | 58 ++ .../compilers/impl/EcjJctFlagBuilderImpl.java | 184 ++++++ .../ascopes/jct/junit/EcjCompilerTest.java | 140 +++++ .../jct/junit/EcjCompilersProvider.java | 64 +++ .../src/main/java/module-info.java | 4 +- .../jct/compilers/JctCompilersTest.java | 19 + .../impl/EcjJctCompilerImplTest.java | 145 +++++ .../impl/EcjJctFlagBuilderImplTest.java | 542 ++++++++++++++++++ .../jct/junit/EcjCompilersProviderTest.java | 166 ++++++ pom.xml | 8 + ...add-development-ecj-to-maven-repository.sh | 77 +++ scripts/ecj.sh | 54 ++ 14 files changed, 1476 insertions(+), 1 deletion(-) create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java create mode 100755 scripts/add-development-ecj-to-maven-repository.sh create mode 100755 scripts/ecj.sh diff --git a/java-compiler-testing/pom.xml b/java-compiler-testing/pom.xml index 51877fac..a4a8e0a2 100644 --- a/java-compiler-testing/pom.xml +++ b/java-compiler-testing/pom.xml @@ -53,6 +53,11 @@ assertj-core + + org.eclipse.jdt + ecj + + org.jspecify jspecify diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java index 9b901b56..e25cafbe 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java @@ -15,6 +15,7 @@ */ package io.github.ascopes.jct.compilers; +import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl; import io.github.ascopes.jct.compilers.impl.JavacJctCompilerImpl; import io.github.ascopes.jct.utils.UtilityClass; @@ -40,4 +41,14 @@ private JctCompilers() { public static JctCompiler newPlatformCompiler() { return new JavacJctCompilerImpl(); } + + /** + * Create a new instance of the ECJ compiler (Eclipse Compiler for Java). + * + * @return the compiler instance. + * @since 5.0.0 + */ + public static JctCompiler newEcjCompiler() { + return new EcjJctCompilerImpl(); + } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java new file mode 100644 index 00000000..939c86b3 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java @@ -0,0 +1,58 @@ +package io.github.ascopes.jct.compilers.impl; + +import io.github.ascopes.jct.compilers.AbstractJctCompiler; +import io.github.ascopes.jct.compilers.JctFlagBuilderFactory; +import io.github.ascopes.jct.compilers.Jsr199CompilerFactory; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler; + + +/** + * Implementation of a JCT compiler that integrates with the Eclipse Java Compiler. + * + * @author Ashley Scopes + * @since 5.0.0 + */ +public final class EcjJctCompilerImpl extends AbstractJctCompiler { + + public EcjJctCompilerImpl() { + super("ECJ"); + } + + @Override + public JctFlagBuilderFactory getFlagBuilderFactory() { + return EcjJctFlagBuilderImpl::new; + } + + @Override + public Jsr199CompilerFactory getCompilerFactory() { + return EclipseCompiler::new; + } + + @Override + public String getDefaultRelease() { + return Integer.toString(getLatestSupportedVersionInt()); + } + + /** + * Get the minimum version of ECJ that is supported. + * + * @return the minimum supported version. + */ + public static int getEarliestSupportedVersionInt() { + return decodeMajorVersion(ClassFileConstants.JDK1_8); + } + + /** + * Get the maximum version of ECJ that is supported. + * + * @return the maximum supported version. + */ + public static int getLatestSupportedVersionInt() { + return decodeMajorVersion(ClassFileConstants.getLatestJDKLevel()); + } + + private static int decodeMajorVersion(long classFileConstant) { + return (int) ((classFileConstant >> 16L) - ClassFileConstants.MAJOR_VERSION_0); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java new file mode 100644 index 00000000..3c66b68a --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 - 2024, the original author or authors. + * + * 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.impl; + +import io.github.ascopes.jct.compilers.CompilationMode; +import io.github.ascopes.jct.compilers.DebuggingInfo; +import io.github.ascopes.jct.compilers.JctFlagBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.jspecify.annotations.Nullable; + +/** + * Helper to build flags for the ECJ compiler implementation. + * + * @author Ashley Scopes + * @since 5.0.0 + */ +public final class EcjJctFlagBuilderImpl implements JctFlagBuilder { + + private static final String VERBOSE = "-verbose"; + private static final String PRINT_ANNOTATION_PROCESSOR_INFO = "-XprintProcessorInfo"; + private static final String PRINT_ANNOTATION_PROCESSOR_ROUNDS = "-XprintRounds"; + private static final String ENABLE_PREVIEW = "--enable-preview"; + private static final String NOWARN = "-nowarn"; + private static final String FAIL_ON_WARNING = "--failOnWarning"; + private static final String DEPRECATION = "-deprecation"; + private static final String RELEASE = "--release"; + private static final String SOURCE = "-source"; + private static final String TARGET = "-target"; + private static final String ANNOTATION_OPT = "-A"; + private static final String PROC_NONE = "-proc:none"; + private static final String PROC_ONLY = "-proc:only"; + private static final String DEBUG_LINES = "-g:lines"; + private static final String DEBUG_VARS = "-g:vars"; + private static final String DEBUG_SOURCE = "-g:source"; + private static final String DEBUG_NONE = "-g:none"; + private static final String PARAMETERS = "-parameters"; + + private final List craftedFlags; + + /** + * Initialize this flag builder. + */ + public EcjJctFlagBuilderImpl() { + craftedFlags = new ArrayList<>(); + } + + @Override + public EcjJctFlagBuilderImpl verbose(boolean enabled) { + return addFlagIfTrue(enabled, VERBOSE) + .addFlagIfTrue(enabled, PRINT_ANNOTATION_PROCESSOR_INFO) + .addFlagIfTrue(enabled, PRINT_ANNOTATION_PROCESSOR_ROUNDS); + } + + @Override + public EcjJctFlagBuilderImpl previewFeatures(boolean enabled) { + return addFlagIfTrue(enabled, ENABLE_PREVIEW); + } + + @Override + public EcjJctFlagBuilderImpl showWarnings(boolean enabled) { + return addFlagIfTrue(!enabled, NOWARN); + } + + @Override + public EcjJctFlagBuilderImpl failOnWarnings(boolean enabled) { + return addFlagIfTrue(enabled, FAIL_ON_WARNING); + } + + @Override + public JctFlagBuilder compilationMode(CompilationMode compilationMode) { + switch (compilationMode) { + case COMPILATION_ONLY: + craftedFlags.add(PROC_NONE); + break; + + case ANNOTATION_PROCESSING_ONLY: + craftedFlags.add(PROC_ONLY); + break; + + default: + // Do nothing. The default behaviour is to allow this. + break; + } + + return this; + } + + @Override + public EcjJctFlagBuilderImpl showDeprecationWarnings(boolean enabled) { + return addFlagIfTrue(enabled, DEPRECATION); + } + + @Override + public EcjJctFlagBuilderImpl release(@Nullable String version) { + return addVersionIfPresent(RELEASE, version); + } + + @Override + public EcjJctFlagBuilderImpl source(@Nullable String version) { + return addVersionIfPresent(SOURCE, version); + } + + @Override + public EcjJctFlagBuilderImpl target(@Nullable String version) { + return addVersionIfPresent(TARGET, version); + } + + @Override + public EcjJctFlagBuilderImpl debuggingInfo(Set set) { + if (set.isEmpty()) { + craftedFlags.add(DEBUG_NONE); + return this; + } + + if (set.contains(DebuggingInfo.LINES)) { + craftedFlags.add(DEBUG_LINES); + } + + if (set.contains(DebuggingInfo.SOURCE)) { + craftedFlags.add(DEBUG_SOURCE); + } + + if (set.contains(DebuggingInfo.VARS)) { + craftedFlags.add(DEBUG_VARS); + } + + return this; + } + + @Override + public EcjJctFlagBuilderImpl parameterInfoEnabled(boolean enabled) { + return addFlagIfTrue(enabled, PARAMETERS); + } + + @Override + public EcjJctFlagBuilderImpl annotationProcessorOptions(List options) { + options.forEach(option -> craftedFlags.add(ANNOTATION_OPT + option)); + return this; + } + + @Override + public EcjJctFlagBuilderImpl compilerOptions(List options) { + craftedFlags.addAll(options); + return this; + } + + @Override + public List build() { + // Immutable copy. + return List.copyOf(craftedFlags); + } + + private EcjJctFlagBuilderImpl addFlagIfTrue(boolean condition, String flag) { + if (condition) { + craftedFlags.add(flag); + } + + return this; + } + + private EcjJctFlagBuilderImpl addVersionIfPresent(String flagPrefix, @Nullable String version) { + if (version != null) { + craftedFlags.add(flagPrefix); + craftedFlags.add(version); + } + + return this; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java new file mode 100644 index 00000000..8c2443b1 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2022 - 2024, the original author or authors. + * + * 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.junit; + +import io.github.ascopes.jct.compilers.JctCompilerConfigurer; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Tags; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** + * Annotation that can be applied to a JUnit parameterized test to invoke that test case across + * multiple ECJ compilers, each configured to a specific version in a range of Java language + * versions. + * + *

This will also add the {@code "java-compiler-testing-test"} tag and {@code "ecj-test"} + * tags to your test method, meaning you can instruct your IDE or build system to optionally only + * run tests annotated with this method for development purposes. As an example, Maven Surefire + * could be instructed to only run these tests by passing {@code -Dgroup="ecj-test"} to Maven. + * + * @author Ashley Scopes + * @since TBC + */ +@ArgumentsSource(EcjCompilersProvider.class) +@DisabledInNativeImage +@Documented +@ParameterizedTest(name = "for compiler \"{0}\"") +@Retention(RetentionPolicy.RUNTIME) +@Tags({ + @Tag("java-compiler-testing-test"), + @Tag("ecj-test") +}) +@Target({ + ElementType.ANNOTATION_TYPE, + ElementType.METHOD, +}) +@TestTemplate +public @interface EcjCompilerTest { + + /** + * Minimum version to use (inclusive). + * + *

By default, it will use the lowest possible version supported by the compiler. This + * varies between versions of the JDK that are in use. + * + *

If the version is lower than the minimum supported version, then the minimum supported + * version of the compiler will be used instead. This enables writing tests that will work on a + * range of JDKs during builds without needing to duplicate the test to satisfy different JDK + * supported version ranges. + * + * @return the minimum version. + */ + int minVersion() default Integer.MIN_VALUE; + + /** + * Maximum version to use (inclusive). + * + *

By default, it will use the highest possible version supported by the compiler. This + * varies between versions of the JDK that are in use. + * + *

If the version is higher than the maximum supported version, then the maximum supported + * version of the compiler will be used instead. This enables writing tests that will work on a + * range of JDKs during builds without needing to duplicate the test to satisfy different JDK + * supported version ranges. + * + * @return the maximum version. + */ + int maxVersion() default Integer.MAX_VALUE; + + /** + * Get an array of compiler configurer classes to apply in-order before starting the test. + * + *

Each configurer must have a public no-args constructor, and their package must be + * open to this module if JPMS modules are in-use, for example: + * + *


+   * module mytests {
+   *   requires io.github.ascopes.jct;
+   *   requires org.junit.jupiter.api;
+   *
+   *   opens org.example.mytests to io.github.ascopes.jct;
+   * }
+   * 
+ * + *

An example of usage: + * + *


+   *   public class WerrorConfigurer implements JctCompilerConfigurer<RuntimeException> {
+   *     {@literal @Override}
+   *     public void configure(JctCompiler compiler) {
+   *       compiler.failOnWarnings(true);
+   *     }
+   *   }
+   *
+   *   // ...
+   *
+   *   class SomeTest {
+   *     {@literal @EcjCompilerTest(configurers = WerrorConfigurer.class)}
+   *     void someTest(JctCompiler compiler) {
+   *       // ...
+   *     }
+   *   }
+   * 
+ * + * @return an array of classes to run to configure the compiler. These run in the given order. + */ + Class>[] configurers() default {}; + + /** + * The version strategy to use. + * + *

This determines whether the version number being iterated across specifies the + * release, source, target, or source and target versions. + * + *

The default is to specify the release. + * + * @return the version strategy to use. + */ + VersionStrategy versionStrategy() default VersionStrategy.RELEASE; +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java new file mode 100644 index 00000000..9587a0d1 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 - 2024, the original author or authors. + * + * 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.junit; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl; +import org.junit.jupiter.params.support.AnnotationConsumer; + +/** + * Argument provider for the {@link EcjCompilerTest} annotation. + * + * @author Ashley Scopes + * @since 5.0.0 + */ +public final class EcjCompilersProvider extends AbstractCompilersProvider + implements AnnotationConsumer { + + /** + * Initialise the provider. + * + *

This is only visible for testing purposes, users should have no need to + * initialise this class directly. + */ + EcjCompilersProvider() { + // Visible for testing only. + } + + @Override + protected JctCompiler initializeNewCompiler() { + return new EcjJctCompilerImpl(); + } + + @Override + protected int minSupportedVersion() { + return EcjJctCompilerImpl.getEarliestSupportedVersionInt(); + } + + @Override + protected int maxSupportedVersion() { + return EcjJctCompilerImpl.getLatestSupportedVersionInt(); + } + + @Override + public void accept(EcjCompilerTest annotation) { + var min = annotation.minVersion(); + var max = annotation.maxVersion(); + var configurers = annotation.configurers(); + var versioning = annotation.versionStrategy(); + configure(min, max, configurers, versioning); + } +} diff --git a/java-compiler-testing/src/main/java/module-info.java b/java-compiler-testing/src/main/java/module-info.java index ce510b28..d6a9ff90 100644 --- a/java-compiler-testing/src/main/java/module-info.java +++ b/java-compiler-testing/src/main/java/module-info.java @@ -75,7 +75,8 @@ * * class JsonSchemaAnnotationProcessorTest { * - * {@literal @JavacCompilerTest(minVersion=11, maxVersion=19)} + * {@literal @EcjCompilerTest(minVersion=17)} + * {@literal @JavacCompilerTest(minVersion=17)} * void theJsonSchemaIsCreatedFromTheInputCode(JctCompiler compiler) { * * try (var workspace = Workspaces.newWorkspace()) { @@ -125,6 +126,7 @@ requires java.management; requires me.xdrop.fuzzywuzzy; requires org.assertj.core; + requires org.eclipse.jdt.core.compiler.batch; requires static org.jspecify; requires static org.junit.jupiter.api; requires static org.junit.jupiter.params; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java index c89636de..3bd0c3c1 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl; import io.github.ascopes.jct.compilers.impl.JavacJctCompilerImpl; import io.github.ascopes.jct.fixtures.UtilityClassTestTemplate; import org.junit.jupiter.api.DisplayName; @@ -53,4 +54,22 @@ void newPlatformCompilerReturnsTheExpectedInstance() { .satisfies(constructed -> assertThat(compiler).isSameAs(constructed)); } } + + @DisplayName(".newEcjCompiler() creates an EcjJctCompilerImpl instance") + @Test + void newEcjCompilerReturnsTheExpectedInstance() { + try (var ecjJctCompilerImplMock = Mockito.mockConstruction(EcjJctCompilerImpl.class)) { + // When + var compiler = JctCompilers.newEcjCompiler(); + + // Then + assertThat(compiler) + .isInstanceOf(EcjJctCompilerImpl.class); + + assertThat(ecjJctCompilerImplMock.constructed()) + .singleElement() + // Nested assertion to swap expected/actual args. + .satisfies(constructed -> assertThat(compiler).isSameAs(constructed)); + } + } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java new file mode 100644 index 00000000..5febde2d --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 - 2024, the original author or authors. + * + * 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.impl; + +import static io.github.ascopes.jct.fixtures.Fixtures.someInt; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; + +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * {@link EcjJctCompilerImpl} tests. + * + * @author Ashley Scopes + */ +@DisplayName("EcjJctCompilerImpl tests") +class EcjJctCompilerImplTest { + + EcjJctCompilerImpl compiler; + + @BeforeEach + void setUp() { + compiler = new EcjJctCompilerImpl(); + } + + @DisplayName("Compilers have the expected JSR-199 compiler factory") + @Test + void compilersHaveTheExpectedCompilerFactory() { + // When + var actualCompiler = compiler.getCompilerFactory().createCompiler(); + + // Then + assertThat(actualCompiler).isInstanceOf(EclipseCompiler.class); + } + + @DisplayName("Compilers have the expected flag builder factory") + @Test + void compilersHaveTheExpectedFlagBuilderFactory() { + // Given + try (var flagBuilderMock = mockConstruction(EcjJctFlagBuilderImpl.class)) { + // When + var flagBuilder = compiler.getFlagBuilderFactory().createFlagBuilder(); + + // Then + assertThat(flagBuilderMock.constructed()).hasSize(1); + assertThat(flagBuilder).isSameAs(flagBuilderMock.constructed().get(0)); + } + } + + @DisplayName("Compilers have the expected default release string") + @Test + void compilersHaveTheExpectedDefaultRelease() { + // Given + try (var compilerClassMock = mockStatic(EcjJctCompilerImpl.class)) { + var latestSupportedInt = someInt(17, 21); + compilerClassMock + .when(EcjJctCompilerImpl::getLatestSupportedVersionInt) + .thenReturn(latestSupportedInt); + + // When + var defaultRelease = compiler.getDefaultRelease(); + + // Then + compilerClassMock + .verify(EcjJctCompilerImpl::getLatestSupportedVersionInt); + + assertThat(defaultRelease) + .isEqualTo("%d", latestSupportedInt); + } + } + + @DisplayName("Compilers have the expected default name") + @Test + void compilersHaveTheExpectedDefaultName() { + // Then + assertThat(compiler.getName()).isEqualTo("ECJ"); + } + + @DisplayName("Compilers have no default compiler flags set") + @Test + void compilersHaveNoDefaultCompilerFlagsSet() { + // Then + assertThat(compiler.getCompilerOptions()).isEmpty(); + } + + @DisplayName("Compilers have no default annotation processor flags set") + @Test + void compilersHaveNoDefaultAnnotationProcessorFlagsSet() { + // Then + assertThat(compiler.getAnnotationProcessorOptions()).isEmpty(); + } + + @DisplayName("Compilers have no default annotation processors set") + @Test + void compilersHaveNoDefaultAnnotationProcessorsSet() { + // Then + assertThat(compiler.getAnnotationProcessors()).isEmpty(); + } + + @DisplayName("Compilers have the expected latest release") + @Test + void latestSupportedVersionReturnsTheExpectedValue() { + // Given + var expected = (int) ((ClassFileConstants.getLatestJDKLevel() >> 16L) + - ClassFileConstants.MAJOR_VERSION_0); + + // When + var actual = EcjJctCompilerImpl.getLatestSupportedVersionInt(); + + // Then + assertThat(expected).isEqualTo(actual); + } + + @DisplayName("Compilers have the expected earliest release") + @Test + void earliestSupportedVersionReturnsTheExpectedValue() { + // Given + var expected = (int) ((ClassFileConstants.JDK1_8 >> 16L) + - ClassFileConstants.MAJOR_VERSION_0); + + // When + var actual = EcjJctCompilerImpl.getEarliestSupportedVersionInt(); + + // Then + assertThat(expected).isEqualTo(actual); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java new file mode 100644 index 00000000..ac49b202 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2022 - 2024, the original author or authors. + * + * 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.impl; + +import static io.github.ascopes.jct.fixtures.Fixtures.someBoolean; +import static io.github.ascopes.jct.fixtures.Fixtures.someRelease; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.ascopes.jct.compilers.CompilationMode; +import io.github.ascopes.jct.compilers.DebuggingInfo; +import io.github.ascopes.jct.fixtures.Fixtures; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * {@link EcjJctFlagBuilderImpl} tests. + * + * @author Ashley Scopes + */ +@DisplayName("EcjJctFlagBuilderImpl tests") +@TestMethodOrder(OrderAnnotation.class) +class EcjJctFlagBuilderImplTest { + + EcjJctFlagBuilderImpl flagBuilder; + + @BeforeEach + void setUp() { + flagBuilder = new EcjJctFlagBuilderImpl(); + } + + @DisplayName(".verbose(boolean) tests") + @Nested + class VerboseFlagTest { + + @DisplayName("Setting .verbose(true) adds the '-verbose' flag") + @Test + void addsFlagIfTrue() { + // When + flagBuilder.verbose(true); + + // Then + assertThat(flagBuilder.build()).contains("-verbose"); + } + + @DisplayName("Setting .verbose(false) does not add the '-verbose' flag") + @Test + void doesNotAddFlagIfFalse() { + // When + flagBuilder.verbose(false); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-verbose"); + } + + @DisplayName(".verbose(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.verbose(someBoolean())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".previewFeatures(boolean) tests") + @Nested + class PreviewFeaturesFlagTest { + + @DisplayName("Setting .previewFeatures(true) adds the '--enable-preview' flag") + @Test + void addsFlagIfTrue() { + // When + flagBuilder.previewFeatures(true); + + // Then + assertThat(flagBuilder.build()).contains("--enable-preview"); + } + + @DisplayName("Setting .previewFeatures(false) does not add the '--enable-preview' flag") + @Test + void doesNotAddFlagIfFalse() { + // When + flagBuilder.previewFeatures(false); + + // Then + assertThat(flagBuilder.build()).doesNotContain("--enable-preview"); + } + + @DisplayName(".previewFeatures(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.previewFeatures(someBoolean())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".showWarnings(boolean) tests") + @Nested + class ShowWarningsFlagTest { + + @DisplayName("Setting .showWarnings(true) does not add the '-nowarn' flag") + @Test + void doesNotAddFlagIfTrue() { + // When + flagBuilder.showWarnings(true); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-nowarn"); + } + + @DisplayName("Setting .showWarnings(false) adds the '-nowarn' flag") + @Test + void addsFlagIfFalse() { + // When + flagBuilder.showWarnings(false); + + // Then + assertThat(flagBuilder.build()).contains("-nowarn"); + } + + @DisplayName(".showWarnings(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.showWarnings(someBoolean())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".failOnWarnings(boolean) tests") + @Nested + class FailOnWarningsFlagTest { + + @DisplayName("Setting .failOnWarnings(true) adds the '--failOnWarning' flag") + @Test + void addsFlagIfTrue() { + // When + flagBuilder.failOnWarnings(true); + + // Then + assertThat(flagBuilder.build()).contains("--failOnWarning"); + } + + @DisplayName("Setting .failOnWarnings(false) does not add the '-Werror' flag") + @Test + void doesNotAddFlagIfFalse() { + // When + flagBuilder.failOnWarnings(false); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-Werror"); + } + + @DisplayName(".failOnWarnings(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.failOnWarnings(someBoolean())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".compilationMode(CompilationMode) tests") + @Nested + class CompilationModeFlagTest { + + @DisplayName(".compilationMode(COMPILATION_ONLY) adds -proc:none") + @Test + void compilationOnlyAddsProcNone() { + // When + flagBuilder.compilationMode(CompilationMode.COMPILATION_ONLY); + + // Then + assertThat(flagBuilder.build()).containsExactly("-proc:none"); + } + + @DisplayName(".compilationMode(ANNOTATION_PROCESSING_ONLY) adds -proc:only") + @Test + void annotationProcessingOnlyAddsProcOnly() { + // When + flagBuilder.compilationMode(CompilationMode.ANNOTATION_PROCESSING_ONLY); + + // Then + assertThat(flagBuilder.build()).containsExactly("-proc:only"); + } + + @DisplayName(".compilationMode(COMPILATION_AND_ANNOTATION_PROCESSING) adds nothing") + @Test + void compilationAndAnnotationProcessingAddsNothing() { + // When + flagBuilder.compilationMode(CompilationMode.COMPILATION_AND_ANNOTATION_PROCESSING); + + // Then + assertThat(flagBuilder.build()).isEmpty(); + } + + @DisplayName(".compilationMode(...) returns the flag builder") + @EnumSource(CompilationMode.class) + @ParameterizedTest(name = "for compilationMode = {0}") + void returnsFlagBuilder(CompilationMode mode) { + // Then + assertThat(flagBuilder.compilationMode(mode)) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".showDeprecationWarnings(boolean) tests") + @Nested + class ShowDeprecationWarningsFlagTest { + + @DisplayName("Setting .showDeprecationWarnings(true) adds the '-deprecation' flag") + @Test + void addsFlagIfTrue() { + // When + flagBuilder.showDeprecationWarnings(true); + + // Then + assertThat(flagBuilder.build()).contains("-deprecation"); + } + + @DisplayName("Setting .showDeprecationWarnings(false) does not add the '-deprecation' flag") + @Test + void doesNotAddFlagIfFalse() { + // When + flagBuilder.showDeprecationWarnings(false); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-deprecation"); + } + + @DisplayName(".showDeprecationWarnings(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.showDeprecationWarnings(someBoolean())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".release(String) tests") + @Nested + class ReleaseFlagTest { + + @DisplayName("Setting .release(String) adds the '--release ' flag") + @ValueSource(strings = {"8", "11", "17"}) + @ParameterizedTest(name = "Setting .release(String) adds the \"--release {0}\" flag") + void addsFlagIfPresent(String version) { + // When + flagBuilder.release(version); + + // Then + assertThat(flagBuilder.build()).containsSequence("--release", version); + } + + @DisplayName("Setting .release(null) does not add the '--release' flag") + @Test + void doesNotAddFlagIfNotPresent() { + // When + flagBuilder.release(null); + + // Then + assertThat(flagBuilder.build()).doesNotContain("--release"); + } + + @DisplayName(".release(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.release(someRelease())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".source(String) tests") + @Nested + class SourceFlagTest { + + @DisplayName("Setting .source(String) adds the '-source ' flag") + @ValueSource(strings = {"8", "11", "17"}) + @ParameterizedTest(name = "Setting .source(String) adds the \"-source {0}\" flag") + void addsFlagIfPresent(String version) { + // When + flagBuilder.source(version); + + // Then + assertThat(flagBuilder.build()).containsSequence("-source", version); + } + + @DisplayName("Setting .source(null) does not add the '-source' flag") + @Test + void doesNotAddFlagIfNotPresent() { + // When + flagBuilder.source(null); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-source"); + } + + + @DisplayName(".source(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.source(someRelease())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".target(String) tests") + @Nested + class TargetFlagTest { + + @DisplayName("Setting .target(String) adds the '-target ' flag") + @ValueSource(strings = {"8", "11", "17"}) + @ParameterizedTest(name = "Setting .target(String) adds the \"-target {0}\" flag") + void addsFlagIfPresent(String version) { + // When + flagBuilder.target(version); + + // Then + assertThat(flagBuilder.build()).containsSequence("-target", version); + } + + @DisplayName("Setting .target(null) does not add the '-target' flag") + @Test + void doesNotAddFlagIfNotPresent() { + // When + flagBuilder.target(null); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-target"); + } + + @DisplayName(".target(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Then + assertThat(flagBuilder.target(someRelease())) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".debuggingInfo(Set) tests") + @Nested + class DebuggingInfoTest { + + @DisplayName("Setting .debuggingInfo with an empty set adds the '-g:none' flag") + @Test + void emptySetAddsGnoneFlag() { + // When + flagBuilder.debuggingInfo(DebuggingInfo.none()); + + // Then + assertThat(flagBuilder.build()).containsOnlyOnce("-g:none"); + } + + @DisplayName("Setting .debuggingInfo with some values set adds the '-g:xxx' flags") + @CsvSource({ + " LINES, -g:lines", + "SOURCE, -g:source", + " VARS, -g:vars", + }) + @ParameterizedTest(name = "expect {0} to set flag {1}") + void correctFlagsAreSet(DebuggingInfo flag, String flagString) { + // When + flagBuilder.debuggingInfo(DebuggingInfo.just(flag)); + + // Then + assertThat(flagBuilder.build()).containsExactly(flagString); + } + + @DisplayName("Setting .debuggingInfo with all values set adds the '-g:xxx' flags") + @Test + void allAddsValues() { + // When + flagBuilder.debuggingInfo(DebuggingInfo.all()); + + // Then + assertThat(flagBuilder.build()) + .doesNotContain("-g", "-g:none") + .containsOnlyOnce("-g:lines", "-g:source", "-g:vars"); + } + } + + @DisplayName(".parameterInfoEnabled(boolean) tests") + @Nested + class ParameterInfoEnabledTest { + + @DisplayName("Setting .parameterInfoEnabled(true) adds the '-parameters' flag") + @Test + void trueAddsFlag() { + // When + flagBuilder.parameterInfoEnabled(true); + + // Then + assertThat(flagBuilder.build()).containsOnlyOnce("-parameters"); + } + + @DisplayName("Setting .parameterInfoEnabled(false) does not add the '-parameters' flag") + @Test + void falseDoesNotAddFlag() { + // When + flagBuilder.parameterInfoEnabled(false); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-parameters"); + } + } + + @DisplayName(".addAnnotationProcessorOptions(List) tests") + @Nested + class AnnotationProcessorOptionsTest { + + @DisplayName("Setting .annotationProcessorOptions(List) adds the options") + @Test + void addsAnnotationProcessorOptions() { + // Given + var options = Stream + .generate(Fixtures::someText) + .limit(5) + .collect(Collectors.toList()); + + // When + flagBuilder.annotationProcessorOptions(options); + + // Then + assertThat(flagBuilder.build()) + .containsSequence(options.stream() + .map("-A"::concat) + .collect(Collectors.toList())); + } + + @DisplayName(".annotationProcessorOptions(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Given + var options = Stream + .generate(Fixtures::someText) + .limit(5) + .collect(Collectors.toList()); + + // Then + assertThat(flagBuilder.annotationProcessorOptions(options)) + .isSameAs(flagBuilder); + } + } + + @DisplayName(".compilerOptions(List) tests") + @Nested + class CompilerOptionsTest { + + @DisplayName("Setting .compilerOptions(List) adds the options") + @Test + void addsCompilerOptions() { + // Given + var options = Stream + .generate(Fixtures::someText) + .limit(5) + .collect(Collectors.toList()); + + // When + flagBuilder.compilerOptions(options); + + // Then + assertThat(flagBuilder.build()) + .containsSequence(options); + } + + @DisplayName(".compilerOptions(...) returns the flag builder") + @Test + void returnsFlagBuilder() { + // Given + var options = Stream + .generate(Fixtures::someText) + .limit(5) + .collect(Collectors.toList()); + + // Then + assertThat(flagBuilder.compilerOptions(options)) + .isSameAs(flagBuilder); + } + } + + @Order(Integer.MAX_VALUE - 1) + @DisplayName("The flag builder adds multiple flags correctly") + @Test + void addsMultipleFlagsCorrectly() { + // When + var flags = flagBuilder + .compilerOptions(List.of("--foo", "--bar")) + .release("15") + .annotationProcessorOptions(List.of("--baz", "--bork")) + .build(); + + // Then + assertThat(flags) + .containsExactly("--foo", "--bar", "--release", "15", "-A--baz", "-A--bork"); + } + + @Order(Integer.MAX_VALUE) + @DisplayName("The flag builder produces an immutable list as the result") + @Test + void resultIsImmutable() { + // When + var flags = flagBuilder + .compilerOptions(List.of("--foo", "--bar")) + .release("15") + .annotationProcessorOptions(List.of("--baz", "--bork")) + .build(); + + // Then + assertThatThrownBy(() -> flags.add("something")) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java new file mode 100644 index 00000000..252ddd27 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 - 2024, the original author or authors. + * + * 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.junit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.params.support.AnnotationConsumerInitializer.initialize; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompilerConfigurer; +import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl; +import java.lang.reflect.AnnotatedElement; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * {@link EcjCompilersProvider} tests. + */ +@DisplayName("EcjCompilersProvider tests") +class EcjCompilersProviderTest { + + @DisplayName("Provider uses the user-provided compiler version bounds when valid") + @Test + void providerUsesTheUserProvidedVersionRangesWhenValid() { + // Given + try (var ecjMock = mockStatic(EcjJctCompilerImpl.class)) { + ecjMock.when(EcjJctCompilerImpl::getEarliestSupportedVersionInt).thenReturn(8); + ecjMock.when(EcjJctCompilerImpl::getLatestSupportedVersionInt).thenReturn(17); + var annotation = someAnnotation(10, 15); + var test = someAnnotatedElement(annotation); + var context = mock(ExtensionContext.class); + + // When + var consumer = initialize(test, new EcjCompilersProvider()); + var compilers = consumer.provideArguments(context) + .map(args -> (EcjJctCompilerImpl) args.get()[0]) + .toList(); + + // Then + assertThat(compilers) + .as("compilers that were initialised (%s)", compilers) + .hasSize(6); + + assertSoftly(softly -> { + for (var i = 0; i < compilers.size(); ++i) { + var compiler = compilers.get(i); + softly.assertThat(compiler.getName()) + .as("compilers[%d].getName()", i) + .isEqualTo("ECJ (release = Java %d)", 10 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 10 + i); + } + }); + } + } + + @DisplayName("Provider uses the minimum compiler version that is allowed if exceeded") + @Test + void providerUsesTheMinCompilerVersionAllowedIfExceeded() { + // Given + try (var ecjMock = mockStatic(EcjJctCompilerImpl.class)) { + ecjMock.when(EcjJctCompilerImpl::getEarliestSupportedVersionInt).thenReturn(8); + ecjMock.when(EcjJctCompilerImpl::getLatestSupportedVersionInt).thenReturn(17); + var annotation = someAnnotation(1, 15); + var test = someAnnotatedElement(annotation); + var context = mock(ExtensionContext.class); + + // When + var consumer = initialize(test, new EcjCompilersProvider()); + var compilers = consumer.provideArguments(context) + .map(args -> (EcjJctCompilerImpl) args.get()[0]) + .toList(); + + // Then + assertThat(compilers) + .as("compilers that were initialised (%s)", compilers) + .hasSize(8); + + assertSoftly(softly -> { + for (var i = 0; i < compilers.size(); ++i) { + var compiler = compilers.get(i); + softly.assertThat(compiler.getName()) + .as("compilers[%d].getName()", i) + .isEqualTo("ECJ (release = Java %d)", 8 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 8 + i); + } + }); + } + } + + @DisplayName("Provider uses the maximum compiler version that is allowed if exceeded") + @Test + void providerUsesTheMaxCompilerVersionAllowedIfExceeded() { + // Given + try (var ecjMock = mockStatic(EcjJctCompilerImpl.class)) { + ecjMock.when(EcjJctCompilerImpl::getEarliestSupportedVersionInt).thenReturn(8); + ecjMock.when(EcjJctCompilerImpl::getLatestSupportedVersionInt).thenReturn(17); + var annotation = someAnnotation(10, 17); + var test = someAnnotatedElement(annotation); + var context = mock(ExtensionContext.class); + + // When + var consumer = initialize(test, new EcjCompilersProvider()); + var compilers = consumer.provideArguments(context) + .map(args -> (EcjJctCompilerImpl) args.get()[0]) + .toList(); + + // Then + assertThat(compilers) + .as("compilers that were initialised (%s)", compilers) + .hasSize(8); + + assertSoftly(softly -> { + for (var i = 0; i < compilers.size(); ++i) { + var compiler = compilers.get(i); + softly.assertThat(compiler.getName()) + .as("compilers[%d].getName()", i) + .isEqualTo("ECJ (release = Java %d)", 10 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 10 + i); + } + }); + } + } + + @SafeVarargs + final EcjCompilerTest someAnnotation( + int min, + int max, + Class>... configurers + ) { + var annotation = mock(EcjCompilerTest.class); + when(annotation.minVersion()).thenReturn(min); + when(annotation.maxVersion()).thenReturn(max); + when(annotation.configurers()).thenReturn(configurers); + when(annotation.versionStrategy()).thenReturn(VersionStrategy.RELEASE); + when(annotation.annotationType()).thenAnswer(ctx -> EcjCompilerTest.class); + return annotation; + } + + AnnotatedElement someAnnotatedElement(EcjCompilerTest annotation) { + var element = mock(AnnotatedElement.class); + when(element.getDeclaredAnnotation(EcjCompilerTest.class)).thenReturn(annotation); + return element; + } +} diff --git a/pom.xml b/pom.xml index dc2c44df..71d42b36 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ 3.27.0 4.2.2 + 3.41.0-SNAPSHOT 1.4.0 1.0.0 5.11.4 @@ -202,6 +203,13 @@ ${awaitility.version} + + + org.eclipse.jdt + ecj + ${ecj.version} + + org.jspecify jspecify diff --git a/scripts/add-development-ecj-to-maven-repository.sh b/scripts/add-development-ecj-to-maven-repository.sh new file mode 100755 index 00000000..f7425f8f --- /dev/null +++ b/scripts/add-development-ecj-to-maven-repository.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 - 2024, the original author or authors. +# +# 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. +# + +### +### Shortcut to injecting a development version of ECJ's JAR into the local Maven registry. +### + +set -o errexit +set -o nounset +[[ -n ${DEBUG+defined} ]] && set -o xtrace + +if [[ $# -ne 2 ]]; then + echo "USAGE: ${BASH_SOURCE[0]} " + echo "Inject the given URL to an ECJ JAR as the given version in the local Maven repository." + echo "" + echo "Arguments:" + echo " The URL to the ECJ JAR to use." + echo " The version number to use for that JAR." + echo "" + exit 1 +fi + +url=$1 +version=$2 + +maven_repository_dir=${M2_HOME:-${HOME}/.m2}/repository +target_dir=${maven_repository_dir}/org/eclipse/jdt/ecj/${version} + +if [[ -d ${target_dir} ]]; then + echo "Clearing existing directory out..." + rm -Rvf "${target_dir}" +fi + +echo "Making ECJ directory" +mkdir "${target_dir}" + +echo "Working out the latest ECJ POM to use..." +latest_published_version=$(curl --fail --silent https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/maven-metadata.xml \ + | grep -oE ".+?" \ + | sed -E 's@@@g' \ + | tail -n 1) + +echo "Making ECJ POM derived from the POM for v${latest_published_version}" +curl --fail --silent https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/"${latest_published_version}"/ecj-"${latest_published_version}".pom \ + | sed 's@'${latest_published_version}'@'${version}'@g' \ + > "${target_dir}/ecj-${version}.pom" + +echo "Downloading ECJ JAR" +curl --fail --silent "${url}" > "${target_dir}/ecj-${version}.jar" + +echo "Computing SHA1 digest of ECJ JAR" +sha1sum < "${target_dir}/ecj-${version}.jar" | cut -d ' ' -f 1 > "${target_dir}/ecj-${version}.jar.sha1" + +echo "Making dummy _remote.repositories file" + +cat > "${target_dir}/_remote.repositories" <<-EOF +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#$(date) +ecj-${version}.jar>central= +ecj-${version}.pom>central= +EOF + +echo "Done." diff --git a/scripts/ecj.sh b/scripts/ecj.sh new file mode 100755 index 00000000..cee74ae0 --- /dev/null +++ b/scripts/ecj.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 - 2024, the original author or authors. +# +# 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. +# + +### +### Shortcut to running the ECJ compiler. +### + +set -o errexit +set -o nounset +[[ -n ${DEBUG+defined} ]] && set -o xtrace + +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ecj_dir="${project_dir}/target/ecj" +if [[ ! -d "${ecj_dir}" ]] || [[ ! "$(ls -A "${ecj_dir}")" ]]; then + mkdir -p "${ecj_dir}" + + echo "[[Determining ECJ version to use, please wait...]]" >&2 + ecj_version="$("${project_dir}/mvnw" -f "${project_dir}/pom.xml" help:evaluate \ + --offline \ + --quiet \ + -Dexpression="ecj.version" \ + -DforceStdout)" + + echo "[[Downloading ECJ ${ecj_version} artifact, please wait...]]" >&2 + "${project_dir}/mvnw" dependency:get \ + --quiet \ + -Dartifact="org.eclipse.jdt:ecj:${ecj_version}" + + echo "[[Copying ECJ ${ecj_version} artifact into ${ecj_dir}, please wait...]]" >&2 + "${project_dir}/mvnw" dependency:copy \ + --offline \ + --quiet \ + -Dartifact="org.eclipse.jdt:ecj:${ecj_version}" \ + -DoutputDirectory="${ecj_dir}" \ + -Dtransitive=true + + echo "[[Completed download of ECJ ${ecj_version}.]]" >&2 +fi + +java -jar "$(find "${ecj_dir}" -type f -name "*.jar" -print | head -n 1)" "${@}"