diff --git a/CHANGES.md b/CHANGES.md index 734ab51e9d..9f6f7b7178 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +- Add `P2Provisioner` interface in lib-extra to enable build-tool-specific caching strategies for Eclipse P2 dependencies, fixing OutOfMemoryError in large multi-project builds. ([#2788](https://github.com/diffplug/spotless/issues/2788)) ## [4.2.0] - 2026-01-22 diff --git a/gradle.properties b/gradle.properties index 734c61dae8..f6bd42e904 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,3 +34,4 @@ VER_JUNIT=6.0.2 VER_ASSERTJ=3.27.6 VER_MOCKITO=5.21.0 VER_SELFIE=2.5.5 +VER_SOLSTICE=1.8.1 diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle index 3687c39928..6776dcb9b8 100644 --- a/lib-extra/build.gradle +++ b/lib-extra/build.gradle @@ -7,7 +7,6 @@ version = rootProject.spotlessChangelog.versionNext apply from: rootProject.file('gradle/java-setup.gradle') apply from: rootProject.file('gradle/java-publish.gradle') -String VER_SOLSTICE = '1.8.1' dependencies { api projects.lib // misc useful utilities @@ -25,6 +24,8 @@ dependencies { // testing testImplementation projects.testlib + testImplementation "com.diffplug.durian:durian-io:${VER_DURIAN}" + testImplementation "com.google.code.findbugs:jsr305:${VER_JSR_305}" testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" 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 c4c41e41df..a7b4b11de1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -18,7 +18,6 @@ import static java.util.stream.Collectors.toMap; import java.io.File; -import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -38,12 +37,7 @@ import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.SerializedFunction; -import dev.equo.solstice.NestedJars; -import dev.equo.solstice.p2.CacheLocations; -import dev.equo.solstice.p2.P2ClientCache; import dev.equo.solstice.p2.P2Model; -import dev.equo.solstice.p2.P2QueryCache; -import dev.equo.solstice.p2.P2QueryResult; /** * Generic Eclipse based formatter step {@link State} builder. @@ -51,6 +45,7 @@ public abstract class EquoBasedStepBuilder { private final String formatterName; private final Provisioner mavenProvisioner; + private final P2Provisioner p2Provisioner; private final SerializedFunction stateToFormatter; private final ImmutableMap.Builder stepProperties; private String formatterVersion; @@ -64,12 +59,14 @@ public abstract class EquoBasedStepBuilder { protected EquoBasedStepBuilder( String formatterName, Provisioner mavenProvisioner, + P2Provisioner p2Provisioner, @Nullable String defaultVersion, SerializedFunction stateToFormatter, ImmutableMap.Builder stepProperties) { this.formatterName = formatterName; this.mavenProvisioner = mavenProvisioner; + this.p2Provisioner = p2Provisioner; this.formatterVersion = defaultVersion; this.stateToFormatter = stateToFormatter; this.stepProperties = stepProperties; @@ -125,25 +122,9 @@ 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(() -> { - P2QueryResult query; - try { - if (cacheDirectory != null) { - CacheLocations.override_p2data = cacheDirectory.toPath().resolve("dev/equo/p2-data").toFile(); - } - query = createModelWithMirrors().query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); - } catch (Exception x) { - throw new IOException("Failed to load " + formatterName + ": " + x, x); - } - var classpath = new ArrayList(); - var mavenDeps = new ArrayList(); - mavenDeps.add("dev.equo.ide:solstice:1.8.1"); - mavenDeps.add("com.diffplug.durian:durian-swt.os:4.3.1"); - mavenDeps.addAll(query.getJarsOnMavenCentral()); - classpath.addAll(mavenProvisioner.provisionWithTransitives(false, mavenDeps)); - classpath.addAll(query.getJarsNotOnMavenCentral()); - for (var nested : NestedJars.inFiles(query.getJarsNotOnMavenCentral()).extractAllNestedJars()) { - classpath.add(nested.getValue()); - } + P2Model model = createModelWithMirrors(); + P2ModelWrapper modelWrapper = P2ModelWrapper.wrap(model); + List classpath = p2Provisioner.provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory); return JarState.preserveOrder(classpath); }), stepProperties.build()); return FormatterStep.create(formatterName, roundtrippableState, EquoStep::state, stateToFormatter); diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/P2ModelWrapper.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2ModelWrapper.java new file mode 100644 index 0000000000..a8fa638856 --- /dev/null +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2ModelWrapper.java @@ -0,0 +1,61 @@ +/* + * 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 java.util.Collection; +import java.util.Set; + +import dev.equo.solstice.p2.P2Model; + +/** + * Wrapper for P2Model that exposes only the data needed for caching, + * without leaking the P2Model type to consumers. + */ +public final class P2ModelWrapper { + private final P2Model model; + + private P2ModelWrapper(P2Model model) { + this.model = model; + } + + public static P2ModelWrapper wrap(P2Model model) { + return new P2ModelWrapper(model); + } + + public P2Model unwrap() { + return model; + } + + public Collection getP2Repos() { + return model.getP2repo(); + } + + public Collection getInstallList() { + return model.getInstall(); + } + + public Set getFilterNames() { + return model.getFilters().keySet(); + } + + public Collection getPureMaven() { + return model.getPureMaven(); + } + + public boolean isUseMavenCentral() { + return model.useMavenCentral; + } +} diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java new file mode 100644 index 0000000000..1a3997dc3c --- /dev/null +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java @@ -0,0 +1,78 @@ +/* + * 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 java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.Provisioner; + +import dev.equo.solstice.NestedJars; +import dev.equo.solstice.p2.CacheLocations; +import dev.equo.solstice.p2.P2ClientCache; +import dev.equo.solstice.p2.P2Model; +import dev.equo.solstice.p2.P2QueryCache; +import dev.equo.solstice.p2.P2QueryResult; + +/** + * Provisions dependencies from Eclipse P2 repositories. + * Similar to {@link Provisioner} but for P2/OSGi bundles. + */ +@FunctionalInterface +public interface P2Provisioner { + /** + * Resolves P2 dependencies and returns the classpath. + * + * @param modelWrapper wrapper around P2Model describing repositories and plugins to install + * @param mavenProvisioner provisioner for Maven dependencies (some P2 bundles are on Maven Central) + * @param cacheDirectory optional cache directory override + * @return ordered list of JAR files forming the classpath + */ + List provisionP2Dependencies( + P2ModelWrapper modelWrapper, + Provisioner mavenProvisioner, + @Nullable File cacheDirectory) throws IOException; + + /** Creates a non-caching P2Provisioner for simple use cases. */ + static P2Provisioner createDefault() { + return (modelWrapper, mavenProvisioner, cacheDirectory) -> { + try { + if (cacheDirectory != null) { + CacheLocations.override_p2data = cacheDirectory.toPath().resolve("dev/equo/p2-data").toFile(); + } + P2Model model = modelWrapper.unwrap(); + P2QueryResult query = model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); + var classpath = new ArrayList(); + var mavenDeps = new ArrayList(); + mavenDeps.add("dev.equo.ide:solstice:1.8.1"); + mavenDeps.add("com.diffplug.durian:durian-swt.os:4.3.1"); + mavenDeps.addAll(query.getJarsOnMavenCentral()); + classpath.addAll(mavenProvisioner.provisionWithTransitives(false, mavenDeps)); + classpath.addAll(query.getJarsNotOnMavenCentral()); + for (var nested : NestedJars.inFiles(query.getJarsNotOnMavenCentral()).extractAllNestedJars()) { + classpath.add(nested.getValue()); + } + return classpath; + } catch (Exception e) { + throw new IOException("Failed to provision P2 dependencies", e); + } + }; + } +} diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java index bd7cdc2645..94b957f357 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -23,6 +23,7 @@ import com.diffplug.spotless.Jvm; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Provisioner; import dev.equo.solstice.p2.P2Model; @@ -45,8 +46,8 @@ public static String defaultVersion() { } /** Provides default configuration */ - public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) { - return new EquoBasedStepBuilder(NAME, provisioner, defaultVersion(), EclipseCdtFormatterStep::apply, ImmutableMap.builder()) { + public static EquoBasedStepBuilder createBuilder(Provisioner provisioner, P2Provisioner p2Provisioner) { + return new EquoBasedStepBuilder(NAME, provisioner, p2Provisioner, defaultVersion(), EclipseCdtFormatterStep::apply, ImmutableMap.builder()) { @Override protected P2Model model(String version) { var model = new P2Model(); diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java index 6a3151e8ab..49b6d44dd6 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -24,6 +24,7 @@ import com.diffplug.spotless.Jvm; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Provisioner; import dev.equo.solstice.p2.P2Model; @@ -39,8 +40,8 @@ public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); } - public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) { - return new EquoBasedStepBuilder(NAME, provisioner, defaultVersion(), GrEclipseFormatterStep::apply, ImmutableMap.builder()) { + public static EquoBasedStepBuilder createBuilder(Provisioner provisioner, P2Provisioner p2Provisioner) { + return new EquoBasedStepBuilder(NAME, provisioner, p2Provisioner, defaultVersion(), GrEclipseFormatterStep::apply, ImmutableMap.builder()) { @Override protected P2Model model(String version) { if (!version.startsWith("4.")) { 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 1e92371d3a..1e621d7886 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -25,6 +25,7 @@ import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.SerializedFunction; import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Provisioner; import dev.equo.solstice.p2.P2Model; @@ -40,8 +41,8 @@ public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); } - public static EclipseJdtFormatterStep.Builder createBuilder(Provisioner provisioner) { - return new EclipseJdtFormatterStep.Builder(NAME, provisioner, defaultVersion(), EclipseJdtFormatterStep::apply, ImmutableMap.builder()); + public static EclipseJdtFormatterStep.Builder createBuilder(Provisioner provisioner, P2Provisioner p2Provisioner) { + return new EclipseJdtFormatterStep.Builder(NAME, provisioner, p2Provisioner, defaultVersion(), EclipseJdtFormatterStep::apply, ImmutableMap.builder()); } private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception { @@ -59,10 +60,11 @@ public static class Builder extends EquoBasedStepBuilder { Builder( String formatterName, Provisioner mavenProvisioner, + P2Provisioner p2Provisioner, String defaultVersion, SerializedFunction stateToFormatter, ImmutableMap.Builder stepProperties) { - super(formatterName, mavenProvisioner, defaultVersion, stateToFormatter, stepProperties); + super(formatterName, mavenProvisioner, p2Provisioner, defaultVersion, stateToFormatter, stepProperties); this.stepProperties = stepProperties; } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/TestP2Provisioner.java b/lib-extra/src/test/java/com/diffplug/spotless/TestP2Provisioner.java new file mode 100644 index 0000000000..1447927d23 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/TestP2Provisioner.java @@ -0,0 +1,139 @@ +/* + * 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; + +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import com.diffplug.common.base.Errors; +import com.diffplug.common.base.StandardSystemProperty; +import com.diffplug.common.base.Suppliers; +import com.diffplug.common.collect.ImmutableList; +import com.diffplug.common.io.Files; +import com.diffplug.spotless.extra.P2Provisioner; + +public class TestP2Provisioner { + /** Creates a P2Provisioner which will cache the result of previous calls. */ + @SuppressWarnings("unchecked") + private static P2Provisioner caching(String name, Supplier input) { + File spotlessDir = new File(StandardSystemProperty.USER_DIR.value()).getParentFile(); + File testlib = new File(spotlessDir, "testlib"); + File cacheFile = new File(testlib, "build/tmp/testp2provisioner." + name + ".cache"); + + Map> cached; + if (cacheFile.exists()) { + try (ObjectInputStream inputStream = new ObjectInputStream(Files.asByteSource(cacheFile).openBufferedStream())) { + cached = (Map>) inputStream.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw Errors.asRuntime(e); + } + } else { + cached = new HashMap<>(); + try { + Files.createParentDirs(cacheFile); + } catch (IOException e) { + throw Errors.asRuntime(e); + } + } + return (modelWrapper, mavenProvisioner, cacheDirectory) -> { + CacheKey key = new CacheKey( + List.copyOf(modelWrapper.getP2Repos()), + List.copyOf(modelWrapper.getInstallList()), + Set.copyOf(modelWrapper.getFilterNames()), + List.copyOf(modelWrapper.getPureMaven()), + modelWrapper.isUseMavenCentral(), + cacheDirectory); + + synchronized (TestP2Provisioner.class) { + ImmutableList result = cached.get(key); + // double-check that depcache pruning hasn't removed them since our cache cached them + boolean needsToBeSet = result == null || !result.stream().allMatch(file -> file.exists() && file.isFile() && file.length() > 0); + if (needsToBeSet) { + result = ImmutableList.copyOf(input.get().provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory)); + cached.put(key, result); + try (ObjectOutputStream outputStream = new ObjectOutputStream(Files.asByteSink(cacheFile).openBufferedStream())) { + outputStream.writeObject(cached); + } catch (IOException e) { + throw Errors.asRuntime(e); + } + } + return result; + } + }; + } + + /** Creates a default P2Provisioner with caching for tests. */ + public static P2Provisioner defaultProvisioner() { + return DEFAULT_PROVISIONER.get(); + } + + private static final Supplier DEFAULT_PROVISIONER = Suppliers.memoize(() -> caching("default", P2Provisioner::createDefault)); + + /** + * Cache key capturing all P2Model state that affects query results. + * Must be Serializable for disk caching. + */ + private static class CacheKey implements Serializable { + private static final long serialVersionUID = 1L; + private final List p2Repos; + private final List installList; + private final Set filterNames; + private final List pureMaven; + private final boolean useMavenCentral; + @Nullable private final File cacheDirectory; + + CacheKey(List p2Repos, List installList, Set filterNames, + List pureMaven, boolean useMavenCentral, @Nullable File cacheDirectory) { + this.p2Repos = p2Repos; + this.installList = installList; + this.filterNames = filterNames; + this.pureMaven = pureMaven; + this.useMavenCentral = useMavenCentral; + this.cacheDirectory = cacheDirectory; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + CacheKey cacheKey = (CacheKey) o; + return useMavenCentral == cacheKey.useMavenCentral && + Objects.equals(p2Repos, cacheKey.p2Repos) && + Objects.equals(installList, cacheKey.installList) && + Objects.equals(filterNames, cacheKey.filterNames) && + Objects.equals(pureMaven, cacheKey.pureMaven) && + Objects.equals(cacheDirectory, cacheKey.cacheDirectory); + } + + @Override + public int hashCode() { + return Objects.hash(p2Repos, installList, filterNames, pureMaven, useMavenCentral, cacheDirectory); + } + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java index 811f811ee0..172073b623 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -20,12 +20,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; class EclipseCdtFormatterStepTest extends EquoResourceHarness { public EclipseCdtFormatterStepTest() { - super(EclipseCdtFormatterStep.createBuilder(TestProvisioner.mavenCentral())); + super(EclipseCdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner())); } @ParameterizedTest diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java index 25eeb9808c..45a8dae6bf 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; public class GrEclipseFormatterStepSpecialCaseTest { @@ -30,13 +31,13 @@ public class GrEclipseFormatterStepSpecialCaseTest { */ @Test public void issue_1657() { - Assertions.assertThrows(RuntimeException.class, () -> StepHarness.forStep(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral()).build()) + Assertions.assertThrows(RuntimeException.class, () -> StepHarness.forStep(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()).build()) .testResourceUnaffected("groovy/greclipse/format/SomeClass.test")); } @Test public void issue_1657_fixed() { - StepHarness.forStep(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral()).build()) + StepHarness.forStep(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()).build()) .testResourceUnaffected("groovy/greclipse/format/SomeClass.fixed"); } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java index 6b00c0e794..b2185602ea 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -20,6 +20,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; @@ -28,7 +29,7 @@ public class GrEclipseFormatterStepTest extends EquoResourceHarness { private static final String EXPECTED = "class F{\n\tdef m(){}\n}"; public GrEclipseFormatterStepTest() { - super(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral())); + super(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner())); } @ParameterizedTest diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java index 18349b046e..39085ca2f8 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 DiffPlug + * Copyright 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.ThrowingEx; @@ -48,7 +49,7 @@ public void test() throws Exception { } private static FormatterStep withSettingsFile(File settingsFile) { - var builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + var builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.setPreferences(List.of(settingsFile)); return builder.build(); } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java index 027e562291..ade103df17 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * 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. @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.EquoBasedStepBuilder; @@ -30,7 +31,7 @@ class EclipseJdtFormatterStepSpecialCaseTest { void issue_1638() { ClassLoader classLoader = getClass().getClassLoader(); File file = new File(classLoader.getResource("eclipse_formatter_issue_1638.xml").getFile()); - EquoBasedStepBuilder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EquoBasedStepBuilder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.setPreferences(List.of(file)); StepHarness.forStep(builder.build()) .testResource("java/eclipse/AbstractType.test", "java/eclipse/AbstractType.clean"); @@ -38,7 +39,7 @@ void issue_1638() { @Test void sort_members_global_by_visibility() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.sortMembersEnabled(true); builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); builder.sortMembersDoNotSortFields(false); @@ -50,7 +51,7 @@ void sort_members_global_by_visibility() { @Test void sort_members_global_enabled() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.sortMembersEnabled(true); builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); builder.sortMembersDoNotSortFields(false); @@ -60,7 +61,7 @@ void sort_members_global_enabled() { @Test void sort_members_global_no_fields() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.sortMembersEnabled(true); builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); builder.sortMembersDoNotSortFields(true); @@ -70,7 +71,7 @@ void sort_members_global_no_fields() { @Test void sort_members_local_by_visibility() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.sortMembersEnabled(true); builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); builder.sortMembersDoNotSortFields(false); @@ -82,7 +83,7 @@ void sort_members_local_by_visibility() { @Test void sort_members_local_enabled_false() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.sortMembersEnabled(true); builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); builder.sortMembersDoNotSortFields(false); @@ -92,7 +93,7 @@ void sort_members_local_enabled_false() { @Test void sort_members_local_no_fields() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); builder.sortMembersEnabled(true); builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); builder.sortMembersDoNotSortFields(false); @@ -102,7 +103,7 @@ void sort_members_local_no_fields() { @Test void sort_members_local_enabled_true() { - EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); StepHarness.forStep(builder.build()) .testResource("java/eclipse/SortExample.localEnabledTrue.test", "java/eclipse/SortExample.localEnabledTrue.clean"); } 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 dbbfdb04a0..ff8eeaeae7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * 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. @@ -22,13 +22,14 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.EquoBasedStepBuilder; import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; class EclipseJdtFormatterStepTest extends EquoResourceHarness { private static EquoBasedStepBuilder createBuilder() { - return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); } public EclipseJdtFormatterStepTest() { diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index e89e0b0c45..98f2bff020 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Fixed +- Fix OutOfMemoryError and slow configuration phase in large multi-project builds when using Eclipse-based formatters (Eclipse JDT, GrEclipse, Eclipse CDT) by implementing P2 dependency caching. ([#2788](https://github.com/diffplug/spotless/issues/2788)) ## [8.2.0] - 2026-01-22 ### Added diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index 7ea455b0b3..633f4e52b6 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -26,6 +26,7 @@ dependencies { testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" testImplementation 'org.owasp.encoder:encoder:1.4.0' + testImplementation "dev.equo.ide:solstice:${VER_SOLSTICE}" testRuntimeOnly "org.junit.platform:junit-platform-launcher" } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java index 230445740b..d38649b73e 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ public static final class GrEclipseConfig { private GrEclipseConfig(String version, FormatExtension extension) { this.extension = extension; - builder = GrEclipseFormatterStep.createBuilder(extension.provisioner()); + builder = GrEclipseFormatterStep.createBuilder(extension.provisioner(), extension.p2Provisioner()); builder.setVersion(version); extension.addStep(builder.build()); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java index 4dcb033fc5..fbbf65fd9d 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -49,7 +49,7 @@ public class EclipseConfig { private final EquoBasedStepBuilder builder; EclipseConfig(String version) { - builder = EclipseCdtFormatterStep.createBuilder(provisioner()); + builder = EclipseCdtFormatterStep.createBuilder(provisioner(), p2Provisioner()); builder.setVersion(version); addStep(builder.build()); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index a368335841..94d965a21f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -97,6 +97,10 @@ protected final Provisioner provisioner() { return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless); } + protected final com.diffplug.spotless.extra.P2Provisioner p2Provisioner() { + return spotless.getRegisterDependenciesTask().getTaskService().get().p2ProvisionerFor(spotless); + } + private String formatName() { for (Map.Entry entry : spotless.formats.entrySet()) { if (entry.getValue() == this) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java index b8260e3b9c..69d52ebd4b 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -16,11 +16,15 @@ package com.diffplug.gradle.spotless; import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; + import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -36,6 +40,8 @@ import com.diffplug.common.base.Unhandled; import com.diffplug.common.collect.ImmutableList; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.P2ModelWrapper; +import com.diffplug.spotless.extra.P2Provisioner; /** Should be package-private. */ final class GradleProvisioner { @@ -45,15 +51,18 @@ enum Policy { INDEPENDENT, ROOT_PROJECT, ROOT_BUILDSCRIPT; public DedupingProvisioner dedupingProvisioner(Project project) { - switch (this) { - case ROOT_PROJECT: - return new DedupingProvisioner(forProject(project)); - case ROOT_BUILDSCRIPT: - return new DedupingProvisioner(forRootProjectBuildscript(project)); - case INDEPENDENT: - default: - throw Unhandled.enumException(this); - } + return switch (this) { + case ROOT_PROJECT -> new DedupingProvisioner(forProject(project)); + case ROOT_BUILDSCRIPT -> new DedupingProvisioner(forRootProjectBuildscript(project)); + default -> throw Unhandled.enumException(this); + }; + } + + public DedupingP2Provisioner dedupingP2Provisioner(Project project) { + return switch (this) { + case ROOT_PROJECT, ROOT_BUILDSCRIPT -> new DedupingP2Provisioner(P2Provisioner.createDefault()); + default -> throw Unhandled.enumException(this); + }; } } @@ -183,4 +192,73 @@ public String toString() { return builder.toString(); } } + + static class DedupingP2Provisioner implements P2Provisioner { + private final Map> cache = new HashMap<>(); + private final P2Provisioner p2Provisioner; + + public DedupingP2Provisioner(P2Provisioner p2Provisioner) { + this.p2Provisioner = p2Provisioner; + } + + @Override + public synchronized List provisionP2Dependencies( + P2ModelWrapper modelWrapper, + Provisioner mavenProvisioner, + @Nullable File cacheDirectory) throws IOException { + + P2Request req = new P2Request( + List.copyOf(modelWrapper.getP2Repos()), + List.copyOf(modelWrapper.getInstallList()), + Set.copyOf(modelWrapper.getFilterNames()), + List.copyOf(modelWrapper.getPureMaven()), + modelWrapper.isUseMavenCentral(), + cacheDirectory); + + List result = cache.get(req); + if (result != null) { + return result; + } + + result = p2Provisioner.provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory); + cache.put(req, List.copyOf(result)); + return result; + } + + /** A child P2Provisioner which retrieves cached elements only. */ + final P2Provisioner cachedOnly = (modelWrapper, mavenProvisioner, cacheDirectory) -> { + P2Request req = new P2Request( + List.copyOf(modelWrapper.getP2Repos()), + List.copyOf(modelWrapper.getInstallList()), + Set.copyOf(modelWrapper.getFilterNames()), + List.copyOf(modelWrapper.getPureMaven()), + modelWrapper.isUseMavenCentral(), + cacheDirectory); + List result; + synchronized (cache) { + result = cache.get(req); + } + if (result != null) { + return result; + } + throw new GradleException("P2 dependencies not predeclared. Add Eclipse formatter configuration to the `spotlessPredeclare` block in the root project."); + }; + + /** + * Cache key capturing all P2Model state that affects query results. + * Based on P2Model fields from equo-ide: + * - p2repo (TreeSet): P2 repository URLs + * - install (TreeSet): Installation targets + * - filters (TreeMap): Named filter configurations + * - pureMaven (TreeSet): Pure Maven dependencies + * - useMavenCentral (boolean): Controls whether Maven Central is used + */ + private record P2Request( + List p2Repos, + List installList, + java.util.Set filterNames, // Filter names (Filter objects aren't easily comparable) + List pureMaven, + boolean useMavenCentral, + @Nullable File cacheDirectory) {} + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java index 489ee5dbb1..7ae0fe90ee 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java @@ -319,7 +319,7 @@ public class EclipseConfig { private final EclipseJdtFormatterStep.Builder builder; EclipseConfig(String version) { - builder = EclipseJdtFormatterStep.createBuilder(provisioner()); + builder = EclipseJdtFormatterStep.createBuilder(provisioner(), p2Provisioner()); builder.setVersion(version); addStep(builder.build()); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java index 8c5a36cdc9..52a40a22b4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ public class SpotlessExtensionPredeclare extends SpotlessExtension { public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy policy) { super(project); getRegisterDependenciesTask().getTaskService().get().predeclaredProvisioner = policy.dedupingProvisioner(project); + getRegisterDependenciesTask().getTaskService().get().predeclaredP2Provisioner = policy.dedupingP2Provisioner(project); project.afterEvaluate(unused -> toSetup.forEach((name, formatExtension) -> { for (Action lazyAction : formatExtension.lazyActions) { lazyAction.execute(formatExtension); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java index 73510af32a..3632f74050 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ import com.diffplug.common.base.Unhandled; import com.diffplug.spotless.Lint; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.P2Provisioner; /** * Allows the check and apply tasks to coordinate @@ -57,8 +58,10 @@ public abstract class SpotlessTaskService implements BuildService apply = Collections.synchronizedMap(new HashMap<>()); private final Map source = Collections.synchronizedMap(new HashMap<>()); private final Map provisioner = Collections.synchronizedMap(new HashMap<>()); + private final Map p2Provisioner = Collections.synchronizedMap(new HashMap<>()); @Nullable GradleProvisioner.DedupingProvisioner predeclaredProvisioner; + @Nullable GradleProvisioner.DedupingP2Provisioner predeclaredP2Provisioner; Provisioner provisionerFor(SpotlessExtension spotless) { if (spotless instanceof SpotlessExtensionPredeclare) { @@ -72,6 +75,19 @@ Provisioner provisionerFor(SpotlessExtension spotless) { } } + P2Provisioner p2ProvisionerFor(SpotlessExtension spotless) { + if (spotless instanceof SpotlessExtensionPredeclare) { + return predeclaredP2Provisioner; + } else { + if (predeclaredP2Provisioner != null) { + return predeclaredP2Provisioner.cachedOnly; + } else { + return p2Provisioner.computeIfAbsent(spotless.project.getPath(), + unused -> new GradleProvisioner.DedupingP2Provisioner(P2Provisioner.createDefault())); + } + } + } + void registerSourceAlreadyRan(SpotlessTask task) { source.put(task.getPath(), task); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleProvisionerTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleProvisionerTest.java new file mode 100644 index 0000000000..b72161c7f0 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleProvisionerTest.java @@ -0,0 +1,313 @@ +/* + * 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.gradle.spotless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import org.gradle.api.GradleException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.P2ModelWrapper; +import com.diffplug.spotless.extra.P2Provisioner; + +import dev.equo.solstice.p2.P2Model; + +class GradleProvisionerTest { + + @Nested + class DedupingProvisionerTest { + @Test + void cacheHitReturnsCachedResult() { + AtomicInteger callCount = new AtomicInteger(0); + Provisioner underlying = mockProvisioner(callCount); + GradleProvisioner.DedupingProvisioner deduping = new GradleProvisioner.DedupingProvisioner(underlying); + + // First call + Set result1 = deduping.provisionWithTransitives(true, List.of("com.google:guava:32.0.0-jre")); + // Second call with same parameters + Set result2 = deduping.provisionWithTransitives(true, List.of("com.google:guava:32.0.0-jre")); + + assertThat(result1).isSameAs(result2); + assertThat(callCount.get()).as("Only called once").isEqualTo(1); + } + + @ParameterizedTest + @MethodSource("cacheMissScenarios") + void cacheMissTriggersNewResolution(String scenario, boolean withTransitives1, List coords1, boolean withTransitives2, List coords2) { + AtomicInteger callCount = new AtomicInteger(0); + Provisioner underlying = mockProvisioner(callCount); + GradleProvisioner.DedupingProvisioner deduping = new GradleProvisioner.DedupingProvisioner(underlying); + + Set result1 = deduping.provisionWithTransitives(withTransitives1, coords1); + Set result2 = deduping.provisionWithTransitives(withTransitives2, coords2); + + assertThat(result1).isNotSameAs(result2); + assertThat(callCount.get()).as("Called twice").isEqualTo(2); + } + + static Stream cacheMissScenarios() { + return Stream.of( + Arguments.of("different coordinates", + true, List.of("com.google:guava:32.0.0-jre"), + true, List.of("org.slf4j:slf4j-api:2.0.0")), + Arguments.of("different transitivity", + true, List.of("com.google:guava:32.0.0-jre"), + false, List.of("com.google:guava:32.0.0-jre")), + Arguments.of("different order", + true, List.of("com.google:guava:32.0.0-jre", "org.slf4j:slf4j-api:2.0.0"), + true, List.of("org.slf4j:slf4j-api:2.0.0", "com.google:guava:32.0.0-jre"))); + } + + @Test + void cachedOnlyCacheHitReturnsResult() { + Provisioner underlying = mockProvisioner(new AtomicInteger(0)); + GradleProvisioner.DedupingProvisioner deduping = new GradleProvisioner.DedupingProvisioner(underlying); + + // Populate cache + deduping.provisionWithTransitives(true, List.of("com.google:guava:32.0.0-jre")); + + // cachedOnly should return cached result + Set result = deduping.cachedOnly.provisionWithTransitives(true, List.of("com.google:guava:32.0.0-jre")); + + assertThat(result).isNotEmpty(); + } + + @Test + void cachedOnlyCacheMissThrowsException() { + Provisioner underlying = mockProvisioner(new AtomicInteger(0)); + GradleProvisioner.DedupingProvisioner deduping = new GradleProvisioner.DedupingProvisioner(underlying); + + // cachedOnly should throw when not cached + assertThatThrownBy(() -> deduping.cachedOnly.provisionWithTransitives(true, List.of("com.google:guava:32.0.0-jre"))) + .isInstanceOf(GradleException.class) + .hasMessageContaining("spotlessPredeclare"); + } + + private Provisioner mockProvisioner(AtomicInteger callCount) { + return (withTransitives, mavenCoordinates) -> { + callCount.incrementAndGet(); + // Return a unique set based on coordinates + return Set.of(new File("/mock/" + String.join("-", mavenCoordinates) + ".jar")); + }; + } + } + + @Nested + class DedupingP2ProvisionerTest { + @Test + void cacheHitReturnsCachedResult() throws IOException { + AtomicInteger callCount = new AtomicInteger(0); + P2Provisioner underlying = mockP2Provisioner(callCount); + GradleProvisioner.DedupingP2Provisioner deduping = new GradleProvisioner.DedupingP2Provisioner(underlying); + + P2ModelWrapper model = createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null); + + // First call + List result1 = deduping.provisionP2Dependencies(model, mockProvisioner(), null); + // Second call with same parameters + List result2 = deduping.provisionP2Dependencies(model, mockProvisioner(), null); + + assertThat(result1).isSameAs(result2); + assertThat(callCount.get()).as("Only called once").isEqualTo(1); + } + + @ParameterizedTest + @MethodSource("cacheMissScenarios") + void cacheMissTriggersNewResolution(String scenario, Function modelModifier, File cacheDir2) throws IOException { + AtomicInteger callCount = new AtomicInteger(0); + P2Provisioner underlying = mockP2Provisioner(callCount); + GradleProvisioner.DedupingP2Provisioner deduping = new GradleProvisioner.DedupingP2Provisioner(underlying); + + P2ModelWrapper model1 = createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null); + + P2ModelWrapper model2 = modelModifier.apply(model1); + + List result1 = deduping.provisionP2Dependencies(model1, mockProvisioner(), null); + List result2 = deduping.provisionP2Dependencies(model2, mockProvisioner(), cacheDir2); + + assertThat(result1).isNotSameAs(result2); + assertThat(callCount.get()).as("Called twice").isEqualTo(2); + } + + static Stream cacheMissScenarios() { + return Stream.of( + Arguments.of("different P2 repo", (Function) (m) -> createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.27/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null), null), + Arguments.of("different install list", (Function) (m) -> createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core", "org.eclipse.jdt.ui"), + Set.of(), + List.of(), + true, + null), null), + Arguments.of("different filters", (Function) (m) -> createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of("osgiFilter1"), + List.of(), + true, + null), null), + Arguments.of("different pure maven", (Function) (m) -> createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of("com.google:guava:32.0.0-jre"), + true, + null), null), + Arguments.of("different useMavenCentral", (Function) (m) -> createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + false, + null), null), + Arguments.of("different cache directory", Function. identity(), new File("/tmp/cache"))); + } + + @Test + void identicalModelsDifferentInstancesUsesCache() throws IOException { + AtomicInteger callCount = new AtomicInteger(0); + P2Provisioner underlying = mockP2Provisioner(callCount); + GradleProvisioner.DedupingP2Provisioner deduping = new GradleProvisioner.DedupingP2Provisioner(underlying); + + // Create two different instances with identical data + P2ModelWrapper model1 = createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null); + + P2ModelWrapper model2 = createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null); + + List result1 = deduping.provisionP2Dependencies(model1, mockProvisioner(), null); + List result2 = deduping.provisionP2Dependencies(model2, mockProvisioner(), null); + + assertThat(result1).as("Cache hit").isSameAs(result2); + assertThat(callCount.get()).as("Only called once").isEqualTo(1); + } + + @Test + void cachedOnlyCacheHitReturnsResult() throws IOException { + P2Provisioner underlying = mockP2Provisioner(new AtomicInteger(0)); + GradleProvisioner.DedupingP2Provisioner deduping = new GradleProvisioner.DedupingP2Provisioner(underlying); + + P2ModelWrapper model = createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null); + + // Populate cache + deduping.provisionP2Dependencies(model, mockProvisioner(), null); + + // cachedOnly should return cached result + List result = deduping.cachedOnly.provisionP2Dependencies(model, mockProvisioner(), null); + + assertThat(result).isNotEmpty(); + } + + @Test + void cachedOnlyCacheMissThrowsException() { + P2Provisioner underlying = mockP2Provisioner(new AtomicInteger(0)); + GradleProvisioner.DedupingP2Provisioner deduping = new GradleProvisioner.DedupingP2Provisioner(underlying); + + P2ModelWrapper model = createMockModel( + List.of("https://download.eclipse.org/eclipse/updates/4.26/"), + List.of("org.eclipse.jdt.core"), + Set.of(), + List.of(), + true, + null); + + // cachedOnly should throw when not cached + assertThatThrownBy(() -> deduping.cachedOnly.provisionP2Dependencies(model, mockProvisioner(), null)) + .isInstanceOf(GradleException.class) + .hasMessageContaining("spotlessPredeclare"); + } + + private P2Provisioner mockP2Provisioner(AtomicInteger callCount) { + return (modelWrapper, mavenProvisioner, cacheDirectory) -> { + callCount.incrementAndGet(); + // Return a unique list based on model + String id = String.join("-", modelWrapper.getP2Repos()) + "-" + String.join("-", modelWrapper.getInstallList()); + return List.of(new File("/mock/p2-" + id.hashCode() + ".jar")); + }; + } + + private Provisioner mockProvisioner() { + return (withTransitives, mavenCoordinates) -> Set.of(new File("/mock/maven.jar")); + } + + private static P2ModelWrapper createMockModel( + List p2Repos, + List installList, + Set filterNames, + List pureMaven, + boolean useMavenCentral, + @Nullable File cacheDirectory) { + P2Model model = new P2Model(); + p2Repos.forEach(model.getP2repo()::add); + installList.forEach(model.getInstall()::add); + filterNames.forEach(name -> model.getFilters().put(name, null)); // Filter value doesn't matter for cache key + pureMaven.forEach(model.getPureMaven()::add); + model.useMavenCentral = useMavenCentral; + return P2ModelWrapper.wrap(model); + } + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java new file mode 100644 index 0000000000..c1bfcf906e --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java @@ -0,0 +1,564 @@ +/* + * Copyright 2025-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.gradle.spotless; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for the spotlessPredeclare feature, which allows dependencies + * to be predeclared in the root project and reused across all subprojects to avoid + * OutOfMemoryErrors when multiple projects resolve the same dependencies concurrently. + */ +class SpotlessPredeclareIntegrationTest extends GradleIntegrationHarness { + + @Nested + class MavenDependencies { + @Test + void predeclareSucceedsWithMavenDependencies() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { googleJavaFormat('1.17.0') } + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + googleJavaFormat('1.17.0') + } + } + """); + setFile("sub/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("sub/test.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test"); + } + + @Test + void predeclareFailsWhenDependencyNotPredeclared() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + // Empty - no dependencies predeclared + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + googleJavaFormat('1.17.0') + } + } + """); + setFile("sub/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(result.getOutput()) + .contains("Add a step with [com.google.googlejavaformat:google-java-format:1.17.0]") + .contains("into the `spotlessPredeclare` block in the root project"); + } + + @Test + void predeclareWorksWithMultipleVersions() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { googleJavaFormat('1.17.0') } + } + """); + setFile("settings.gradle").toContent("include 'sub1', 'sub2'"); + setFile("sub1/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + googleJavaFormat('1.17.0') + } + } + """); + setFile("sub1/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + setFile("sub2/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + googleJavaFormat('1.17.0') + } + } + """); + setFile("sub2/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("sub1/test.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test"); + assertFile("sub2/test.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test"); + } + } + + @Nested + class P2Dependencies { + @Test + void predeclareSucceedsWithEclipseFormatter() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { eclipse() } + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + eclipse() + } + } + """); + setFile("sub/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + // Verify the file was formatted (has proper indentation now) + String formatted = read("sub/test.java"); + assertThat(formatted).contains("main(String[] args)"); + assertThat(formatted).as("Should be indented").doesNotStartWith("public static void main"); + } + + @Test + void predeclareFailsWhenEclipseFormatterNotPredeclared() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + // Empty - no P2 dependencies predeclared + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + eclipse() + } + } + """); + setFile("sub/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(result.getOutput()) + .contains("P2 dependencies not predeclared") + .contains("Add Eclipse formatter configuration to the `spotlessPredeclare` block in the root project"); + } + + @Test + void predeclareWorksWithEclipseConfigFile() throws IOException { + setFile("eclipse-formatter.xml").toResource("java/eclipse/formatter.xml"); + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { eclipse().configFile('eclipse-formatter.xml') } + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + eclipse().configFile(rootProject.file('eclipse-formatter.xml')) + } + } + """); + setFile("sub/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + } + + @Test + void predeclareWorksWithMultipleEclipseProjects() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { eclipse() } + } + """); + setFile("settings.gradle").toContent("include 'sub1', 'sub2', 'sub3'"); + for (int i = 1; i <= 3; i++) { + setFile("sub" + i + "/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + eclipse() + } + } + """); + setFile("sub" + i + "/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); + } + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + // Verify all files were formatted + for (int i = 1; i <= 3; i++) { + String formatted = read("sub" + i + "/test.java"); + assertThat(formatted).contains("main(String[] args)"); + assertThat(formatted).as("Should be indented").doesNotStartWith("public static void main"); + } + } + } + + @Nested + class GroovyDependencies { + @Test + void predeclareSucceedsWithGroovyGrEclipseFormatter() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + groovy { greclipse() } + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + id 'groovy' + } + repositories { mavenCentral() } + spotless { + groovy { + target file('test.groovy') + greclipse() + } + } + """); + setFile("sub/test.groovy").toResource("groovy/greclipse/format/unformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + // Verify the file was formatted + String formatted = read("sub/test.groovy"); + assertThat(formatted).contains("class Foo"); + assertThat(formatted).contains("def callBar()"); + } + + @Test + void predeclareFailsWhenGroovyGrEclipseNotPredeclared() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + // Empty - no Groovy P2 dependencies predeclared + } + """); + setFile("settings.gradle").toContent("include 'sub'"); + setFile("sub/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + id 'groovy' + } + repositories { mavenCentral() } + spotless { + groovy { + target file('test.groovy') + greclipse() + } + } + """); + setFile("sub/test.groovy").toResource("groovy/greclipse/format/unformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(result.getOutput()) + .contains("P2 dependencies not predeclared") + .contains("Add Eclipse formatter configuration to the `spotlessPredeclare` block in the root project"); + } + + @Test + void predeclareWorksWithMultipleGroovyProjects() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + groovy { greclipse() } + } + """); + setFile("settings.gradle").toContent("include 'sub1', 'sub2'"); + for (int i = 1; i <= 2; i++) { + setFile("sub" + i + "/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + id 'groovy' + } + repositories { mavenCentral() } + spotless { + groovy { + target file('test.groovy') + greclipse() + } + } + """); + setFile("sub" + i + "/test.groovy").toResource("groovy/greclipse/format/unformatted.test"); + } + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + // Verify all files were formatted + for (int i = 1; i <= 2; i++) { + String formatted = read("sub" + i + "/test.groovy"); + assertThat(formatted).contains("class Foo"); + assertThat(formatted).contains("def callBar()"); + } + } + } + + @Nested + class MixedDependencies { + @Test + void predeclareWorksWithJavaAndGroovyFormatters() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { eclipse() } + groovy { greclipse() } + } + """); + setFile("settings.gradle").toContent("include 'sub1', 'sub2'"); + setFile("sub1/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + eclipse() + } + } + """); + setFile("sub1/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); + setFile("sub2/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + id 'groovy' + } + repositories { mavenCentral() } + spotless { + groovy { + target file('test.groovy') + greclipse() + } + } + """); + setFile("sub2/test.groovy").toResource("groovy/greclipse/format/unformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + // Verify Java file was formatted + String javaFormatted = read("sub1/test.java"); + assertThat(javaFormatted).contains("main(String[] args)"); + assertThat(javaFormatted).doesNotStartWith("public static void main"); + // Verify Groovy file was formatted + String groovyFormatted = read("sub2/test.groovy"); + assertThat(groovyFormatted).contains("class Foo"); + assertThat(groovyFormatted).contains("def callBar()"); + } + + @Test + void predeclareWorksWithBothMavenAndP2Dependencies() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + java { + googleJavaFormat('1.17.0') + eclipse() + } + } + """); + setFile("settings.gradle").toContent("include 'sub1', 'sub2'"); + setFile("sub1/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + googleJavaFormat('1.17.0') + } + } + """); + setFile("sub1/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + setFile("sub2/build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { + java { + target file('test.java') + eclipse() + } + } + """); + setFile("sub2/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); + + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("sub1/test.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test"); + // Verify Eclipse formatted file has proper indentation + String eclipseFormatted = read("sub2/test.java"); + assertThat(eclipseFormatted).contains("main(String[] args)"); + assertThat(eclipseFormatted).as("Should be indented").doesNotStartWith("public static void main"); + } + } + + @Nested + class EdgeCases { + @Test + void predeclareRequiresPredeclareDepsCall() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotlessPredeclare { + java { googleJavaFormat('1.17.0') } + } + """); + + BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(result.getOutput()) + .contains("Could not find method spotlessPredeclare() for arguments"); + } + + @Test + void predeclareBlockMustComeAfterPredeclareDeps() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotlessPredeclare { + java { googleJavaFormat('1.17.0') } + } + spotless { predeclareDeps() } + """); + + BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(result.getOutput()) + .contains("Could not find method spotlessPredeclare() for arguments"); + } + + @Test + void emptyPredeclareBlockIsValid() throws IOException { + setFile("build.gradle").toContent(""" + plugins { + id 'com.diffplug.spotless' + } + repositories { mavenCentral() } + spotless { predeclareDeps() } + spotlessPredeclare { + } + """); + + BuildResult result = gradleRunner().withArguments("help").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + } + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index 3952a8de70..84b980ab5e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -59,6 +59,7 @@ import com.diffplug.spotless.LintState; import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.P2Provisioner; import com.diffplug.spotless.maven.antlr4.Antlr4; import com.diffplug.spotless.maven.cpp.Cpp; import com.diffplug.spotless.maven.css.Css; @@ -393,11 +394,12 @@ private Set getExcludes(FormatterFactory formatterFactory) { private FormatterConfig getFormatterConfig() { ArtifactResolver resolver = new ArtifactResolver(repositorySystem, repositorySystemSession, repositories, getLog()); Provisioner provisioner = MavenProvisioner.create(resolver); + P2Provisioner p2Provisioner = P2Provisioner.createDefault(); List formatterStepFactories = getFormatterStepFactories(); FileLocator fileLocator = getFileLocator(); final Optional optionalRatchetFrom = Optional.ofNullable(this.ratchetFrom) .filter(ratchet -> !RATCHETFROM_NONE.equals(ratchet)); - return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory), lintSuppressions); + return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, p2Provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory), lintSuppressions); } private FileLocator getFileLocator() { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java index 842d4da68e..66fa572a25 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -24,6 +24,7 @@ import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.P2Provisioner; public class FormatterConfig { @@ -31,17 +32,19 @@ public class FormatterConfig { private final LineEnding lineEndings; private final Optional ratchetFrom; private final Provisioner provisioner; + private final P2Provisioner p2Provisioner; private final FileLocator fileLocator; private final List globalStepFactories; private final Optional spotlessSetLicenseHeaderYearsFromGitHistory; private final List lintSuppressions; public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Optional ratchetFrom, Provisioner provisioner, - FileLocator fileLocator, List globalStepFactories, Optional spotlessSetLicenseHeaderYearsFromGitHistory, List lintSuppressions) { + P2Provisioner p2Provisioner, FileLocator fileLocator, List globalStepFactories, Optional spotlessSetLicenseHeaderYearsFromGitHistory, List lintSuppressions) { this.encoding = encoding; this.lineEndings = lineEndings; this.ratchetFrom = ratchetFrom; this.provisioner = provisioner; + this.p2Provisioner = p2Provisioner; this.fileLocator = fileLocator; this.globalStepFactories = globalStepFactories; this.spotlessSetLicenseHeaderYearsFromGitHistory = spotlessSetLicenseHeaderYearsFromGitHistory; @@ -64,6 +67,10 @@ public Provisioner getProvisioner() { return provisioner; } + public P2Provisioner getP2Provisioner() { + return p2Provisioner; + } + public List getGlobalStepFactories() { return unmodifiableList(globalStepFactories); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java index 6c7cc0d349..05c207d6d4 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -185,7 +185,7 @@ Optional ratchetFrom(FormatterConfig config) { } private FormatterStepConfig stepConfig(Charset encoding, FormatterConfig config) { - return new FormatterStepConfig(encoding, licenseHeaderDelimiter(), ratchetFrom(config), config.getProvisioner(), config.getFileLocator(), config.getSpotlessSetLicenseHeaderYearsFromGitHistory()); + return new FormatterStepConfig(encoding, licenseHeaderDelimiter(), ratchetFrom(config), config.getProvisioner(), config.getP2Provisioner(), config.getFileLocator(), config.getSpotlessSetLicenseHeaderYearsFromGitHistory()); } private static List gatherStepFactories(List allGlobal, List allConfigured) { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java index a1f2e52b41..139b3f0a69 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * 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. @@ -19,6 +19,7 @@ import java.util.Optional; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.P2Provisioner; public class FormatterStepConfig { @@ -26,14 +27,16 @@ public class FormatterStepConfig { private final String licenseHeaderDelimiter; private final Optional ratchetFrom; private final Provisioner provisioner; + private final P2Provisioner p2Provisioner; private final FileLocator fileLocator; private final Optional spotlessSetLicenseHeaderYearsFromGitHistory; - public FormatterStepConfig(Charset encoding, String licenseHeaderDelimiter, Optional ratchetFrom, Provisioner provisioner, FileLocator fileLocator, Optional spotlessSetLicenseHeaderYearsFromGitHistory) { + public FormatterStepConfig(Charset encoding, String licenseHeaderDelimiter, Optional ratchetFrom, Provisioner provisioner, P2Provisioner p2Provisioner, FileLocator fileLocator, Optional spotlessSetLicenseHeaderYearsFromGitHistory) { this.encoding = encoding; this.licenseHeaderDelimiter = licenseHeaderDelimiter; this.ratchetFrom = ratchetFrom; this.provisioner = provisioner; + this.p2Provisioner = p2Provisioner; this.fileLocator = fileLocator; this.spotlessSetLicenseHeaderYearsFromGitHistory = spotlessSetLicenseHeaderYearsFromGitHistory; } @@ -54,6 +57,10 @@ public Provisioner getProvisioner() { return provisioner; } + public P2Provisioner getP2Provisioner() { + return p2Provisioner; + } + public FileLocator getFileLocator() { return fileLocator; } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java index 5594562ad4..6709e78116 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -42,7 +42,7 @@ public class EclipseCdt implements FormatterStepFactory { @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { - EquoBasedStepBuilder eclipseConfig = EclipseCdtFormatterStep.createBuilder(stepConfig.getProvisioner()); + EquoBasedStepBuilder eclipseConfig = EclipseCdtFormatterStep.createBuilder(stepConfig.getProvisioner(), stepConfig.getP2Provisioner()); eclipseConfig.setVersion(version == null ? EclipseCdtFormatterStep.defaultVersion() : version); if (file != null) { File settingsFile = stepConfig.getFileLocator().locateFile(file); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java index 0929ab67e7..613a624c01 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2025 DiffPlug + * Copyright 2020-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ public class GrEclipse implements FormatterStepFactory { @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { - EquoBasedStepBuilder grEclipseConfig = GrEclipseFormatterStep.createBuilder(stepConfig.getProvisioner()); + EquoBasedStepBuilder grEclipseConfig = GrEclipseFormatterStep.createBuilder(stepConfig.getProvisioner(), stepConfig.getP2Provisioner()); grEclipseConfig.setVersion(version == null ? GrEclipseFormatterStep.defaultVersion() : version); if (file != null) { File settingsFile = stepConfig.getFileLocator().locateFile(file); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java index b93df43f3c..4d3df28cd7 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * 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. @@ -59,7 +59,7 @@ public class Eclipse implements FormatterStepFactory { @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { - EclipseJdtFormatterStep.Builder eclipseConfig = EclipseJdtFormatterStep.createBuilder(stepConfig.getProvisioner()); + EclipseJdtFormatterStep.Builder eclipseConfig = EclipseJdtFormatterStep.createBuilder(stepConfig.getProvisioner(), stepConfig.getP2Provisioner()); eclipseConfig.setVersion(version == null ? EclipseJdtFormatterStep.defaultVersion() : version); if (file != null) { File settingsFile = stepConfig.getFileLocator().locateFile(file);