diff --git a/CHANGES.md b/CHANGES.md index 5fac10fd37..bc7c2ce002 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Changes +- Add embedded lockfiles to Eclipse JDT (4.9, 4.11, 4.39, 4.40), bump version to latest `4.39` -> `4.40`. ([#1996](https://github.com/diffplug/spotless/issues/1996)) ## [4.9.0] - 2026-07-27 ### Added diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java index f9748217be..462c9832c6 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java @@ -18,7 +18,10 @@ import static java.util.stream.Collectors.toMap; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -28,6 +31,7 @@ import javax.annotation.Nullable; +import com.diffplug.common.base.Errors; import com.diffplug.common.collect.ImmutableMap; import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; @@ -122,6 +126,10 @@ protected void addPlatformRepo(P2Model model, String version) { /** Returns the FormatterStep (whose state will be calculated lazily). */ public FormatterStep build() { var roundtrippableState = new EquoStep(formatterVersion, settingProperties, settingXml, FileSignature.promise(settingsFiles), JarState.promise(() -> { + List lockfileDependencies = readEmbeddedLockfileDependencies(formatterVersion); + if (lockfileDependencies != null) { + return JarState.withoutTransitives(lockfileDependencies, mavenProvisioner); + } P2Model model = createModelWithMirrors(); P2ModelWrapper modelWrapper = P2ModelWrapper.wrap(model); List classpath = p2Provisioner.provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory).stream() @@ -133,6 +141,54 @@ public FormatterStep build() { return FormatterStep.create(formatterName, roundtrippableState, EquoStep::state, stateToFormatter); } + private @Nullable List readEmbeddedLockfileDependencies(String version) { + String lockfileResourcePath = lockfileResourcePath(version); + if (lockfileResourcePath == null) { + return null; + } + if (!lockfileResourcePath.startsWith("/")) { + throw new IllegalArgumentException("Lockfile resource path must start with '/': " + lockfileResourcePath); + } + InputStream lockfile = EquoBasedStepBuilder.class.getResourceAsStream(lockfileResourcePath); + if (lockfile == null) { + // No lockfile embedded for this version — fall back to P2 provisioning + return null; + } + String allLines; + try (lockfile) { + allLines = new String(lockfile.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw Errors.asRuntime(e); + } + var dependencies = new ArrayList(); + for (String line : allLines.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + dependencies.add(trimmed); + } + } + if (dependencies.isEmpty()) { + throw new IllegalArgumentException("No dependencies defined in lockfile " + lockfileResourcePath); + } + return dependencies; + } + + /** + * Returns the classpath resource path of an embedded lockfile for the given formatter version. + *

+ * The default implementation always returns {@code null}, which means dependency resolution + * falls back to P2 provisioning. + *

+ * Overriding implementations should return an absolute classpath resource path (starting with + * {@code /}) that is compatible with {@link Class#getResourceAsStream(String)}. + * + * @return absolute classpath resource path of the embedded lockfile, or {@code null} to use + * P2 provisioning + */ + protected @Nullable String lockfileResourcePath(String version) { + return null; + } + private P2Model createModelWithMirrors() { P2Model model = model(formatterVersion); if (p2Mirrors.isEmpty()) { diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java index d140d03042..961d889334 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java @@ -35,7 +35,7 @@ public final class EclipseJdtFormatterStep { private EclipseJdtFormatterStep() {} private static final String NAME = "eclipse jdt formatter"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "4.39"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "4.40"); public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); @@ -76,6 +76,11 @@ protected P2Model model(String version) { return model; } + @Override + protected String lockfileResourcePath(String version) { + return "/com/diffplug/spotless/extra/eclipse_jdt_formatter/v" + version + ".lockfile"; + } + @Override public void setVersion(String version) { if (version.endsWith(".0")) { diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile new file mode 100644 index 0000000000..47a5454c25 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.11 +org.eclipse.jdt:org.eclipse.jdt.core:3.17.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.300 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.300 +org.eclipse.platform:org.eclipse.core.expressions:3.6.300 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.300 +org.eclipse.platform:org.eclipse.core.jobs:3.10.300 +org.eclipse.platform:org.eclipse.core.resources:3.13.300 +org.eclipse.platform:org.eclipse.core.runtime:3.15.200 +org.eclipse.platform:org.eclipse.equinox.app:1.4.100 +org.eclipse.platform:org.eclipse.equinox.common:3.10.300 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.300 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.300 +org.eclipse.platform:org.eclipse.equinox.supplement:1.8.200 +org.eclipse.platform:org.eclipse.osgi:3.13.300 +org.eclipse.platform:org.eclipse.text:3.8.100 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile new file mode 100644 index 0000000000..c51599e651 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile @@ -0,0 +1,17 @@ +# Spotless formatter based on Eclipse-JDT 4.25 +org.eclipse.jdt:org.eclipse.jdt.core:3.31.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.200 +org.eclipse.platform:org.eclipse.core.expressions:3.8.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.500 +org.eclipse.platform:org.eclipse.core.jobs:3.13.100 +org.eclipse.platform:org.eclipse.core.resources:3.18.0 +org.eclipse.platform:org.eclipse.core.runtime:3.26.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.200 +org.eclipse.platform:org.eclipse.equinox.common:3.16.200 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.600 +org.eclipse.platform:org.eclipse.osgi:3.18.100 +org.eclipse.platform:org.eclipse.text:3.12.200 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile new file mode 100644 index 0000000000..b4c053796e --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile @@ -0,0 +1,17 @@ +# Spotless formatter based on Eclipse-JDT 4.26 +org.eclipse.jdt:org.eclipse.jdt.core:3.32.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.300 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.200 +org.eclipse.platform:org.eclipse.core.expressions:3.8.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.500 +org.eclipse.platform:org.eclipse.core.jobs:3.13.200 +org.eclipse.platform:org.eclipse.core.resources:3.18.100 +org.eclipse.platform:org.eclipse.core.runtime:3.26.100 +org.eclipse.platform:org.eclipse.equinox.app:1.6.200 +org.eclipse.platform:org.eclipse.equinox.common:3.17.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.600 +org.eclipse.platform:org.eclipse.osgi:3.18.200 +org.eclipse.platform:org.eclipse.text:3.12.300 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile new file mode 100644 index 0000000000..e86e95e3d3 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.39 +org.eclipse.jdt:org.eclipse.jdt.core:3.45.0 +net.java.dev.jna:jna-platform:5.18.1 +org.eclipse.jdt:ecj:3.45.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.500 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.800 +org.eclipse.platform:org.eclipse.core.expressions:3.9.500 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.400 +org.eclipse.platform:org.eclipse.core.jobs:3.15.700 +org.eclipse.platform:org.eclipse.core.resources:3.23.200 +org.eclipse.platform:org.eclipse.core.runtime:3.34.200 +org.eclipse.platform:org.eclipse.equinox.app:1.7.600 +org.eclipse.platform:org.eclipse.equinox.common:3.20.300 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.200 +org.eclipse.platform:org.eclipse.osgi:3.24.100 +org.eclipse.platform:org.eclipse.text:3.14.600 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile new file mode 100644 index 0000000000..f30d63dc8c --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.40 +org.eclipse.jdt:org.eclipse.jdt.core:3.46.0 +net.java.dev.jna:jna-platform:5.18.1 +org.eclipse.jdt:ecj:3.46.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.500 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.800 +org.eclipse.platform:org.eclipse.core.expressions:3.9.600 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.400 +org.eclipse.platform:org.eclipse.core.jobs:3.15.800 +org.eclipse.platform:org.eclipse.core.resources:3.24.0 +org.eclipse.platform:org.eclipse.core.runtime:3.34.200 +org.eclipse.platform:org.eclipse.equinox.app:1.7.600 +org.eclipse.platform:org.eclipse.equinox.common:3.20.400 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.300 +org.eclipse.platform:org.eclipse.osgi:3.24.200 +org.eclipse.platform:org.eclipse.text:3.14.700 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile new file mode 100644 index 0000000000..19ddfb2389 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.9 +org.eclipse.jdt:org.eclipse.jdt.core:3.15.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.100 +org.eclipse.platform:org.eclipse.core.expressions:3.6.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.200 +org.eclipse.platform:org.eclipse.core.jobs:3.10.100 +org.eclipse.platform:org.eclipse.core.resources:3.13.100 +org.eclipse.platform:org.eclipse.core.runtime:3.15.0 +org.eclipse.platform:org.eclipse.equinox.app:1.3.600 +org.eclipse.platform:org.eclipse.equinox.common:3.10.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.8.100 +org.eclipse.platform:org.eclipse.osgi:3.13.100 +org.eclipse.platform:org.eclipse.text:3.7.0 diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java new file mode 100644 index 0000000000..b6d859e559 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2026 DiffPlug + * + * 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 com.diffplug.spotless.extra; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.StepHarness; + +import dev.equo.solstice.p2.P2Model; + +class EquoBasedStepBuilderLockfileTest { + + @Test + void missingEmbeddedLockfileFallsBackToP2() throws Exception { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + when(p2Provisioner.provisionP2Dependencies(any(), any(), any())).thenReturn(List.of()); + EquoBasedStepBuilder builder = builderWithLockfilePath("/com/diffplug/spotless/extra/missing.lockfile", p2Provisioner, mavenProvisioner); + StepHarness.forStep(builder.build()).test("class T {}", "class T {}"); + verify(p2Provisioner).provisionP2Dependencies(any(), any(), any()); + verifyNoInteractions(mavenProvisioner); + } + + @Test + void lockfilePathMustBeAbsolute() { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + EquoBasedStepBuilder builder = builderWithLockfilePath("com/diffplug/spotless/extra/empty.lockfile", + p2Provisioner, mavenProvisioner); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> StepHarness.forStep(builder.build()).test("class T {}", "class T {}")); + assertTrue(exception.getMessage().contains("must start with '/'")); + verifyNoInteractions(p2Provisioner, mavenProvisioner); + } + + @Test + void emptyEmbeddedLockfileThrows() { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + EquoBasedStepBuilder builder = builderWithLockfilePath("/com/diffplug/spotless/extra/empty.lockfile", + p2Provisioner, mavenProvisioner); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> StepHarness.forStep(builder.build()).test("class T {}", "class T {}")); + assertTrue(exception.getMessage().contains("No dependencies defined in lockfile")); + verifyNoInteractions(p2Provisioner, mavenProvisioner); + } + + @Test + void embeddedLockfileUsesWithoutTransitivesAndAllCoordinates(@TempDir Path tempDir) throws Exception { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + File dummyJar = tempDir.resolve("spotless-lockfile-test.jar").toFile(); + assertTrue(dummyJar.createNewFile()); + when(mavenProvisioner.provisionWithTransitives(eq(false), anyCollection())).thenReturn(Set.of(dummyJar)); + EquoBasedStepBuilder builder = builderWithLockfilePath("/com/diffplug/spotless/extra/two-deps.lockfile", + p2Provisioner, mavenProvisioner); + + StepHarness.forStep(builder.build()).test("class T {}", "class T {}"); + + verify(mavenProvisioner).provisionWithTransitives(eq(false), assertArg((Collection coords) -> assertThat(coords) + .containsExactlyInAnyOrder( + "org.eclipse.jdt:org.eclipse.jdt.core:3.45.0", + "org.eclipse.jdt:ecj:3.45.0"))); + verifyNoInteractions(p2Provisioner); + } + + private static EquoBasedStepBuilder builderWithLockfilePath(String lockfilePath, P2Provisioner p2Provisioner, Provisioner mavenProvisioner) { + return new EquoBasedStepBuilder( + "lockfile test formatter", + mavenProvisioner, + p2Provisioner, + "4.40", + state -> input -> input, + ImmutableMap.builder()) { + @Override + protected P2Model model(String version) { + return new P2Model(); + } + + @Override + protected String lockfileResourcePath(String version) { + return lockfilePath; + } + }; + } + +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java index ff8eeaeae7..e69549da6f 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java @@ -15,19 +15,37 @@ */ package com.diffplug.spotless.extra.java; -import java.util.stream.Stream; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.FieldSource; +import com.diffplug.spotless.StepHarnessWithFile; import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Provisioner; import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; class EclipseJdtFormatterStepTest extends EquoResourceHarness { + + /** + * Embedded lockfile coverage includes both dependency styles: + *

    + *
  • Range-based Maven POM dependencies: 4.9, 4.11, and 4.25
  • + *
  • Exact Maven POM dependencies: 4.26, 4.39, and the default version
  • + *
+ * The cutoff aligns with + * eclipse-platform/eclipse.platform.releng#135, + * which switched Maven dependency mapping from OSGi ranges to resolved concrete versions. + */ + private static final List EMBEDDED_LOCKFILE_VERSIONS = List.of("4.9", "4.11", "4.25", "4.26", "4.39", EclipseJdtFormatterStep.defaultVersion()); + private static EquoBasedStepBuilder createBuilder() { return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); } @@ -37,15 +55,24 @@ public EclipseJdtFormatterStepTest() { } @ParameterizedTest - @MethodSource + @FieldSource("EMBEDDED_LOCKFILE_VERSIONS") void formatWithVersion(String version) throws Exception { harnessFor(version).test("test.java", "package p; class C{}", "package p;\nclass C {\n}"); } - private static Stream formatWithVersion() { - return Stream.of("4.9", EclipseJdtFormatterStep.defaultVersion()); + @ParameterizedTest + @FieldSource("EMBEDDED_LOCKFILE_VERSIONS") + void embeddedLockfileVersionsDoNotUseP2(String version) { + P2Provisioner p2Provisioner = mock(); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), p2Provisioner); + builder.setVersion(version); + StepHarnessWithFile.forStep(this, builder.build()).test( + "test.java", + "package p; class C{}", + "package p;\nclass C {\n}"); + verifyNoInteractions(p2Provisioner); } /** New format interface requires source file information to distinguish module-info from compilation unit */ diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java new file mode 100644 index 0000000000..f370291eca --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java @@ -0,0 +1,251 @@ +/* + * Copyright 2016-2026 DiffPlug + * + * 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 com.diffplug.spotless.extra.java; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +import dev.equo.solstice.p2.P2ClientCache; +import dev.equo.solstice.p2.P2Model; +import dev.equo.solstice.p2.P2QueryCache; +import dev.equo.solstice.p2.P2QueryResult; + +/** + * Executable class for validating or updating embedded JDT lockfiles from Eclipse P2 metadata. + *

+ * Intended for manual execution via the following entrypoints: + *

+ * - {@link EclipseJdtLockfileMetadataTool.Verify#main(String[])}
+ * - {@link EclipseJdtLockfileMetadataTool.Update#main(String[])}
+ * 
+ */ +public class EclipseJdtLockfileMetadataTool { + + // Full explicit lockfiles can be produced for any target by taking Solstice's P2-resolved Maven + // coordinates directly (already fully version-resolved), then writing them as a lockfile. + private static final List TARGET_VERSIONS = List.of("4.9", "4.11", "4.25", "4.26", "4.39", "4.40"); + + /** + * Verifies the JDT lockfiles at {@link #TARGET_VERSIONS} against Eclipse P2 metadata. + */ + public static class Verify { + + public static void main(String[] args) { + run(false); + } + + } + + /** + * Updates the JDT lockfiles at {@link #TARGET_VERSIONS} from Eclipse P2 metadata. + *

+ * Missing lockfiles will be created. + */ + public static class Update { + + public static void main(String[] args) { + run(true); + } + + } + + private static final String JDT_ID = "org.eclipse.jdt.core"; + private static final String JDT_MAVEN_KEY = "org.eclipse.jdt:org.eclipse.jdt.core"; + private static final List ECLIPSE_UPDATE_BASE_URLS = List.of( + "https://download.eclipse.org/eclipse/updates/", + "https://archive.eclipse.org/eclipse/updates/"); + + private static void run(boolean update) { + Path lockfileDir = lockfileDir(); + Map lockfilesByVersion = targetLockfiles(lockfileDir, TARGET_VERSIONS); + + int checked = 0; + int failures = 0; + for (Map.Entry entry : lockfilesByVersion.entrySet()) { + checked++; + String eclipseVersion = entry.getKey(); + Path lockfilePath = entry.getValue(); + try { + List allCoordinates = resolveTransitiveClosure(eclipseVersion); + String rootCoordinate = allCoordinates.get(0); + if (update) { + Files.writeString(lockfilePath, lockfileContent(eclipseVersion, allCoordinates), StandardCharsets.UTF_8); + System.out.println("WROTE v" + eclipseVersion + " -> " + allCoordinates.size() + " artifact(s), root: " + rootCoordinate); + } else { + if (!Files.exists(lockfilePath)) { + System.err.println("MISSING v" + eclipseVersion + " -> " + lockfilePath + " (expected root: " + rootCoordinate + ")"); + failures++; + continue; + } + List actualCoordinates = lockfileCoordinates(lockfilePath); + String mismatch = mismatchMessage(eclipseVersion, allCoordinates, actualCoordinates); + if (mismatch != null) { + System.err.println(mismatch); + failures++; + continue; + } + System.out.println("OK v" + eclipseVersion + " -> " + rootCoordinate + " (" + allCoordinates.size() + " coordinates)"); + } + } catch (Exception e) { + System.err.println("ERROR v" + eclipseVersion + " -> " + e.getMessage()); + failures++; + } + } + if (update) { + System.out.println("Updated " + checked + " lockfile(s)."); + } else { + System.out.println("Verified " + checked + " lockfile(s) with " + failures + " issue(s)."); + } + } + + private static Map targetLockfiles(Path lockfileDir, List versions) { + Map targets = new LinkedHashMap<>(); + for (String version : versions) { + targets.put(version, lockfileDir.resolve("v" + version + ".lockfile")); + } + return targets; + } + + private static String lockfileContent(String eclipseVersion, List coordinates) { + String prefix = "# Spotless formatter based on Eclipse-JDT " + eclipseVersion + "\n"; + StringJoiner joiner = new StringJoiner("\n", prefix, "\n"); + coordinates.forEach(joiner::add); + return joiner.toString(); + } + + private static List lockfileCoordinates(Path lockfilePath) throws IOException { + try (var lines = Files.lines(lockfilePath, StandardCharsets.UTF_8)) { + List coordinates = lines.map(String::trim) + .filter(line -> !line.isEmpty()) + .filter(line -> !line.startsWith("#")) + .toList(); + if (coordinates.isEmpty()) { + throw new IllegalArgumentException("No dependency coordinate found in " + lockfilePath); + } + return coordinates; + } + } + + static String mismatchMessage(String eclipseVersion, List expectedCoordinates, List actualCoordinates) { + if (expectedCoordinates.equals(actualCoordinates)) { + return null; + } + String expectedRoot = expectedCoordinates.isEmpty() ? "" : expectedCoordinates.get(0); + String actualRoot = actualCoordinates.isEmpty() ? "" : actualCoordinates.get(0); + return "MISMATCH v" + eclipseVersion + " -> expected root: " + expectedRoot + " (" + expectedCoordinates.size() + + " coordinates), actual: " + actualRoot + " (" + actualCoordinates.size() + " coordinates)"; + } + + private static Path lockfileDir() { + Path fromRepoRoot = Path.of("lib-extra", "src", "main", "resources", "com", "diffplug", "spotless", "extra", "eclipse_jdt_formatter"); + if (Files.isDirectory(fromRepoRoot)) { + return fromRepoRoot; + } + Path fromLibExtra = Path.of("src", "main", "resources", "com", "diffplug", "spotless", "extra", "eclipse_jdt_formatter"); + if (Files.isDirectory(fromLibExtra)) { + return fromLibExtra; + } + throw new IllegalStateException("Unable to locate eclipse_jdt_formatter resource directory"); + } + + /** + * Resolves full, explicit Maven coordinates from the P2 query result for the given Eclipse version. + *

+ * This supports both exact and range-based historical metadata because the P2 solver has already + * chosen concrete versions before exposing Maven coordinates. + */ + private static List resolveTransitiveClosure(String eclipseVersion) throws Exception { + P2QueryResult query = queryJdtFromP2(eclipseVersion); + + // "g:a" -> selected version + Map selected = new LinkedHashMap<>(); + for (String coordinate : query.getJarsOnMavenCentral()) { + String[] parts = coordinate.split(":"); + if (parts.length != 3) { + throw new IllegalStateException("Expected Maven coordinate g:a:v but got: " + coordinate); + } + String groupId = parts[0], artifactId = parts[1], version = parts[2]; + String key = groupId + ":" + artifactId; + String existing = selected.get(key); + if (existing == null || compareVersions(existing, version) < 0) { + selected.put(key, version); + } + } + String rootVersion = selected.remove(JDT_MAVEN_KEY); + if (rootVersion == null) { + throw new IllegalStateException("P2 result for Eclipse " + eclipseVersion + " did not contain " + JDT_MAVEN_KEY); + } + List result = new ArrayList<>(); + result.add(JDT_MAVEN_KEY + ":" + rootVersion); + selected.entrySet().stream() + .map(e -> e.getKey() + ":" + e.getValue()) + .sorted() + .forEach(result::add); + return result; + } + + private static P2QueryResult queryJdtFromP2(String eclipseVersion) throws Exception { + Exception lastError = null; + for (String baseUrl : ECLIPSE_UPDATE_BASE_URLS) { + try { + P2Model model = new P2Model(); + addPlatformRepo(model, eclipseVersion, baseUrl); + model.getInstall().add(JDT_ID); + return model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); + } catch (Exception e) { + lastError = e; + } + } + throw new IllegalStateException("Failed to query Eclipse " + eclipseVersion + " from known update sites", lastError); + } + + private static void addPlatformRepo(P2Model model, String version, String baseUrl) { + if (!version.startsWith("4.")) { + throw new IllegalArgumentException("Expected 4.x"); + } + model.addP2Repo(baseUrl + version + "/"); + } + + /** Compares two version strings numerically, segment by segment. Returns positive if v1 > v2. */ + private static int compareVersions(String v1, String v2) { + String[] p1 = v1.split("\\."); + String[] p2 = v2.split("\\."); + int len = Math.max(p1.length, p2.length); + for (int i = 0; i < len; i++) { + int n1 = i < p1.length ? parseVersionSegment(p1[i]) : 0; + int n2 = i < p2.length ? parseVersionSegment(p2[i]) : 0; + if (n1 != n2) + return Integer.compare(n1, n2); + } + return 0; + } + + private static int parseVersionSegment(String segment) { + try { + return Integer.parseInt(segment); + } catch (NumberFormatException e) { + return 0; + } + } + +} diff --git a/lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile b/lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile new file mode 100644 index 0000000000..d25c36fa9a --- /dev/null +++ b/lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile @@ -0,0 +1 @@ +# intentionally empty lockfile used by EquoBasedStepBuilderLockfileTest diff --git a/lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile b/lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile new file mode 100644 index 0000000000..757276a5bb --- /dev/null +++ b/lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile @@ -0,0 +1,3 @@ +# test lockfile +org.eclipse.jdt:org.eclipse.jdt.core:3.45.0 +org.eclipse.jdt:ecj:3.45.0 diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 296d18c172..44a624acba 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Changes +- Add embedded lockfiles to Eclipse JDT (4.9, 4.11, 4.39, 4.40), bump version to latest `4.39` -> `4.40`. ([#1996](https://github.com/diffplug/spotless/issues/1996)) ## [8.9.0] - 2026-07-27 ### Added diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index d87cc8bedc..0adbad27d1 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Changes +- Add embedded lockfiles to Eclipse JDT (4.9, 4.11, 4.39, 4.40), bump version to latest `4.39` -> `4.40`. ([#1996](https://github.com/diffplug/spotless/issues/1996)) ## [3.9.0] - 2026-07-27 ### Added