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/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 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