From 3802edb46974992ef2e874beeb43538a1e8e0c3e Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sat, 25 Jul 2026 13:04:53 +0200 Subject: [PATCH 1/5] Add Eclipse JDT embedded lockfiles, upgrade formatter version to 4.40 --- .../spotless/extra/EquoBasedStepBuilder.java | 58 ++++ .../extra/java/EclipseJdtFormatterStep.java | 7 +- .../eclipse_jdt_formatter/v4.11.lockfile | 2 + .../eclipse_jdt_formatter/v4.39.lockfile | 2 + .../eclipse_jdt_formatter/v4.40.lockfile | 2 + .../extra/eclipse_jdt_formatter/v4.9.lockfile | 2 + .../EquoBasedStepBuilderLockfileTest.java | 93 ++++++ .../java/EclipseJdtFormatterStepTest.java | 27 +- .../java/EclipseJdtLockfileMetadataTool.java | 278 ++++++++++++++++++ .../diffplug/spotless/extra/empty.lockfile | 1 + 10 files changed, 466 insertions(+), 6 deletions(-) create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile create mode 100644 lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java create mode 100644 lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java create mode 100644 lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile 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..4d0a1ef624 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.from(lockfileDependencies, mavenProvisioner); + } P2Model model = createModelWithMirrors(); P2ModelWrapper modelWrapper = P2ModelWrapper.wrap(model); List classpath = p2Provisioner.provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory).stream() @@ -133,6 +141,56 @@ public FormatterStep build() { return FormatterStep.create(formatterName, roundtrippableState, EquoStep::state, stateToFormatter); } + @Nullable + private 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 + */ + @Nullable + protected 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..a16f9b3d35 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile @@ -0,0 +1,2 @@ +# Spotless formatter based on Eclipse-JDT 4.11 +org.eclipse.jdt:org.eclipse.jdt.core:3.17.0 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..1723597337 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile @@ -0,0 +1,2 @@ +# Spotless formatter based on Eclipse-JDT 4.39 +org.eclipse.jdt:org.eclipse.jdt.core:3.45.0 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..20212967ba --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile @@ -0,0 +1,2 @@ +# Spotless formatter based on Eclipse-JDT 4.40 +org.eclipse.jdt:org.eclipse.jdt.core:3.46.0 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..cbe0b690ea --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile @@ -0,0 +1,2 @@ +# Spotless formatter based on Eclipse-JDT 4.9 +org.eclipse.jdt:org.eclipse.jdt.core:3.15.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..32fc5ea1da --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java @@ -0,0 +1,93 @@ +/* + * 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.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +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.util.List; + +import org.junit.jupiter.api.Test; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.Provisioner; + +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); + } + + 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..1b204fa129 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,27 @@ */ 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 { + + private static final List EMBEDDED_LOCKFILE_VERSIONS = List.of("4.9", "4.11", "4.39", EclipseJdtFormatterStep.defaultVersion()); + private static EquoBasedStepBuilder createBuilder() { return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); } @@ -37,15 +45,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..602639eafc --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java @@ -0,0 +1,278 @@ +/* + * 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.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.jar.JarInputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +/** + * 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 { + + private static final List TARGET_VERSIONS = List.of("4.9", "4.11", "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_PREFIX = "org.eclipse.jdt:org.eclipse.jdt.core:"; + private static final Pattern MAJOR_MINOR_PATCH = Pattern.compile("^([0-9]+\\.[0-9]+\\.[0-9]+).*$"); + private static final int MAX_TRAVERSAL = 200; + + private static final HttpClient HTTP = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .build(); + + 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 { + String bundleVersion = resolveBundleVersion(eclipseVersion); + String expectedCoordinate = JDT_MAVEN_PREFIX + normalizeToMavenVersion(bundleVersion); + if (update) { + Files.writeString(lockfilePath, lockfileContent(eclipseVersion, expectedCoordinate), StandardCharsets.UTF_8); + System.out.println("WROTE v" + eclipseVersion + " -> " + expectedCoordinate); + } else { + if (!Files.exists(lockfilePath)) { + System.err.println("MISSING v" + eclipseVersion + " -> " + lockfilePath + " (expected: " + expectedCoordinate + ")"); + failures++; + continue; + } + String actualCoordinate = lockfileCoordinate(lockfilePath); + if (!expectedCoordinate.equals(actualCoordinate)) { + System.err.println("MISMATCH v" + eclipseVersion + " -> expected: " + expectedCoordinate + " actual: " + actualCoordinate); + failures++; + continue; + } + System.out.println("OK v" + eclipseVersion + " -> " + actualCoordinate); + } + } 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, String coordinate) { + return "# Spotless formatter based on Eclipse-JDT " + eclipseVersion + "\n" + coordinate + "\n"; + } + + private static String lockfileCoordinate(Path lockfilePath) throws IOException { + try (var lines = Files.lines(lockfilePath, StandardCharsets.UTF_8)) { + return lines.map(String::trim) + .filter(line -> !line.isEmpty()) + .filter(line -> !line.startsWith("#")) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No dependency coordinate found in " + lockfilePath)); + } + } + + 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"); + } + + private static String resolveBundleVersion(String eclipseVersion) throws Exception { + ArrayDeque queue = new ArrayDeque<>(); + Set visited = new LinkedHashSet<>(); + queue.add("https://download.eclipse.org/eclipse/updates/" + eclipseVersion + "/"); + int traversed = 0; + while (!queue.isEmpty()) { + String repoUrl = queue.removeFirst(); + if (!visited.add(repoUrl)) { + continue; + } + traversed++; + if (traversed > MAX_TRAVERSAL) { + throw new IllegalStateException("Traversal exceeded " + MAX_TRAVERSAL + " repositories while resolving Eclipse " + eclipseVersion); + } + Optional contentXml = readJarEntry(repoUrl, "content.jar", "content.xml"); + if (contentXml.isPresent()) { + String bundleVersion = extractBundleVersion(contentXml.get()); + if (bundleVersion != null) { + return bundleVersion; + } + } + Optional compositeXml = readJarEntry(repoUrl, "compositeContent.jar", "compositeContent.xml"); + if (compositeXml.isPresent()) { + queue.addAll(compositeChildren(repoUrl, compositeXml.get(), visited)); + } + } + throw new IllegalStateException("Unable to resolve " + JDT_ID + " from Eclipse " + eclipseVersion + " update site"); + } + + private static Optional readJarEntry(String repoUrl, String jarName, String entryName) throws Exception { + Optional bytes = download(repoUrl + jarName); + if (bytes.isEmpty()) { + return Optional.empty(); + } + try (JarInputStream jarInputStream = new JarInputStream(new ByteArrayInputStream(bytes.get()))) { + var entry = jarInputStream.getNextJarEntry(); + while (entry != null) { + if (!entry.isDirectory() && entryName.equals(entry.getName())) { + return Optional.of(new String(jarInputStream.readAllBytes(), StandardCharsets.UTF_8)); + } + entry = jarInputStream.getNextJarEntry(); + } + } + throw new IllegalStateException("Entry " + entryName + " not found in " + repoUrl + jarName); + } + + private static Optional download(String url) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build(); + HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() == 200) { + return Optional.of(response.body()); + } + if (response.statusCode() == 404) { + return Optional.empty(); + } + throw new IOException("Unexpected HTTP status " + response.statusCode() + " from " + url); + } + + private static String extractBundleVersion(String contentXml) throws Exception { + Document document = parseXml(contentXml); + var xpath = XPathFactory.newInstance().newXPath(); + String value = (String) xpath.evaluate("//unit[@id='" + JDT_ID + "']/@version", document, XPathConstants.STRING); + return value.isBlank() ? null : value; + } + + private static List compositeChildren(String parentRepoUrl, String compositeXml, Set visited) throws Exception { + Document document = parseXml(compositeXml); + var xpath = XPathFactory.newInstance().newXPath(); + var nodes = (NodeList) xpath.evaluate("//child/@location", document, XPathConstants.NODESET); + List children = new ArrayList<>(nodes.getLength()); + for (int i = 0; i < nodes.getLength(); i++) { + String location = nodes.item(i).getNodeValue(); + String childUrl = parentRepoUrl + trimTrailingSlash(location) + "/"; + if (!visited.contains(childUrl)) { + children.add(childUrl); + } + } + return children; + } + + private static String trimTrailingSlash(String value) { + return value.endsWith("/") ? value.substring(0, value.length() - 1) : value; + } + + private static Document parseXml(String xml) throws Exception { + var factory = DocumentBuilderFactory.newInstance(); + // Disable DTDs/external entities and enable secure processing to prevent XXE + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + factory.setExpandEntityReferences(false); + factory.setNamespaceAware(false); + var builder = factory.newDocumentBuilder(); + return builder.parse(new InputSource(new StringReader(xml))); + } + + private static String normalizeToMavenVersion(String bundleVersion) { + Matcher matcher = MAJOR_MINOR_PATCH.matcher(bundleVersion); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unexpected org.eclipse.jdt.core bundle version: " + bundleVersion); + } + return matcher.group(1); + } + +} 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 From 9c204fbf13ba9d9cd0d819ea6f4ff6d16e1c84e8 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sat, 25 Jul 2026 13:52:25 +0200 Subject: [PATCH 2/5] Add summary to `CHANGES.md` files --- CHANGES.md | 1 + plugin-gradle/CHANGES.md | 1 + plugin-maven/CHANGES.md | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 67dc8e1940..905c30e5b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Changes - Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989)) +- 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.8.0] - 2026-06-29 ### Added diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 17f8167d02..e789acc3d6 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -7,6 +7,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - Prevent parallel Gradle input fingerprinting from failing when `toggleOffOn()` wraps a slow lazy formatter step with no matching target files. ([#2994](https://github.com/diffplug/spotless/pull/2994)) ### Changes - Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989)) +- 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.8.0] - 2026-06-29 ### Added diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index a7e517860c..d62b6cf49f 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Changes - Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989)) +- 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.8.0] - 2026-06-29 ### Added From e70065e3b23611d6269ced13f0a05de383984cdc Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 28 Jul 2026 11:18:17 +0200 Subject: [PATCH 3/5] Fix format violations --- .../com/diffplug/spotless/extra/EquoBasedStepBuilder.java | 6 ++---- .../spotless/extra/EquoBasedStepBuilderLockfileTest.java | 2 +- .../spotless/extra/java/EclipseJdtLockfileMetadataTool.java | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) 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 4d0a1ef624..b4e6698f8c 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 @@ -141,8 +141,7 @@ public FormatterStep build() { return FormatterStep.create(formatterName, roundtrippableState, EquoStep::state, stateToFormatter); } - @Nullable - private List readEmbeddedLockfileDependencies(String version) { + private @Nullable List readEmbeddedLockfileDependencies(String version) { String lockfileResourcePath = lockfileResourcePath(version); if (lockfileResourcePath == null) { return null; @@ -186,8 +185,7 @@ private List readEmbeddedLockfileDependencies(String version) { * @return absolute classpath resource path of the embedded lockfile, or {@code null} to use * P2 provisioning */ - @Nullable - protected String lockfileResourcePath(String version) { + protected @Nullable String lockfileResourcePath(String version) { return null; } 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 index 32fc5ea1da..df7b37de8a 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java @@ -28,8 +28,8 @@ import org.junit.jupiter.api.Test; import com.diffplug.common.collect.ImmutableMap; -import com.diffplug.spotless.StepHarness; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.StepHarness; import dev.equo.solstice.p2.P2Model; 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 index 602639eafc..930b533e50 100644 --- 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 @@ -90,8 +90,8 @@ public static void main(String[] args) { private static final int MAX_TRAVERSAL = 200; private static final HttpClient HTTP = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .build(); + .followRedirects(Redirect.NORMAL) + .build(); private static void run(boolean update) { Path lockfileDir = lockfileDir(); From 8bb51946c0bccdb368f9398b4286ce929162e6f0 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 28 Jul 2026 13:50:51 +0200 Subject: [PATCH 4/5] Add missing transitive coordinates, remove lockfiles with underlying version ranges --- .../spotless/extra/EquoBasedStepBuilder.java | 2 +- .../eclipse_jdt_formatter/v4.11.lockfile | 2 - .../eclipse_jdt_formatter/v4.39.lockfile | 18 +++ .../eclipse_jdt_formatter/v4.40.lockfile | 18 +++ .../extra/eclipse_jdt_formatter/v4.9.lockfile | 2 - .../java/EclipseJdtFormatterStepTest.java | 2 +- .../java/EclipseJdtLockfileMetadataTool.java | 149 ++++++++++++++++-- 7 files changed, 175 insertions(+), 18 deletions(-) delete mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile delete mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile 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 b4e6698f8c..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 @@ -128,7 +128,7 @@ public FormatterStep build() { var roundtrippableState = new EquoStep(formatterVersion, settingProperties, settingXml, FileSignature.promise(settingsFiles), JarState.promise(() -> { List lockfileDependencies = readEmbeddedLockfileDependencies(formatterVersion); if (lockfileDependencies != null) { - return JarState.from(lockfileDependencies, mavenProvisioner); + return JarState.withoutTransitives(lockfileDependencies, mavenProvisioner); } P2Model model = createModelWithMirrors(); P2ModelWrapper modelWrapper = P2ModelWrapper.wrap(model); 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 deleted file mode 100644 index a16f9b3d35..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on Eclipse-JDT 4.11 -org.eclipse.jdt:org.eclipse.jdt.core:3.17.0 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 index 1723597337..9b82d35258 100644 --- 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 @@ -1,2 +1,20 @@ # 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 +net.java.dev.jna:jna: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.osgi:3.24.100 +org.eclipse.platform:org.eclipse.text:3.14.600 +org.osgi:org.osgi.service.prefs:1.1.2 +org.osgi:osgi.annotation:8.0.1 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 index 20212967ba..1b8309d0ba 100644 --- 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 @@ -1,2 +1,20 @@ # 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 +net.java.dev.jna:jna: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.700 +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.osgi:3.24.200 +org.eclipse.platform:org.eclipse.text:3.14.700 +org.osgi:org.osgi.service.prefs:1.1.2 +org.osgi:osgi.annotation:8.0.1 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 deleted file mode 100644 index cbe0b690ea..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on Eclipse-JDT 4.9 -org.eclipse.jdt:org.eclipse.jdt.core:3.15.0 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 1b204fa129..73d50baa33 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 @@ -34,7 +34,7 @@ class EclipseJdtFormatterStepTest extends EquoResourceHarness { - private static final List EMBEDDED_LOCKFILE_VERSIONS = List.of("4.9", "4.11", "4.39", EclipseJdtFormatterStep.defaultVersion()); + private static final List EMBEDDED_LOCKFILE_VERSIONS = List.of("4.39", EclipseJdtFormatterStep.defaultVersion()); private static EquoBasedStepBuilder createBuilder() { return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); 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 index 930b533e50..ffeb2a50cc 100644 --- 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 @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.StringJoiner; import java.util.jar.JarInputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,6 +45,7 @@ import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; @@ -58,7 +60,13 @@ */ public class EclipseJdtLockfileMetadataTool { - private static final List TARGET_VERSIONS = List.of("4.9", "4.11", "4.39", "4.40"); + // Lockfiles are only provided for Eclipse versions whose org.eclipse.jdt.core POM on Maven Central + // declares exact transitive dependency versions rather than version ranges (e.g. "[3.12.0,4.0.0)"). + // org.eclipse.jdt.core:3.32.0 is the first version on Maven Central with exact version dependencies; + // the likely cause is eclipse-platform/eclipse.platform.releng#135 (issue #128), which updated the + // CBI aggregator configuration to map bundle requirements to their resolved versions rather than the + // OSGi version ranges from MANIFEST.MF. + private static final List TARGET_VERSIONS = List.of("4.39", "4.40"); /** * Verifies the JDT lockfiles at {@link #TARGET_VERSIONS} against Eclipse P2 metadata. @@ -105,23 +113,24 @@ private static void run(boolean update) { Path lockfilePath = entry.getValue(); try { String bundleVersion = resolveBundleVersion(eclipseVersion); - String expectedCoordinate = JDT_MAVEN_PREFIX + normalizeToMavenVersion(bundleVersion); + String rootCoordinate = JDT_MAVEN_PREFIX + normalizeToMavenVersion(bundleVersion); if (update) { - Files.writeString(lockfilePath, lockfileContent(eclipseVersion, expectedCoordinate), StandardCharsets.UTF_8); - System.out.println("WROTE v" + eclipseVersion + " -> " + expectedCoordinate); + List allCoordinates = resolveTransitiveClosure(rootCoordinate); + 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: " + expectedCoordinate + ")"); + System.err.println("MISSING v" + eclipseVersion + " -> " + lockfilePath + " (expected root: " + rootCoordinate + ")"); failures++; continue; } - String actualCoordinate = lockfileCoordinate(lockfilePath); - if (!expectedCoordinate.equals(actualCoordinate)) { - System.err.println("MISMATCH v" + eclipseVersion + " -> expected: " + expectedCoordinate + " actual: " + actualCoordinate); + String actualRootCoordinate = lockfileRootCoordinate(lockfilePath); + if (!rootCoordinate.equals(actualRootCoordinate)) { + System.err.println("MISMATCH v" + eclipseVersion + " -> expected root: " + rootCoordinate + " actual: " + actualRootCoordinate); failures++; continue; } - System.out.println("OK v" + eclipseVersion + " -> " + actualCoordinate); + System.out.println("OK v" + eclipseVersion + " -> " + actualRootCoordinate); } } catch (Exception e) { System.err.println("ERROR v" + eclipseVersion + " -> " + e.getMessage()); @@ -143,11 +152,14 @@ private static Map targetLockfiles(Path lockfileDir, List return targets; } - private static String lockfileContent(String eclipseVersion, String coordinate) { - return "# Spotless formatter based on Eclipse-JDT " + eclipseVersion + "\n" + coordinate + "\n"; + 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 String lockfileCoordinate(Path lockfilePath) throws IOException { + private static String lockfileRootCoordinate(Path lockfilePath) throws IOException { try (var lines = Files.lines(lockfilePath, StandardCharsets.UTF_8)) { return lines.map(String::trim) .filter(line -> !line.isEmpty()) @@ -169,6 +181,119 @@ private static Path lockfileDir() { throw new IllegalStateException("Unable to locate eclipse_jdt_formatter resource directory"); } + /** + * Resolves the full Maven transitive closure of the given root coordinate using + * Maven Central POM traversal, with highest-version-wins conflict resolution. + *

