prepareFormatterSteps() {
+ throw new IllegalStateException("This method must be overridden or not be called");
+ }
+}
diff --git a/cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java b/cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java
new file mode 100644
index 0000000000..32dad8b32d
--- /dev/null
+++ b/cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 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.cli.version;
+
+import java.util.Properties;
+
+import picocli.CommandLine;
+
+public class SpotlessCLIVersionProvider implements CommandLine.IVersionProvider {
+
+ @Override
+ public String[] getVersion() throws Exception {
+ // load application.properties
+ Properties properties = new Properties();
+ properties.load(getClass().getResourceAsStream("/application.properties"));
+ String version = properties.getProperty("cli.version");
+ return new String[]{"Spotless CLI version " + version};
+ }
+}
diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties
new file mode 100644
index 0000000000..6dc79bfbd7
--- /dev/null
+++ b/cli/src/main/resources/application.properties
@@ -0,0 +1 @@
+cli.version=@cli.version@
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java
new file mode 100644
index 0000000000..02ec270a8c
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.cli;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.BeforeEach;
+
+import com.diffplug.spotless.ResourceHarness;
+import com.diffplug.spotless.tag.CliNativeNpmTest;
+import com.diffplug.spotless.tag.CliNativeTest;
+import com.diffplug.spotless.tag.CliProcessNpmTest;
+import com.diffplug.spotless.tag.CliProcessTest;
+
+public abstract class CLIIntegrationHarness extends ResourceHarness {
+
+ /**
+ * Each test gets its own temp folder, and we create a gradle
+ * build there and run it.
+ *
+ * Because those test folders don't have a .gitattributes file,
+ * git (on windows) will default to \r\n. So now if you read a
+ * test file from the spotless test resources, and compare it
+ * to a build result, the line endings won't match.
+ *
+ * By sticking this .gitattributes file into the test directory,
+ * we ensure that the default Spotless line endings policy of
+ * GIT_ATTRIBUTES will use \n, so that tests match the test
+ * resources on win and linux.
+ */
+ @BeforeEach
+ void gitAttributes() throws IOException {
+ setFile(".gitattributes").toContent("* text eol=lf");
+ }
+
+ protected SpotlessCLIRunner cliRunner() {
+ return createRunnerForTag()
+ .withWorkingDir(rootFolder());
+ }
+
+ private SpotlessCLIRunner createRunnerForTag() {
+ CliProcessTest cliProcessTest = getClass().getAnnotation(CliProcessTest.class);
+ CliProcessNpmTest cliProcessNpmTest = getClass().getAnnotation(CliProcessNpmTest.class);
+ if (cliProcessTest != null || cliProcessNpmTest != null) {
+ return SpotlessCLIRunner.createExternalProcess();
+ }
+ CliNativeTest cliNativeTest = getClass().getAnnotation(CliNativeTest.class);
+ CliNativeNpmTest cliNativeNpmTest = getClass().getAnnotation(CliNativeNpmTest.class);
+ if (cliNativeTest != null || cliNativeNpmTest != null) {
+ return SpotlessCLIRunner.createNative();
+ }
+ return SpotlessCLIRunner.create();
+ }
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIHelpAndVersionTest.java b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIHelpAndVersionTest.java
new file mode 100644
index 0000000000..f352bfa747
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIHelpAndVersionTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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.cli;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+public class SpotlessCLIHelpAndVersionTest extends CLIIntegrationHarness {
+
+ @Test
+ void testHelp() {
+ SpotlessCLIRunner.Result result = cliRunner().withOption("--help").run();
+ assertThat(result.stdOut()).contains("Usage: spotless");
+ }
+
+ @Test
+ void testVersion() {
+ SpotlessCLIRunner.Result result = cliRunner().withOption("--version").run();
+ assertThat(result.stdOut()).contains("Spotless CLI version");
+ }
+
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java
new file mode 100644
index 0000000000..1a57901271
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 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.cli;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep;
+
+import picocli.CommandLine;
+
+public abstract class SpotlessCLIRunner {
+
+ private File workingDir = new File(".");
+
+ private final List args = new ArrayList<>();
+
+ public static SpotlessCLIRunner create() {
+ return new SpotlessCLIRunnerInSameThread();
+ }
+
+ public static SpotlessCLIRunner createExternalProcess() {
+ return new SpotlessCLIRunnerInExternalJavaProcess();
+ }
+
+ public static SpotlessCLIRunner createNative() {
+ return new SpotlessCLIRunnerInNativeExternalProcess();
+ }
+
+ public SpotlessCLIRunner withWorkingDir(@NotNull File workingDir) {
+ this.workingDir = Objects.requireNonNull(workingDir);
+ return this;
+ }
+
+ protected File workingDir() {
+ return workingDir;
+ }
+
+ public SpotlessCLIRunner withOption(@NotNull String option) {
+ args.add(Objects.requireNonNull(option));
+ return this;
+ }
+
+ public SpotlessCLIRunner withOption(@NotNull String option, @NotNull String value) {
+ args.add(String.format("%s=%s", Objects.requireNonNull(option), Objects.requireNonNull(value)));
+ return this;
+ }
+
+ public SpotlessCLIRunner withTargets(String... targets) {
+ for (String target : targets) {
+ withOption("--target", target);
+ }
+ return this;
+ }
+
+ public SpotlessCLIRunner withStep(@NotNull String stepName) {
+ args.add(Objects.requireNonNull(stepName));
+ return this;
+ }
+
+ public SpotlessCLIRunner withStep(@NotNull Class extends SpotlessCLIFormatterStep> stepClass) {
+ String stepName = determineStepName(stepClass);
+ return withStep(stepName);
+ }
+
+ private String determineStepName(Class extends SpotlessCLIFormatterStep> stepClass) {
+ CommandLine.Command annotation = stepClass.getAnnotation(CommandLine.Command.class);
+ if (annotation == null) {
+ throw new IllegalArgumentException("Step class must be annotated with @CommandLine.Command");
+ }
+ return annotation.name();
+ }
+
+ public Result run() {
+ Result result = executeCommand(args);
+ if (result.executionException() != null) {
+ throwRuntimeException("Error while executing Spotless CLI command", result);
+ }
+ if (result.exitCode == null || result.exitCode != 0) {
+ throwRuntimeException("Spotless CLI command failed with exit code " + result.exitCode, result);
+ }
+ return result;
+ }
+
+ public Result runAndFail() {
+ Result result = executeCommand(args);
+ if (result.executionException() != null) {
+ throwRuntimeException("Error while executing Spotless CLI command", result);
+ }
+ if (result.exitCode == null || result.exitCode == 0) {
+ throwRuntimeException("Spotless CLI command should have failed but exited with code " + result.exitCode, result);
+ }
+ return result;
+ }
+
+ private void throwRuntimeException(String message, Result result) {
+ StringBuilder sb = new StringBuilder(message)
+ .append("\nExit code: ").append(result.exitCode()).append("\n")
+ .append("\n--- Standard output: ---\n").append(result.stdOut()).append("\n------------------------\n")
+ .append("\n--- Standard error: ---\n").append(result.stdErr()).append("\n------------------------\n");
+
+ if (result.executionException() != null) {
+ throw new RuntimeException(sb.toString(), result.executionException());
+ }
+ throw new RuntimeException(sb.toString());
+ }
+
+ protected abstract Result executeCommand(List args);
+
+ public static class Result {
+
+ private final Integer exitCode;
+ private final String stdOut;
+ private final String stdErr;
+ private final Exception executionException;
+
+ protected Result(@Nullable Integer exitCode, @Nullable Exception executionException, @NotNull String stdOut, @NotNull String stdErr) {
+ this.exitCode = exitCode;
+ this.executionException = executionException;
+ this.stdOut = Objects.requireNonNull(stdOut);
+ this.stdErr = Objects.requireNonNull(stdErr);
+ }
+
+ public Integer exitCode() {
+ return exitCode;
+ }
+
+ public String stdOut() {
+ return stdOut;
+ }
+
+ public String stdErr() {
+ return stdErr;
+ }
+
+ public Exception executionException() {
+ return executionException;
+ }
+ }
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInExternalJavaProcess.java b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInExternalJavaProcess.java
new file mode 100644
index 0000000000..486077b7d5
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInExternalJavaProcess.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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.cli;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.diffplug.spotless.ProcessRunner;
+import com.diffplug.spotless.ThrowingEx;
+
+public class SpotlessCLIRunnerInExternalJavaProcess extends SpotlessCLIRunner {
+
+ private static final String SPOTLESS_CLI_SHADOW_JAR_SYSPROP = "spotless.cli.shadowJar";
+
+ public SpotlessCLIRunnerInExternalJavaProcess() {
+ super();
+ if (System.getProperty(SPOTLESS_CLI_SHADOW_JAR_SYSPROP) == null) {
+ throw new IllegalStateException("spotless.cli.shadowJar system property must be set to the path of the shadow jar");
+ }
+ }
+
+ protected Result executeCommand(List args) {
+ try (ProcessRunner runner = new ProcessRunner()) {
+
+ ProcessRunner.Result pResult = ThrowingEx.get(() -> runner.exec(
+ workingDir(),
+ System.getenv(),
+ null,
+ processArgs(args)));
+
+ return new Result(pResult.exitCode(), null, pResult.stdOutUtf8(), pResult.stdErrUtf8());
+ }
+ }
+
+ private List processArgs(List args) {
+ List processArgs = new ArrayList<>();
+ processArgs.add(currentJavaExecutable());
+ processArgs.add("-jar");
+ String jarPath = System.getProperty(SPOTLESS_CLI_SHADOW_JAR_SYSPROP);
+ processArgs.add(jarPath);
+
+ // processArgs.add(SpotlessCLI.class.getProtectionDomain().getCodeSource().getLocation().getPath());
+
+ processArgs.addAll(args);
+ return processArgs;
+ }
+
+ private String currentJavaExecutable() {
+ return ProcessHandle.current().info().command().orElse("java");
+ }
+
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInNativeExternalProcess.java b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInNativeExternalProcess.java
new file mode 100644
index 0000000000..414f7bdcb8
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInNativeExternalProcess.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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.cli;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.diffplug.spotless.ProcessRunner;
+import com.diffplug.spotless.ThrowingEx;
+
+public class SpotlessCLIRunnerInNativeExternalProcess extends SpotlessCLIRunner {
+
+ private static final String SPOTLESS_CLI_NATIVE_IMAGE_SYSPROP = "spotless.cli.nativeImage";
+
+ public SpotlessCLIRunnerInNativeExternalProcess() {
+ super();
+ if (System.getProperty(SPOTLESS_CLI_NATIVE_IMAGE_SYSPROP) == null) {
+ throw new IllegalStateException(SPOTLESS_CLI_NATIVE_IMAGE_SYSPROP + " system property must be set to the path of the native binary");
+ }
+ System.out.println("SpotlessCLIRunnerInNativeExternalProcess: " + System.getProperty(SPOTLESS_CLI_NATIVE_IMAGE_SYSPROP));
+ }
+
+ protected Result executeCommand(List args) {
+ try (ProcessRunner runner = new ProcessRunner()) {
+
+ ProcessRunner.Result pResult = ThrowingEx.get(() -> runner.exec(
+ workingDir(),
+ System.getenv(),
+ null,
+ processArgs(args)));
+
+ return new Result(pResult.exitCode(), null, pResult.stdOutUtf8(), pResult.stdErrUtf8());
+ }
+ }
+
+ private List processArgs(List args) {
+ List processArgs = new ArrayList<>();
+ processArgs.add(System.getProperty(SPOTLESS_CLI_NATIVE_IMAGE_SYSPROP));
+ // processArgs.add(SpotlessCLI.class.getProtectionDomain().getCodeSource().getLocation().getPath());
+
+ processArgs.addAll(args);
+ return processArgs;
+ }
+
+ private String currentJavaExecutable() {
+ return ProcessHandle.current().info().command().orElse("java");
+ }
+
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInSameThread.java b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInSameThread.java
new file mode 100644
index 0000000000..071434d538
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInSameThread.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 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.cli;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+import picocli.CommandLine;
+
+public class SpotlessCLIRunnerInSameThread extends SpotlessCLIRunner {
+
+ protected Result executeCommand(List args) {
+ SpotlessCLI cli = SpotlessCLI.createInstance();
+ CommandLine commandLine = SpotlessCLI.createCommandLine(cli);
+
+ StringWriter out = new StringWriter();
+ StringWriter err = new StringWriter();
+
+ try (PrintWriter outWriter = new PrintWriter(out);
+ PrintWriter errWriter = new PrintWriter(err)) {
+ commandLine.setOut(outWriter);
+ commandLine.setErr(errWriter);
+ Exception executionException = null;
+ Integer exitCode = null;
+ try {
+ exitCode = commandLine.execute(argsWithBaseDir(args));
+ } catch (Exception e) {
+ executionException = e;
+ }
+
+ // finalize
+ outWriter.flush();
+ errWriter.flush();
+ return new Result(exitCode, executionException, out.toString(), err.toString());
+ }
+ }
+
+ private String[] argsWithBaseDir(List args) {
+ // prepend the base dir
+ String[] argsWithBaseDir = new String[args.size() + 2];
+ argsWithBaseDir[0] = "--basedir";
+ argsWithBaseDir[1] = workingDir().getAbsolutePath();
+ System.arraycopy(args.toArray(new String[0]), 0, argsWithBaseDir, 2, args.size());
+ return argsWithBaseDir;
+ }
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/core/ChecksumCalculatorTest.java b/cli/src/test/java/com/diffplug/spotless/cli/core/ChecksumCalculatorTest.java
new file mode 100644
index 0000000000..4195940fcc
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/core/ChecksumCalculatorTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2024 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.cli.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Stream;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.cli.SpotlessAction;
+import com.diffplug.spotless.cli.SpotlessActionContextProvider;
+import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep;
+import com.diffplug.spotless.cli.steps.SpotlessFormatterStep;
+
+import picocli.CommandLine;
+
+class ChecksumCalculatorTest {
+
+ private ChecksumCalculator checksumCalculator = new ChecksumCalculator();
+
+ @Test
+ void itCalculatesAChecksumForStep() {
+ Step step = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+
+ String checksum = checksumCalculator.calculateChecksum(step);
+
+ assertThat(checksum).isNotNull();
+ }
+
+ @Test
+ void itCalculatesDifferentChecksumsForSteps() {
+ Step step1 = step(randomPath(), randomString(), argGroup(randomString(), null), List.of(randomPath(), randomPath()));
+ Step step2 = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+
+ String checksum1 = checksumCalculator.calculateChecksum(step1);
+ String checksum2 = checksumCalculator.calculateChecksum(step2);
+
+ assertThat(checksum1).isNotEqualTo(checksum2);
+ }
+
+ @Test
+ void itRecalculatesSameChecksumsForStep() {
+ Step step = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+
+ String checksum1 = checksumCalculator.calculateChecksum(step);
+ String checksum2 = checksumCalculator.calculateChecksum(step);
+
+ assertThat(checksum1).isEqualTo(checksum2);
+ }
+
+ @Test
+ void itCalculatesAChecksumForCommandLineStream() {
+ Step step = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+ Action action = action(randomPath());
+ SpotlessCommandLineStream commandLineStream = commandLine(action, step);
+
+ String checksum = checksumCalculator.calculateChecksum(commandLineStream);
+
+ assertThat(checksum).isNotNull();
+ }
+
+ @Test
+ void itCalculatesDifferentChecksumForDifferentCommandLineStreamDueToAction() {
+ Step step = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+ Action action1 = action(randomPath());
+ Action action2 = action(randomPath());
+ SpotlessCommandLineStream commandLineStream1 = commandLine(action1, step);
+ SpotlessCommandLineStream commandLineStream2 = commandLine(action2, step);
+
+ String checksum1 = checksumCalculator.calculateChecksum(commandLineStream1);
+ String checksum2 = checksumCalculator.calculateChecksum(commandLineStream2);
+
+ assertThat(checksum1).isNotEqualTo(checksum2);
+ }
+
+ @Test
+ void itCalculatesDifferentChecksumForDifferentCommandLineStreamDueToSteps() {
+ Step step1 = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+ Step step2 = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+ Action action = action(randomPath());
+ SpotlessCommandLineStream commandLineStream1 = commandLine(action, step1);
+ SpotlessCommandLineStream commandLineStream2 = commandLine(action, step2);
+
+ String checksum1 = checksumCalculator.calculateChecksum(commandLineStream1);
+ String checksum2 = checksumCalculator.calculateChecksum(commandLineStream2);
+
+ assertThat(checksum1).isNotEqualTo(checksum2);
+ }
+
+ @Test
+ void itCalculatesDifferentChecksumForDifferentCommandLineStreamDueToStepOrder() {
+ Step step1 = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+ Step step2 = step(randomPath(), randomString(), argGroup(null, randomByteArray()), List.of(randomPath(), randomPath()));
+ Action action = action(randomPath());
+ SpotlessCommandLineStream commandLineStream1 = commandLine(action, step1, step2);
+ SpotlessCommandLineStream commandLineStream2 = commandLine(action, step2, step1);
+
+ String checksum1 = checksumCalculator.calculateChecksum(commandLineStream1);
+ String checksum2 = checksumCalculator.calculateChecksum(commandLineStream2);
+
+ assertThat(checksum1).isNotEqualTo(checksum2);
+ }
+
+ @Test
+ void itDoesSomething2() {
+ System.out.println("Hello");
+ }
+
+ private static Step step(Path test1, String test2, StepArgGroup argGroup, List parameters) {
+ Step step = new Step();
+ step.test1 = test1;
+ step.test2 = test2;
+ step.argGroup = argGroup;
+ step.parameters = parameters;
+ return step;
+ }
+
+ private static StepArgGroup argGroup(String test3, byte[] test4) {
+ StepArgGroup argGroup = new StepArgGroup();
+ argGroup.test3 = test3;
+ argGroup.test4 = test4;
+ return argGroup;
+ }
+
+ private static Path randomPath() {
+ return Path.of(randomString());
+ }
+
+ private static byte[] randomByteArray() {
+ return randomString().getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String randomString() {
+ return Long.toHexString(ThreadLocalRandom.current().nextLong());
+ }
+
+ static class Step extends SpotlessFormatterStep {
+
+ @CommandLine.Option(names = "--test1")
+ Path test1;
+
+ @CommandLine.Option(names = "--test2")
+ String test2;
+
+ @CommandLine.ArgGroup(exclusive = true)
+ StepArgGroup argGroup;
+
+ @CommandLine.Parameters
+ List parameters;
+
+ @NotNull
+ @Override
+ public List prepareFormatterSteps(SpotlessActionContext context) {
+ return List.of();
+ }
+ }
+
+ static class StepArgGroup {
+ @CommandLine.Option(names = "--test3")
+ String test3;
+
+ @CommandLine.Option(names = "--test4")
+ byte[] test4;
+ }
+
+ private static Action action(Path baseDir) {
+ Action action = new Action();
+ action.baseDir = baseDir;
+ return action;
+ }
+
+ @CommandLine.Command(name = "action")
+ static class Action implements SpotlessAction {
+ @CommandLine.Option(names = {"--basedir"})
+ Path baseDir;
+
+ @Override
+ public Integer executeSpotlessAction(@NotNull List formatterSteps) {
+ return 0;
+ }
+ }
+
+ private static SpotlessCommandLineStream commandLine(SpotlessAction action, SpotlessFormatterStep... steps) {
+ return new FixedCommandLineStream(Arrays.asList(steps), List.of(action));
+ }
+
+ static class FixedCommandLineStream implements SpotlessCommandLineStream {
+ private final List formatterSteps;
+ private final List actions;
+
+ FixedCommandLineStream(List formatterSteps, List actions) {
+ this.formatterSteps = formatterSteps;
+ this.actions = actions;
+ }
+
+ @Override
+ public Stream formatterSteps() {
+ return formatterSteps.stream();
+ }
+
+ @Override
+ public Stream actions() {
+ return actions.stream();
+ }
+
+ @Override
+ public Stream contextProviders() {
+ return Stream.empty();
+ }
+ }
+
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatJavaProcessTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatJavaProcessTest.java
new file mode 100644
index 0000000000..939b3b395f
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatJavaProcessTest.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import com.diffplug.spotless.tag.CliProcessTest;
+
+@CliProcessTest
+public class GoogleJavaFormatJavaProcessTest extends GoogleJavaFormatTest {}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatNativeProcessTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatNativeProcessTest.java
new file mode 100644
index 0000000000..1aea50d8dd
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatNativeProcessTest.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import com.diffplug.spotless.tag.CliNativeTest;
+
+@CliNativeTest
+public class GoogleJavaFormatNativeProcessTest extends GoogleJavaFormatTest {
+
+ // TODO include the correct google-java-format class to be available in native
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatTest.java
new file mode 100644
index 0000000000..9cada7c590
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.cli.CLIIntegrationHarness;
+import com.diffplug.spotless.cli.SpotlessCLIRunner;
+
+public class GoogleJavaFormatTest extends CLIIntegrationHarness {
+
+ @Test
+ void formattingWithGoogleJavaFormatWorks() throws IOException {
+ setFile("Java.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test");
+
+ SpotlessCLIRunner.Result result = cliRunner().withTargets("*.java").withStep(GoogleJavaFormat.class).run();
+
+ System.out.println(result.stdOut());
+ System.out.println("-------");
+ System.out.println(result.stdErr());
+ assertFile("Java.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test");
+ }
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderJavaProcessTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderJavaProcessTest.java
new file mode 100644
index 0000000000..b6fa1f0bf1
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderJavaProcessTest.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import com.diffplug.spotless.tag.CliProcessTest;
+
+@CliProcessTest
+public class LicenseHeaderJavaProcessTest extends LicenseHeaderTest {}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderNativeProcessTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderNativeProcessTest.java
new file mode 100644
index 0000000000..e9659d24d4
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderNativeProcessTest.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import com.diffplug.spotless.tag.CliNativeTest;
+
+@CliNativeTest
+public class LicenseHeaderNativeProcessTest extends LicenseHeaderTest {}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java
new file mode 100644
index 0000000000..69702caa11
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.LocalDate;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.cli.CLIIntegrationHarness;
+import com.diffplug.spotless.cli.SpotlessCLIRunner;
+import com.diffplug.spotless.generic.LicenseHeaderStep;
+
+public class LicenseHeaderTest extends CLIIntegrationHarness {
+
+ @Test
+ void assertHeaderMustBeSpecified() {
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("**/*.java")
+ .withStep(LicenseHeader.class)
+ .runAndFail();
+
+ assertThat(result.stdErr())
+ .containsPattern(".*Missing required.*header.*");
+ }
+
+ @Test
+ void assertHeaderIsApplied() {
+ setFile("TestFile.java").toContent("public class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License */")
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertHeaderFileIsApplied() {
+ setFile("TestFile.java").toContent("public class TestFile {}");
+ setFile("header.txt").toContent("/* License */");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header-file", "header.txt")
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertDelimiterIsApplied() {
+ setFile("TestFile.java").toContent("/* keep me */\npublic class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License */")
+ .withOption("--delimiter", "\\/\\* keep me")
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License */\n/* keep me */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertYearModeIsApplied() {
+ setFile("TestFile.java").toContent("/* License (c) 2022 */\npublic class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License (c) $YEAR */")
+ .withOption("--year-mode", LicenseHeaderStep.YearMode.UPDATE_TO_TODAY.toString())
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License (c) 2022-" + LocalDate.now().getYear() + " */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertYearSeparatorIsApplied() {
+ setFile("TestFile.java").toContent("/* License (c) 2022...2023 */\npublic class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License (c) $YEAR */")
+ .withOption("--year-mode", LicenseHeaderStep.YearMode.UPDATE_TO_TODAY.toString())
+
+ .withOption("--year-separator", "...")
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License (c) 2022..." + LocalDate.now().getYear() + " */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertSkipLinesMatchingIsApplied() {
+ setFile("TestFile.java").toContent("/* skip me */\npublic class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License */")
+ .withOption("--skip-lines-matching", ".*skip me.*")
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* skip me */\n/* License */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertPreserveModeIsApplied() {
+ setFile("TestFile.java").toContent("/* License (c) 2022 */\npublic class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License (c) $YEAR */")
+ .withOption("--year-mode", LicenseHeaderStep.YearMode.PRESERVE.toString())
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License (c) 2022 */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertContentPatternIsAppliedIfMatching() {
+ setFile("TestFile.java").toContent("public class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License */")
+ .withOption("--content-pattern", ".*TestFile.*")
+ .run();
+
+ assertFile("TestFile.java").hasContent("/* License */\npublic class TestFile {}");
+ }
+
+ @Test
+ void assertContentPatternIsNotAppliedIfNotMatching() {
+ setFile("TestFile.java").toContent("public class TestFile {}");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("TestFile.java")
+ .withStep(LicenseHeader.class)
+ .withOption("--header", "/* License */")
+ .withOption("--content-pattern", ".*NonExistent.*")
+ .run();
+
+ assertFile("TestFile.java").hasContent("public class TestFile {}");
+ }
+}
diff --git a/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java
new file mode 100644
index 0000000000..2fea1ef9df
--- /dev/null
+++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 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.cli.steps;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.cli.CLIIntegrationHarness;
+import com.diffplug.spotless.cli.SpotlessCLIRunner;
+import com.diffplug.spotless.tag.NpmTest;
+
+@NpmTest
+public class PrettierTest extends CLIIntegrationHarness {
+
+ // TODO
+
+ @Test
+ void itRunsPrettierForTsFilesWithOptions() throws IOException {
+ setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("test.ts")
+ .withStep(Prettier.class)
+ .withOption("--prettier-config-option", "printWidth=20")
+ .withOption("--prettier-config-option", "parser=typescript")
+ .run();
+
+ assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean");
+ }
+
+ @Test
+ void itRunsPrettierForTsFilesWithOptionFile() throws Exception {
+ setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc.yml");
+ setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+
+ SpotlessCLIRunner.Result result = cliRunner()
+ .withTargets("test.ts")
+ .withStep(Prettier.class)
+ .withOption("--prettier-config-path", ".prettierrc.yml")
+ .run();
+
+ assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean");
+ }
+
+}
diff --git a/gradle.properties b/gradle.properties
index 3ed73f7fa1..9a17f782fb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -32,4 +32,5 @@ VER_JGIT=6.10.0.202406032230-r
VER_JUNIT=5.11.4
VER_ASSERTJ=3.27.3
VER_MOCKITO=5.15.2
-VER_SELFIE=2.4.2
\ No newline at end of file
+VER_SELFIE=2.4.2
+VER_PICOCLI=4.7.6
diff --git a/gradle/changelog.gradle b/gradle/changelog.gradle
index 994e3558dc..9e412795a8 100644
--- a/gradle/changelog.gradle
+++ b/gradle/changelog.gradle
@@ -6,6 +6,9 @@ if (project.name == 'plugin-gradle') {
} else if (project.name == 'plugin-maven') {
kind = 'maven'
releaseTitle = 'Maven Plugin'
+} else if (project.name == 'cli') {
+ kind = 'cli'
+ releaseTitle = 'Spotless CLI'
} else {
assert project == rootProject
kind = 'lib'
diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle
index 650c04b84b..eff60c4bfb 100644
--- a/gradle/special-tests.gradle
+++ b/gradle/special-tests.gradle
@@ -7,7 +7,11 @@ def special = [
'clang',
'gofmt',
'npm',
- 'shfmt'
+ 'shfmt',
+ 'cliProcess',
+ 'cliProcessNpm',
+ 'cliNative',
+ 'cliNativeNpm'
]
boolean isCiServer = System.getenv().containsKey("CI")
diff --git a/lib/src/main/java/com/diffplug/spotless/JarState.java b/lib/src/main/java/com/diffplug/spotless/JarState.java
index 8680932b9e..561cb8c02b 100644
--- a/lib/src/main/java/com/diffplug/spotless/JarState.java
+++ b/lib/src/main/java/com/diffplug/spotless/JarState.java
@@ -37,6 +37,13 @@
* catch changes in a SNAPSHOT version.
*/
public final class JarState implements Serializable {
+
+ private static ClassLoader OVERRIDE_CLASS_LOADER = null;
+
+ public static void setOverrideClassLoader(ClassLoader overrideClassLoader) {
+ OVERRIDE_CLASS_LOADER = overrideClassLoader;
+ }
+
/** A lazily evaluated JarState, which becomes a set of files when serialized. */
public static class Promised implements Serializable {
private static final long serialVersionUID = 1L;
@@ -133,6 +140,9 @@ URL[] jarUrls() {
* The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}.
*/
public ClassLoader getClassLoader() {
+ if (OVERRIDE_CLASS_LOADER != null) {
+ return OVERRIDE_CLASS_LOADER;
+ }
return SpotlessCache.instance().classloader(this);
}
@@ -145,6 +155,9 @@ public ClassLoader getClassLoader() {
* The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}.
*/
public ClassLoader getClassLoader(Serializable key) {
+ if (OVERRIDE_CLASS_LOADER != null) {
+ return OVERRIDE_CLASS_LOADER;
+ }
return SpotlessCache.instance().classloader(key, this);
}
}
diff --git a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java
index 941c1c376f..94a4106e37 100644
--- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java
@@ -190,7 +190,7 @@ private String sanitizePattern(@Nullable String pattern) {
}
private static final String DEFAULT_NAME_PREFIX = LicenseHeaderStep.class.getName();
- private static final String DEFAULT_YEAR_DELIMITER = "-";
+ public static final String DEFAULT_YEAR_DELIMITER = "-";
private static final List YEAR_TOKENS = Arrays.asList("$YEAR", "$today.year");
private static final SerializableFileFilter UNSUPPORTED_JVM_FILES_FILTER = SerializableFileFilter.skipFilesNamed(
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
index 27a1002df5..d75ee8d37d 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
@@ -17,12 +17,16 @@
import static java.util.Objects.requireNonNull;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
+import java.io.ObjectOutputStream;
import java.io.Serializable;
+import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
@@ -57,7 +61,30 @@ public static FormatterStep create(Map devDependencies, Provisio
requireNonNull(buildDir);
return FormatterStep.createLazy(NAME,
() -> new State(NAME, devDependencies, projectDir, buildDir, cacheDir, npmPathResolver, prettierConfig),
- State::createFormatterFunc);
+ PrettierFormatterStep::cachedStateToFormatterFunc);
+ }
+
+ // TODO (simschla, 21.11.2024): this is a hack for the POC
+ // problem is, that the function is instantiated multiple times for cli call, which
+ // results in concurrent initialization of the node_modules dir and starting multiple
+ // server instances.
+ // I'm not sure if this is intended/expected or if it is a bug, will have to check with the team.
+ // For now, I will cache the formatter function based on the state, so that it is only initialized once.
+ private static final ConcurrentHashMap CACHED_FORMATTERS = new ConcurrentHashMap<>();
+
+ public static FormatterFunc cachedStateToFormatterFunc(State state) {
+ String serializedState = serializeToBase64(state);
+ return CACHED_FORMATTERS.computeIfAbsent(serializedState, key -> state.createFormatterFunc());
+ }
+
+ private static String serializeToBase64(State state) {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(state);
+ return Base64.getEncoder().encodeToString(baos.toByteArray());
+ } catch (IOException e) {
+ throw ThrowingEx.asRuntime(e);
+ }
}
private static class State extends NpmFormatterStepStateBase implements Serializable {
diff --git a/settings.gradle b/settings.gradle
index 2ca7c46cf4..71574372c1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -23,6 +23,10 @@ plugins {
id 'com.gradle.develocity' version '3.19.1'
// https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md
id 'dev.equo.ide' version '1.7.8' apply false
+ // https://github.com/graalvm/native-build-tools/releases
+ id 'org.graalvm.buildtools.native' version '0.10.2' apply false
+ // https://github.com/GradleUp/shadow/releases
+ id 'com.gradleup.shadow' version '8.3.5' apply false
}
dependencyResolutionManagement {
@@ -76,6 +80,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = 'spotless'
+include 'cli' // command-line interface
include 'lib' // reusable library with no dependencies
include 'testlib' // library for sharing test infrastructure between the projects below
@@ -99,3 +104,4 @@ def getStartProperty(java.lang.String name) {
if (System.getenv('SPOTLESS_EXCLUDE_MAVEN') != 'true' && getStartProperty('SPOTLESS_EXCLUDE_MAVEN') != 'true') {
include 'plugin-maven' // maven-specific glue code
}
+
diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/CliNativeNpmTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/CliNativeNpmTest.java
new file mode 100644
index 0000000000..37d777eb3b
--- /dev/null
+++ b/testlib/src/main/java/com/diffplug/spotless/tag/CliNativeNpmTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021-2024 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.tag;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.Tag;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@Tag("cliNativeNpm")
+public @interface CliNativeNpmTest {}
diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/CliNativeTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/CliNativeTest.java
new file mode 100644
index 0000000000..d35473cc5b
--- /dev/null
+++ b/testlib/src/main/java/com/diffplug/spotless/tag/CliNativeTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021-2024 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.tag;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.Tag;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@Tag("cliNative")
+public @interface CliNativeTest {}
diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/CliProcessNpmTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/CliProcessNpmTest.java
new file mode 100644
index 0000000000..beec995592
--- /dev/null
+++ b/testlib/src/main/java/com/diffplug/spotless/tag/CliProcessNpmTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021-2024 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.tag;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.Tag;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@Tag("cliProcessNpm")
+public @interface CliProcessNpmTest {}
diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/CliProcessTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/CliProcessTest.java
new file mode 100644
index 0000000000..0973fe0a2b
--- /dev/null
+++ b/testlib/src/main/java/com/diffplug/spotless/tag/CliProcessTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021-2024 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.tag;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.Tag;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@Tag("cliProcess")
+public @interface CliProcessTest {}