+ * Optional dependencies and test/provided/system-scoped dependencies are excluded. + * The root coordinate appears first in the result; remaining entries are sorted. + */ + private static List resolveTransitiveClosure(String rootCoordinate) throws Exception { + // "g:a" -> selected version + Map selected = new LinkedHashMap<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(rootCoordinate.split(":")); + + while (!queue.isEmpty()) { + String[] parts = queue.removeFirst(); + 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) { + continue; + } + selected.put(key, version); + + List deps = fetchNonOptionalDepsFromPom(groupId, artifactId, version); + for (String[] dep : deps) { + String depKey = dep[0] + ":" + dep[1]; + String existingDepVersion = selected.get(depKey); + if (existingDepVersion == null || compareVersions(existingDepVersion, dep[2]) < 0) { + queue.add(dep); + } + } + } + + String[] rootParts = rootCoordinate.split(":"); + String rootKey = rootParts[0] + ":" + rootParts[1]; + List result = new ArrayList<>(); + result.add(rootCoordinate); + selected.entrySet().stream() + .filter(e -> !e.getKey().equals(rootKey)) + .map(e -> e.getKey() + ":" + e.getValue()) + .sorted() + .forEach(result::add); + return result; + } + + private static List fetchNonOptionalDepsFromPom(String groupId, String artifactId, String version) throws Exception { + String path = groupId.replace('.', '/') + "/" + artifactId + "/" + version + "/" + artifactId + "-" + version + ".pom"; + Optional bytes = download("https://repo1.maven.org/maven2/" + path); + if (bytes.isEmpty()) { + System.err.println("WARNING: POM not found on Maven Central: " + groupId + ":" + artifactId + ":" + version); + return List.of(); + } + return parseNonOptionalDepsFromPom(new String(bytes.get(), StandardCharsets.UTF_8)); + } + + private static List parseNonOptionalDepsFromPom(String pomXml) throws Exception { + Document doc = parseXml(pomXml); + var xpath = XPathFactory.newInstance().newXPath(); + NodeList deps = (NodeList) xpath.evaluate("/project/dependencies/dependency", doc, XPathConstants.NODESET); + List result = new ArrayList<>(); + for (int i = 0; i < deps.getLength(); i++) { + Element dep = (Element) deps.item(i); + String g = textContent(dep, "groupId"); + String a = textContent(dep, "artifactId"); + String v = textContent(dep, "version"); + String scope = textContent(dep, "scope"); + String optional = textContent(dep, "optional"); + String type = textContent(dep, "type"); + + if ("true".equals(optional)) + continue; + if ("test".equals(scope) || "provided".equals(scope) || "system".equals(scope)) + continue; + if ("pom".equals(type)) + continue; + if (v.isEmpty() || v.startsWith("[") || v.startsWith("(") || v.startsWith("$")) + continue; + if (g.isEmpty() || a.isEmpty()) + continue; + + result.add(new String[]{g, a, v}); + } + return result; + } + + private static String textContent(Element parent, String tag) { + var nodes = parent.getElementsByTagName(tag); + return nodes.getLength() > 0 ? nodes.item(0).getTextContent().trim() : ""; + } + + /** 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; + } + } + private static String resolveBundleVersion(String eclipseVersion) throws Exception { ArrayDeque queue = new ArrayDeque<>(); Set visited = new LinkedHashSet<>(); From 71c40eb575ce341785a21e69a668794b5e32631d Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 28 Jul 2026 15:44:40 +0200 Subject: [PATCH 5/5] Add support for version range based lockfiles, improve lockfile-first coverage --- .../eclipse_jdt_formatter/v4.11.lockfile | 16 + .../eclipse_jdt_formatter/v4.25.lockfile | 17 + .../eclipse_jdt_formatter/v4.26.lockfile | 17 + .../eclipse_jdt_formatter/v4.39.lockfile | 3 +- .../eclipse_jdt_formatter/v4.40.lockfile | 5 +- .../extra/eclipse_jdt_formatter/v4.9.lockfile | 16 + .../EquoBasedStepBuilderLockfileTest.java | 28 ++ .../java/EclipseJdtFormatterStepTest.java | 12 +- .../java/EclipseJdtLockfileMetadataTool.java | 290 +++++------------- .../diffplug/spotless/extra/two-deps.lockfile | 3 + 10 files changed, 180 insertions(+), 227 deletions(-) create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile create mode 100644 lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile create mode 100644 lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile 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 index 9b82d35258..e86e95e3d3 100644 --- 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 @@ -1,7 +1,6 @@ # 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 -net.java.dev.jna:jna: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 @@ -14,7 +13,7 @@ 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 -org.osgi:osgi.annotation:8.0.1 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 index 1b8309d0ba..f30d63dc8c 100644 --- 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 @@ -1,20 +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 -net.java.dev.jna:jna: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.700 +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 -org.osgi:osgi.annotation:8.0.1 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 index df7b37de8a..b6d859e559 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java @@ -15,17 +15,26 @@ */ 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; @@ -70,6 +79,25 @@ void emptyEmbeddedLockfileThrows() { 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", 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 73d50baa33..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 @@ -34,7 +34,17 @@ class EclipseJdtFormatterStepTest extends EquoResourceHarness { - private static final List EMBEDDED_LOCKFILE_VERSIONS = List.of("4.39", EclipseJdtFormatterStep.defaultVersion()); + /** + * 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()); 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 index ffeb2a50cc..f370291eca 100644 --- 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 @@ -15,39 +15,20 @@ */ package com.diffplug.spotless.extra.java; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.StringReader; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpClient.Redirect; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.StringJoiner; -import java.util.jar.JarInputStream; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathFactory; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; +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. @@ -60,13 +41,9 @@ */ public class EclipseJdtLockfileMetadataTool { - // Lockfiles are only provided for Eclipse versions whose org.eclipse.jdt.core POM on Maven Central - // declares exact transitive dependency versions rather than version ranges (e.g. "[3.12.0,4.0.0)"). - // org.eclipse.jdt.core:3.32.0 is the first version on Maven Central with exact version dependencies; - // the likely cause is eclipse-platform/eclipse.platform.releng#135 (issue #128), which updated the - // CBI aggregator configuration to map bundle requirements to their resolved versions rather than the - // OSGi version ranges from MANIFEST.MF. - private static final List TARGET_VERSIONS = List.of("4.39", "4.40"); + // 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. @@ -93,13 +70,10 @@ public static void main(String[] args) { } private static final String JDT_ID = "org.eclipse.jdt.core"; - private static final String JDT_MAVEN_PREFIX = "org.eclipse.jdt:org.eclipse.jdt.core:"; - private static final Pattern MAJOR_MINOR_PATCH = Pattern.compile("^([0-9]+\\.[0-9]+\\.[0-9]+).*$"); - private static final int MAX_TRAVERSAL = 200; - - private static final HttpClient HTTP = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .build(); + 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(); @@ -112,10 +86,9 @@ private static void run(boolean update) { String eclipseVersion = entry.getKey(); Path lockfilePath = entry.getValue(); try { - String bundleVersion = resolveBundleVersion(eclipseVersion); - String rootCoordinate = JDT_MAVEN_PREFIX + normalizeToMavenVersion(bundleVersion); + List allCoordinates = resolveTransitiveClosure(eclipseVersion); + String rootCoordinate = allCoordinates.get(0); if (update) { - List allCoordinates = resolveTransitiveClosure(rootCoordinate); Files.writeString(lockfilePath, lockfileContent(eclipseVersion, allCoordinates), StandardCharsets.UTF_8); System.out.println("WROTE v" + eclipseVersion + " -> " + allCoordinates.size() + " artifact(s), root: " + rootCoordinate); } else { @@ -124,13 +97,14 @@ private static void run(boolean update) { failures++; continue; } - String actualRootCoordinate = lockfileRootCoordinate(lockfilePath); - if (!rootCoordinate.equals(actualRootCoordinate)) { - System.err.println("MISMATCH v" + eclipseVersion + " -> expected root: " + rootCoordinate + " actual: " + actualRootCoordinate); + 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 + " -> " + actualRootCoordinate); + System.out.println("OK v" + eclipseVersion + " -> " + rootCoordinate + " (" + allCoordinates.size() + " coordinates)"); } } catch (Exception e) { System.err.println("ERROR v" + eclipseVersion + " -> " + e.getMessage()); @@ -159,16 +133,29 @@ private static String lockfileContent(String eclipseVersion, List coordi return joiner.toString(); } - private static String lockfileRootCoordinate(Path lockfilePath) throws IOException { + private static List lockfileCoordinates(Path lockfilePath) throws IOException { try (var lines = Files.lines(lockfilePath, StandardCharsets.UTF_8)) { - return lines.map(String::trim) + List coordinates = lines.map(String::trim) .filter(line -> !line.isEmpty()) .filter(line -> !line.startsWith("#")) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No dependency coordinate found in " + lockfilePath)); + .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)) { @@ -182,94 +169,61 @@ private static Path lockfileDir() { } /** - * Resolves the full Maven transitive closure of the given root coordinate using - * Maven Central POM traversal, with highest-version-wins conflict resolution. + * Resolves full, explicit Maven coordinates from the P2 query result for the given Eclipse version. *

- * Optional dependencies and test/provided/system-scoped dependencies are excluded. - * The root coordinate appears first in the result; remaining entries are sorted. + * 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 rootCoordinate) throws Exception { + private static List resolveTransitiveClosure(String eclipseVersion) throws Exception { + P2QueryResult query = queryJdtFromP2(eclipseVersion); + // "g:a" -> selected version Map selected = new LinkedHashMap<>(); - ArrayDeque queue = new ArrayDeque<>(); - queue.add(rootCoordinate.split(":")); - - while (!queue.isEmpty()) { - String[] parts = queue.removeFirst(); + 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) { - continue; - } - selected.put(key, version); - - List deps = fetchNonOptionalDepsFromPom(groupId, artifactId, version); - for (String[] dep : deps) { - String depKey = dep[0] + ":" + dep[1]; - String existingDepVersion = selected.get(depKey); - if (existingDepVersion == null || compareVersions(existingDepVersion, dep[2]) < 0) { - queue.add(dep); - } + if (existing == null || compareVersions(existing, version) < 0) { + selected.put(key, version); } } - - String[] rootParts = rootCoordinate.split(":"); - String rootKey = rootParts[0] + ":" + rootParts[1]; + 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(rootCoordinate); + result.add(JDT_MAVEN_KEY + ":" + rootVersion); selected.entrySet().stream() - .filter(e -> !e.getKey().equals(rootKey)) .map(e -> e.getKey() + ":" + e.getValue()) .sorted() .forEach(result::add); return result; } - private static List fetchNonOptionalDepsFromPom(String groupId, String artifactId, String version) throws Exception { - String path = groupId.replace('.', '/') + "/" + artifactId + "/" + version + "/" + artifactId + "-" + version + ".pom"; - Optional bytes = download("https://repo1.maven.org/maven2/" + path); - if (bytes.isEmpty()) { - System.err.println("WARNING: POM not found on Maven Central: " + groupId + ":" + artifactId + ":" + version); - return List.of(); + 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; + } } - return parseNonOptionalDepsFromPom(new String(bytes.get(), StandardCharsets.UTF_8)); + throw new IllegalStateException("Failed to query Eclipse " + eclipseVersion + " from known update sites", lastError); } - private static List parseNonOptionalDepsFromPom(String pomXml) throws Exception { - Document doc = parseXml(pomXml); - var xpath = XPathFactory.newInstance().newXPath(); - NodeList deps = (NodeList) xpath.evaluate("/project/dependencies/dependency", doc, XPathConstants.NODESET); - List result = new ArrayList<>(); - for (int i = 0; i < deps.getLength(); i++) { - Element dep = (Element) deps.item(i); - String g = textContent(dep, "groupId"); - String a = textContent(dep, "artifactId"); - String v = textContent(dep, "version"); - String scope = textContent(dep, "scope"); - String optional = textContent(dep, "optional"); - String type = textContent(dep, "type"); - - if ("true".equals(optional)) - continue; - if ("test".equals(scope) || "provided".equals(scope) || "system".equals(scope)) - continue; - if ("pom".equals(type)) - continue; - if (v.isEmpty() || v.startsWith("[") || v.startsWith("(") || v.startsWith("$")) - continue; - if (g.isEmpty() || a.isEmpty()) - continue; - - result.add(new String[]{g, a, v}); + private static void addPlatformRepo(P2Model model, String version, String baseUrl) { + if (!version.startsWith("4.")) { + throw new IllegalArgumentException("Expected 4.x"); } - return result; - } - - private static String textContent(Element parent, String tag) { - var nodes = parent.getElementsByTagName(tag); - return nodes.getLength() > 0 ? nodes.item(0).getTextContent().trim() : ""; + model.addP2Repo(baseUrl + version + "/"); } /** Compares two version strings numerically, segment by segment. Returns positive if v1 > v2. */ @@ -294,110 +248,4 @@ private static int parseVersionSegment(String segment) { } } - private static String resolveBundleVersion(String eclipseVersion) throws Exception { - ArrayDeque queue = new ArrayDeque<>(); - Set visited = new LinkedHashSet<>(); - queue.add("https://download.eclipse.org/eclipse/updates/" + eclipseVersion + "/"); - int traversed = 0; - while (!queue.isEmpty()) { - String repoUrl = queue.removeFirst(); - if (!visited.add(repoUrl)) { - continue; - } - traversed++; - if (traversed > MAX_TRAVERSAL) { - throw new IllegalStateException("Traversal exceeded " + MAX_TRAVERSAL + " repositories while resolving Eclipse " + eclipseVersion); - } - Optional contentXml = readJarEntry(repoUrl, "content.jar", "content.xml"); - if (contentXml.isPresent()) { - String bundleVersion = extractBundleVersion(contentXml.get()); - if (bundleVersion != null) { - return bundleVersion; - } - } - Optional compositeXml = readJarEntry(repoUrl, "compositeContent.jar", "compositeContent.xml"); - if (compositeXml.isPresent()) { - queue.addAll(compositeChildren(repoUrl, compositeXml.get(), visited)); - } - } - throw new IllegalStateException("Unable to resolve " + JDT_ID + " from Eclipse " + eclipseVersion + " update site"); - } - - private static Optional readJarEntry(String repoUrl, String jarName, String entryName) throws Exception { - Optional bytes = download(repoUrl + jarName); - if (bytes.isEmpty()) { - return Optional.empty(); - } - try (JarInputStream jarInputStream = new JarInputStream(new ByteArrayInputStream(bytes.get()))) { - var entry = jarInputStream.getNextJarEntry(); - while (entry != null) { - if (!entry.isDirectory() && entryName.equals(entry.getName())) { - return Optional.of(new String(jarInputStream.readAllBytes(), StandardCharsets.UTF_8)); - } - entry = jarInputStream.getNextJarEntry(); - } - } - throw new IllegalStateException("Entry " + entryName + " not found in " + repoUrl + jarName); - } - - private static Optional download(String url) throws Exception { - HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build(); - HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofByteArray()); - if (response.statusCode() == 200) { - return Optional.of(response.body()); - } - if (response.statusCode() == 404) { - return Optional.empty(); - } - throw new IOException("Unexpected HTTP status " + response.statusCode() + " from " + url); - } - - private static String extractBundleVersion(String contentXml) throws Exception { - Document document = parseXml(contentXml); - var xpath = XPathFactory.newInstance().newXPath(); - String value = (String) xpath.evaluate("//unit[@id='" + JDT_ID + "']/@version", document, XPathConstants.STRING); - return value.isBlank() ? null : value; - } - - private static List compositeChildren(String parentRepoUrl, String compositeXml, Set visited) throws Exception { - Document document = parseXml(compositeXml); - var xpath = XPathFactory.newInstance().newXPath(); - var nodes = (NodeList) xpath.evaluate("//child/@location", document, XPathConstants.NODESET); - List children = new ArrayList<>(nodes.getLength()); - for (int i = 0; i < nodes.getLength(); i++) { - String location = nodes.item(i).getNodeValue(); - String childUrl = parentRepoUrl + trimTrailingSlash(location) + "/"; - if (!visited.contains(childUrl)) { - children.add(childUrl); - } - } - return children; - } - - private static String trimTrailingSlash(String value) { - return value.endsWith("/") ? value.substring(0, value.length() - 1) : value; - } - - private static Document parseXml(String xml) throws Exception { - var factory = DocumentBuilderFactory.newInstance(); - // Disable DTDs/external entities and enable secure processing to prevent XXE - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setFeature("http://xml.org/sax/features/external-general-entities", false); - factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - factory.setExpandEntityReferences(false); - factory.setNamespaceAware(false); - var builder = factory.newDocumentBuilder(); - return builder.parse(new InputSource(new StringReader(xml))); - } - - private static String normalizeToMavenVersion(String bundleVersion) { - Matcher matcher = MAJOR_MINOR_PATCH.matcher(bundleVersion); - if (!matcher.matches()) { - throw new IllegalArgumentException("Unexpected org.eclipse.jdt.core bundle version: " + bundleVersion); - } - return matcher.group(1); - } - } 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