From 271a154e7859ed7738360210a97859352ef8245a Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 31 Oct 2024 20:21:01 +0100 Subject: [PATCH 01/21] feat: poc of cli (step 1) --- cli/CHANGES.md | 0 cli/build.gradle | 62 +++++++++++++++ .../diffplug/spotless/cli/SpotlessCLI.java | 43 ++++++++++ .../spotless/cli/SpotlessCommand.java | 18 +++++ .../execution/SpotlessExecutionStrategy.java | 78 +++++++++++++++++++ .../subcommands/SpotlessActionCommand.java | 22 ++++++ .../subcommands/SpotlessActionSubCommand.java | 38 +++++++++ .../cli/subcommands/SpotlessApply.java | 23 ++++++ .../cli/subcommands/SpotlessCheck.java | 23 ++++++ .../subcommands/steps/SpotlessCLIStep.java | 22 ++++++ .../steps/generic/LicenseHeader.java | 40 ++++++++++ .../generic/RemoveMeLaterSubCommand.java | 34 ++++++++ .../steps/generic/SpotlessStepSubCommand.java | 29 +++++++ gradle.properties | 3 +- gradle/changelog.gradle | 3 + settings.gradle | 4 + 16 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 cli/CHANGES.md create mode 100644 cli/build.gradle create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/SpotlessCommand.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java diff --git a/cli/CHANGES.md b/cli/CHANGES.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/build.gradle b/cli/build.gradle new file mode 100644 index 0000000000..ad8916d277 --- /dev/null +++ b/cli/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'org.graalvm.buildtools.native' +} +apply from: rootProject.file('gradle/changelog.gradle') +ext.artifactId = project.artifactIdGradle +version = spotlessChangelog.versionNext +apply plugin: 'java-library' +apply plugin: 'application' +apply from: rootProject.file('gradle/java-setup.gradle') +apply from: rootProject.file('gradle/spotless-freshmark.gradle') + +dependencies { + // todo, unify with plugin-gradle/build.gradle -- BEGIN + if (version.endsWith('-SNAPSHOT') || (rootProject.spotlessChangelog.versionNext == rootProject.spotlessChangelog.versionLast)) { + api projects.lib + api projects.libExtra + } else { + api "com.diffplug.spotless:spotless-lib:${rootProject.spotlessChangelog.versionLast}" + api "com.diffplug.spotless:spotless-lib-extra:${rootProject.spotlessChangelog.versionLast}" + } + implementation "com.diffplug.durian:durian-core:${VER_DURIAN}" + implementation "com.diffplug.durian:durian-io:${VER_DURIAN}" + implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}" + implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}" + + testImplementation projects.testlib + testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" + testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" + testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" + testImplementation 'org.owasp.encoder:encoder:1.3.1' + testRuntimeOnly "org.junit.platform:junit-platform-launcher" + // todo, unify with plugin-gradle/build.gradle -- END + + implementation "info.picocli:picocli:${VER_PICOCLI}" + annotationProcessor "info.picocli:picocli-codegen:${VER_PICOCLI}" +} + +compileJava { + options.compilerArgs += [ + "-Aproject=${project.group}/${project.name}" + ] +} + +tasks.withType(org.graalvm.buildtools.gradle.tasks.GenerateResourcesConfigFile).configureEach { + notCompatibleWithConfigurationCache('https://github.com/britter/maven-plugin-development/issues/8') +} +tasks.withType(org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask).configureEach { + notCompatibleWithConfigurationCache('https://github.com/britter/maven-plugin-development/issues/8') +} + +// use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image +graalvmNative { + binaries { + main { + imageName = 'spotless' + mainClass = 'com.diffplug.spotless.cli.SpotlessCLI' + sharedLibrary = false + + runtimeArgs.add('--user=ABC') + } + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java new file mode 100644 index 0000000000..ca664983fb --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -0,0 +1,43 @@ +/* + * 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 com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; +import com.diffplug.spotless.cli.subcommands.SpotlessApply; +import com.diffplug.spotless.cli.subcommands.SpotlessCheck; + +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "spotless cli", mixinStandardHelpOptions = true, version = "spotless ${version}", // https://picocli.info/#_dynamic_version_information + description = "Runs spotless", subcommands = {SpotlessCheck.class, SpotlessApply.class}) +public class SpotlessCLI implements SpotlessCommand { + + @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit") + boolean versionRequested; + + @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "display this help message") + boolean usageHelpRequested; + + public static void main(String... args) { + // args = new String[]{"--version"}; + args = new String[]{"apply", "license-header", "--header-file", "abc.txt", "license-header", "--header", "abc"}; + int exitCode = new CommandLine(new SpotlessCLI()) + .setExecutionStrategy(new SpotlessExecutionStrategy()) + .execute(args); + System.exit(exitCode); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCommand.java new file mode 100644 index 0000000000..603bdb0df3 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCommand.java @@ -0,0 +1,18 @@ +/* + * 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; + +public interface SpotlessCommand {} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java new file mode 100644 index 0000000000..b005423617 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java @@ -0,0 +1,78 @@ +/* + * 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.execution; + +import static picocli.CommandLine.executeHelpRequest; + +import java.util.function.Function; + +import com.diffplug.spotless.cli.SpotlessCommand; +import com.diffplug.spotless.cli.subcommands.SpotlessActionCommand; +import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIStep; + +import picocli.CommandLine; + +public class SpotlessExecutionStrategy implements CommandLine.IExecutionStrategy { + + public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException { + Integer helpResult = executeHelpRequest(parseResult); + if (helpResult != null) { + return helpResult; + } + return runSpotlessActions(parseResult); + } + + private Integer runSpotlessActions(CommandLine.ParseResult parseResult) { + // 1. run setup (for combining steps handled as subcommands) + + // TODO: maybe collect a list of steps and pass them to the spotless action in step 2? + Integer prepareResult = runSpotlessRecursive(parseResult, this::prepareStep); + if (prepareResult != null) { + return prepareResult; + } + // 2. run spotless steps + return runSpotlessRecursive(parseResult, this::executeSpotlessAction); + } + + private Integer runSpotlessRecursive(CommandLine.ParseResult parseResult, Function action) { + SpotlessCommand spotlessCommand = parseResult.commandSpec().commandLine().getCommand(); + Integer result = action.apply(spotlessCommand); + if (result != null) { + return result; + } + for (CommandLine.ParseResult subCommand : parseResult.subcommands()) { + Integer subResult = runSpotlessRecursive(subCommand, action); + if (subResult != null) { + return subResult; + } + } + return null; + } + + private Integer prepareStep(SpotlessCommand spotlessCommand) { + if (spotlessCommand instanceof SpotlessCLIStep) { + ((SpotlessCLIStep) spotlessCommand).prepare(); + } + return null; + } + + private Integer executeSpotlessAction(SpotlessCommand spotlessCommand) { + if (spotlessCommand instanceof SpotlessActionCommand) { + return ((SpotlessActionCommand) spotlessCommand).executeSpotlessAction(); + } + return null; + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java new file mode 100644 index 0000000000..f28daf1674 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java @@ -0,0 +1,22 @@ +/* + * 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.subcommands; + +import com.diffplug.spotless.cli.SpotlessCommand; + +public interface SpotlessActionCommand extends SpotlessCommand { + Integer executeSpotlessAction(); +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java new file mode 100644 index 0000000000..699327c498 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.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.subcommands; + +import com.diffplug.spotless.cli.subcommands.steps.generic.LicenseHeader; +import com.diffplug.spotless.cli.subcommands.steps.generic.RemoveMeLaterSubCommand; + +import picocli.CommandLine; + +// repeatable subcommands: https://picocli.info/#_repeatable_subcommands_specification + +// access command spec: https://picocli.info/#spec-annotation + +@CommandLine.Command(mixinStandardHelpOptions = true, subcommandsRepeatable = true, subcommands = { + LicenseHeader.class, + RemoveMeLaterSubCommand.class +}) +public abstract class SpotlessActionSubCommand implements SpotlessActionCommand { + + @Override + public Integer executeSpotlessAction() { + System.out.println("Hello " + getClass().getSimpleName() + ", abc!"); + return 0; + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java new file mode 100644 index 0000000000..ccecfdd62e --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java @@ -0,0 +1,23 @@ +/* + * 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.subcommands; + +import picocli.CommandLine; + +@CommandLine.Command(name = "apply", description = "Runs spotless apply") +public class SpotlessApply extends SpotlessActionSubCommand { + +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java new file mode 100644 index 0000000000..e1217ab26b --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java @@ -0,0 +1,23 @@ +/* + * 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.subcommands; + +import picocli.CommandLine; + +@CommandLine.Command(name = "check", description = "Runs spotless check") +public class SpotlessCheck extends SpotlessActionSubCommand { + +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java new file mode 100644 index 0000000000..252c5a07c3 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java @@ -0,0 +1,22 @@ +/* + * 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.subcommands.steps; + +import com.diffplug.spotless.cli.SpotlessCommand; + +public interface SpotlessCLIStep extends SpotlessCommand { + void prepare(); +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java new file mode 100644 index 0000000000..054d14b612 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java @@ -0,0 +1,40 @@ +/* + * 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.subcommands.steps.generic; + +import java.io.File; + +import picocli.CommandLine; + +@CommandLine.Command(name = "license-header", description = "Runs license header") +public class LicenseHeader extends SpotlessStepSubCommand { + + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") + LicenseHeaderOption licenseHeaderOption; + + static class LicenseHeaderOption { + @CommandLine.Option(names = {"--header", "-H"}, required = true) + String header; + @CommandLine.Option(names = {"--header-file", "-f"}, required = true) + File headerFile; + } + + @Override + public void prepare() { + super.prepare(); + System.out.println(licenseHeaderOption.header != null ? "Header: " + licenseHeaderOption.header : "HeaderFile:" + licenseHeaderOption.headerFile); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java new file mode 100644 index 0000000000..36829d8f8e --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java @@ -0,0 +1,34 @@ +/* + * 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.subcommands.steps.generic; + +import java.io.File; + +import picocli.CommandLine; + +@CommandLine.Command(name = "ignoreme") +public class RemoveMeLaterSubCommand extends SpotlessStepSubCommand { + + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") + LicenseHeaderOption licenseHeaderOption; + + static class LicenseHeaderOption { + @CommandLine.Option(names = {"--header", "-H"}, required = true) + String header; + @CommandLine.Option(names = {"--header-file", "-f"}, required = true) + File headerFile; + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java new file mode 100644 index 0000000000..ae7129dc3e --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java @@ -0,0 +1,29 @@ +/* + * 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.subcommands.steps.generic; + +import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIStep; + +import picocli.CommandLine; + +@CommandLine.Command(mixinStandardHelpOptions = true) +public abstract class SpotlessStepSubCommand implements SpotlessCLIStep { + + @Override + public void prepare() { + System.out.println("Prepare SpotlessStepSubCommand " + getClass().getSimpleName() + ", abc!"); + } +} 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/settings.gradle b/settings.gradle index 2ca7c46cf4..f3a5230be3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,8 @@ 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 } dependencyResolutionManagement { @@ -76,6 +78,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 +102,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 } + From 6bf8840f0fc45d10602fbdd7774894ff6a0cc6b3 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Fri, 1 Nov 2024 06:37:21 +0100 Subject: [PATCH 02/21] feat: poc of cli (step 2) --- .../diffplug/spotless/cli/SpotlessCLI.java | 3 +- .../execution/SpotlessExecutionStrategy.java | 55 ++++++--------- .../subcommands/SpotlessActionCommand.java | 7 +- .../subcommands/SpotlessActionSubCommand.java | 8 ++- ...tep.java => SpotlessCLIFormatterStep.java} | 10 ++- .../steps/generic/LicenseHeader.java | 68 +++++++++++++++++-- .../generic/RemoveMeLaterSubCommand.java | 13 +++- ...a => SpotlessFormatterStepSubCommand.java} | 10 +-- 8 files changed, 120 insertions(+), 54 deletions(-) rename cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/{SpotlessCLIStep.java => SpotlessCLIFormatterStep.java} (75%) rename cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/{SpotlessStepSubCommand.java => SpotlessFormatterStepSubCommand.java} (72%) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index ca664983fb..bf28e68243 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -34,9 +34,10 @@ public class SpotlessCLI implements SpotlessCommand { public static void main(String... args) { // args = new String[]{"--version"}; - args = new String[]{"apply", "license-header", "--header-file", "abc.txt", "license-header", "--header", "abc"}; + args = new String[]{"apply", "license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; int exitCode = new CommandLine(new SpotlessCLI()) .setExecutionStrategy(new SpotlessExecutionStrategy()) + .setCaseInsensitiveEnumValuesAllowed(true) .execute(args); System.exit(exitCode); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java index b005423617..ff58d66f35 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java @@ -17,11 +17,12 @@ import static picocli.CommandLine.executeHelpRequest; -import java.util.function.Function; +import java.util.List; +import java.util.stream.Collectors; -import com.diffplug.spotless.cli.SpotlessCommand; +import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.subcommands.SpotlessActionCommand; -import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIStep; +import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; @@ -37,42 +38,28 @@ public int execute(CommandLine.ParseResult parseResult) throws CommandLine.Execu private Integer runSpotlessActions(CommandLine.ParseResult parseResult) { // 1. run setup (for combining steps handled as subcommands) + List steps = prepareFormatterSteps(parseResult); - // TODO: maybe collect a list of steps and pass them to the spotless action in step 2? - Integer prepareResult = runSpotlessRecursive(parseResult, this::prepareStep); - if (prepareResult != null) { - return prepareResult; - } // 2. run spotless steps - return runSpotlessRecursive(parseResult, this::executeSpotlessAction); + return executeSpotlessAction(parseResult, steps); } - private Integer runSpotlessRecursive(CommandLine.ParseResult parseResult, Function action) { - SpotlessCommand spotlessCommand = parseResult.commandSpec().commandLine().getCommand(); - Integer result = action.apply(spotlessCommand); - if (result != null) { - return result; - } - for (CommandLine.ParseResult subCommand : parseResult.subcommands()) { - Integer subResult = runSpotlessRecursive(subCommand, action); - if (subResult != null) { - return subResult; - } - } - return null; + private List prepareFormatterSteps(CommandLine.ParseResult parseResult) { + return parseResult.asCommandLineList().stream() + .map(CommandLine::getCommand) + .filter(command -> command instanceof SpotlessCLIFormatterStep) + .map(SpotlessCLIFormatterStep.class::cast) + .flatMap(step -> step.prepareFormatterSteps().stream()) + .collect(Collectors.toList()); } - private Integer prepareStep(SpotlessCommand spotlessCommand) { - if (spotlessCommand instanceof SpotlessCLIStep) { - ((SpotlessCLIStep) spotlessCommand).prepare(); - } - return null; - } - - private Integer executeSpotlessAction(SpotlessCommand spotlessCommand) { - if (spotlessCommand instanceof SpotlessActionCommand) { - return ((SpotlessActionCommand) spotlessCommand).executeSpotlessAction(); - } - return null; + private Integer executeSpotlessAction(CommandLine.ParseResult parseResult, List steps) { + return parseResult.asCommandLineList().stream() + .map(CommandLine::getCommand) + .filter(command -> command instanceof SpotlessActionCommand) + .map(SpotlessActionCommand.class::cast) + .findFirst() + .map(spotlessActionCommand -> spotlessActionCommand.executeSpotlessAction(steps)) + .orElse(-1); } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java index f28daf1674..c73c3ef3a8 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java @@ -15,8 +15,13 @@ */ package com.diffplug.spotless.cli.subcommands; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.SpotlessCommand; public interface SpotlessActionCommand extends SpotlessCommand { - Integer executeSpotlessAction(); + Integer executeSpotlessAction(@Nonnull List formatterSteps); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java index 699327c498..1dfa2fed88 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java @@ -15,6 +15,11 @@ */ package com.diffplug.spotless.cli.subcommands; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.subcommands.steps.generic.LicenseHeader; import com.diffplug.spotless.cli.subcommands.steps.generic.RemoveMeLaterSubCommand; @@ -31,8 +36,9 @@ public abstract class SpotlessActionSubCommand implements SpotlessActionCommand { @Override - public Integer executeSpotlessAction() { + public Integer executeSpotlessAction(@Nonnull List formatterSteps) { System.out.println("Hello " + getClass().getSimpleName() + ", abc!"); + formatterSteps.forEach(step -> System.out.println("Step: " + step)); return 0; } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIFormatterStep.java similarity index 75% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java rename to cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIFormatterStep.java index 252c5a07c3..2de6719983 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIStep.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIFormatterStep.java @@ -15,8 +15,14 @@ */ package com.diffplug.spotless.cli.subcommands.steps; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.SpotlessCommand; -public interface SpotlessCLIStep extends SpotlessCommand { - void prepare(); +public interface SpotlessCLIFormatterStep extends SpotlessCommand { + @Nonnull + List prepareFormatterSteps(); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java index 054d14b612..3aab896097 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java @@ -16,25 +16,81 @@ package com.diffplug.spotless.cli.subcommands.steps.generic; import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.antlr4.Antlr4Defaults; +import com.diffplug.spotless.cpp.CppDefaults; +import com.diffplug.spotless.generic.LicenseHeaderStep; +import com.diffplug.spotless.kotlin.KotlinConstants; +import com.diffplug.spotless.protobuf.ProtobufConstants; import picocli.CommandLine; @CommandLine.Command(name = "license-header", description = "Runs license header") -public class LicenseHeader extends SpotlessStepSubCommand { +public class LicenseHeader extends SpotlessFormatterStepSubCommand { @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") - LicenseHeaderOption licenseHeaderOption; + LicenseHeaderSourceOption licenseHeaderSourceOption; - static class LicenseHeaderOption { + @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") + LicenseHeaderDelimiterOption licenseHeaderDelimiterOption; + + static class LicenseHeaderSourceOption { @CommandLine.Option(names = {"--header", "-H"}, required = true) String header; @CommandLine.Option(names = {"--header-file", "-f"}, required = true) File headerFile; } + static class LicenseHeaderDelimiterOption { + + @CommandLine.Option(names = {"--delimiter", "-d"}, required = true) + String delimiter; + + @CommandLine.Option(names = {"--delimiter-for", "-D"}, required = true) + DefaultDelimiterType defaultDelimiterType; + } + + enum DefaultDelimiterType { + JAVA(LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER), CPP(CppDefaults.DELIMITER_EXPR), ANTLR4(Antlr4Defaults.licenseHeaderDelimiter()), GROOVY(LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER), PROTOBUF(ProtobufConstants.LICENSE_HEADER_DELIMITER), KOTLIN(KotlinConstants.LICENSE_HEADER_DELIMITER); + + private final String delimiterExpression; + + DefaultDelimiterType(String delimiterExpression) { + this.delimiterExpression = delimiterExpression; + } + } + + @Nonnull @Override - public void prepare() { - super.prepare(); - System.out.println(licenseHeaderOption.header != null ? "Header: " + licenseHeaderOption.header : "HeaderFile:" + licenseHeaderOption.headerFile); + public List prepareFormatterSteps() { + FormatterStep licenseHeaderStep = LicenseHeaderStep.headerDelimiter(headerSource(), delimiter()) + // TODO add more config options + .build(); + return List.of(licenseHeaderStep); + } + + private ThrowingEx.Supplier headerSource() { + if (licenseHeaderSourceOption.header != null) { + return () -> licenseHeaderSourceOption.header; + } else { + return () -> ThrowingEx.get(() -> Files.readString(licenseHeaderSourceOption.headerFile.toPath())); + } + } + + private String delimiter() { + if (licenseHeaderDelimiterOption == null) { + return DefaultDelimiterType.JAVA.delimiterExpression; + } + if (licenseHeaderDelimiterOption.delimiter != null) { + return licenseHeaderDelimiterOption.delimiter; + } else { + return licenseHeaderDelimiterOption.defaultDelimiterType.delimiterExpression; + } } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java index 36829d8f8e..ea87534dee 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java @@ -16,11 +16,16 @@ package com.diffplug.spotless.cli.subcommands.steps.generic; import java.io.File; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatterStep; import picocli.CommandLine; @CommandLine.Command(name = "ignoreme") -public class RemoveMeLaterSubCommand extends SpotlessStepSubCommand { +public class RemoveMeLaterSubCommand extends SpotlessFormatterStepSubCommand { @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") LicenseHeaderOption licenseHeaderOption; @@ -31,4 +36,10 @@ static class LicenseHeaderOption { @CommandLine.Option(names = {"--header-file", "-f"}, required = true) File headerFile; } + + @Nonnull + @Override + public List prepareFormatterSteps() { + return List.of(); + } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessFormatterStepSubCommand.java similarity index 72% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java rename to cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessFormatterStepSubCommand.java index ae7129dc3e..7ff70f635e 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessStepSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessFormatterStepSubCommand.java @@ -15,15 +15,9 @@ */ package com.diffplug.spotless.cli.subcommands.steps.generic; -import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIStep; +import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; @CommandLine.Command(mixinStandardHelpOptions = true) -public abstract class SpotlessStepSubCommand implements SpotlessCLIStep { - - @Override - public void prepare() { - System.out.println("Prepare SpotlessStepSubCommand " + getClass().getSimpleName() + ", abc!"); - } -} +public abstract class SpotlessFormatterStepSubCommand implements SpotlessCLIFormatterStep {} From 94dfdd320a7fffd8958976e051f365123154d737 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Wed, 6 Nov 2024 20:05:35 +0100 Subject: [PATCH 03/21] feat: poc of cli (step 3) --- .../diffplug/spotless/cli/SpotlessCLI.java | 12 +- .../spotless/cli/core/TargetResolver.java | 110 ++++++++++++++++++ .../subcommands/SpotlessActionSubCommand.java | 67 ++++++++++- .../steps/generic/LicenseHeader.java | 1 + .../version/SpotlessCLIVersionProvider.java | 26 +++++ 5 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index bf28e68243..40a106f731 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -15,15 +15,17 @@ */ package com.diffplug.spotless.cli; +import java.util.List; + import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; import com.diffplug.spotless.cli.subcommands.SpotlessApply; import com.diffplug.spotless.cli.subcommands.SpotlessCheck; +import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; import picocli.CommandLine; import picocli.CommandLine.Command; -@Command(name = "spotless cli", mixinStandardHelpOptions = true, version = "spotless ${version}", // https://picocli.info/#_dynamic_version_information - description = "Runs spotless", subcommands = {SpotlessCheck.class, SpotlessApply.class}) +@Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", subcommands = {SpotlessCheck.class, SpotlessApply.class}) public class SpotlessCLI implements SpotlessCommand { @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit") @@ -32,9 +34,13 @@ public class SpotlessCLI implements SpotlessCommand { @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "display this help message") boolean usageHelpRequested; + @CommandLine.Option(names = {"--target", "-t"}, required = true, arity = "1..*", description = "The target files to format", scope = CommandLine.ScopeType.INHERIT) + public List targets; + public static void main(String... args) { // args = new String[]{"--version"}; - args = new String[]{"apply", "license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; + // args = new String[]{"apply", "license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; + args = new String[]{"apply", "--target", "src/poc/java/**/*.java", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; int exitCode = new CommandLine(new SpotlessCLI()) .setExecutionStrategy(new SpotlessExecutionStrategy()) .setCaseInsensitiveEnumValuesAllowed(true) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java new file mode 100644 index 0000000000..fe79a078d6 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java @@ -0,0 +1,110 @@ +/* + * 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 java.util.function.Predicate.not; + +import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.diffplug.spotless.ThrowingEx; + +public class TargetResolver { + + private final List targets; + + public TargetResolver(List targets) { + this.targets = targets; + } + + public Stream resolveTargets() { + return targets.stream() + .flatMap(this::resolveTarget); + } + + private Stream resolveTarget(String target) { + + final boolean isGlob = target.contains("*") || target.contains("?"); + + if (isGlob) { + return resolveGlob(target); + } + return resolveDir(Path.of(target)); + } + + private Stream resolveDir(Path startDir) { + List collected = new ArrayList<>(); + ThrowingEx.run(() -> Files.walkFileTree(startDir, + EnumSet.of(FileVisitOption.FOLLOW_LINKS), + Integer.MAX_VALUE, + new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + collected.add(file); + return FileVisitResult.CONTINUE; + } + })); + return collected.stream(); + } + + private Stream resolveGlob(String glob) { + Path startDir; + String globPart; + // if the glob is absolute, we need to split the glob into its parts and use all parts except glob chars '*', '**', and '?' + String[] parts = glob.split("\\Q" + File.separator + "\\E"); + List startDirParts = Stream.of(parts) + .takeWhile(not(TargetResolver::isGlobPathPart)) + .collect(Collectors.toList()); + + startDir = Path.of(glob.startsWith(File.separator) ? File.separator : "", startDirParts.toArray(String[]::new)); + globPart = Stream.of(parts) + .skip(startDirParts.size()) + .collect(Collectors.joining(File.separator)); + + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + globPart); + List collected = new ArrayList<>(); + ThrowingEx.run(() -> Files.walkFileTree(startDir, + EnumSet.of(FileVisitOption.FOLLOW_LINKS), + Integer.MAX_VALUE, + new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (matcher.matches(file)) { + collected.add(file); + } + return FileVisitResult.CONTINUE; + } + })); + return collected.stream() + .map(Path::toAbsolutePath); + } + + private static boolean isGlobPathPart(String part) { + return part.contains("*") || part.contains("?") || part.matches(".*\\[.*].*") || part.matches(".*\\{.*}.*"); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java index 1dfa2fed88..e9b18f545c 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java @@ -15,11 +15,23 @@ */ package com.diffplug.spotless.cli.subcommands; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; +import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.xml.transform.Result; +import com.diffplug.spotless.FormatExceptionPolicyStrict; +import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.cli.SpotlessCLI; +import com.diffplug.spotless.cli.core.TargetResolver; import com.diffplug.spotless.cli.subcommands.steps.generic.LicenseHeader; import com.diffplug.spotless.cli.subcommands.steps.generic.RemoveMeLaterSubCommand; @@ -35,10 +47,59 @@ }) public abstract class SpotlessActionSubCommand implements SpotlessActionCommand { + @CommandLine.ParentCommand + SpotlessCLI parent; + @Override public Integer executeSpotlessAction(@Nonnull List formatterSteps) { - System.out.println("Hello " + getClass().getSimpleName() + ", abc!"); - formatterSteps.forEach(step -> System.out.println("Step: " + step)); - return 0; + TargetResolver targetResolver = new TargetResolver(parent.targets); + + try (Formatter formatter = Formatter.builder() + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(Charset.defaultCharset()) // TODO charset! + .rootDir(Paths.get(".")) // TODO root dir? + .steps(formatterSteps) + .exceptionPolicy(new FormatExceptionPolicyStrict()) + .build()) { + + boolean success = targetResolver.resolveTargets() + .parallel() // needed? + .map(target -> this.executeFormatter(formatter, target)) + .filter(result -> result.success && result.updated != null) + .peek(this::writeBack) + .allMatch(result -> result.success); + System.out.println("Hello " + getClass().getSimpleName() + ", abc! Files: " + new TargetResolver(parent.targets).resolveTargets().collect(Collectors.toList())); + System.out.println("success: " + success); + formatterSteps.forEach(step -> System.out.println("Step: " + step)); + return 0; + } + } + + private Result executeFormatter(Formatter formatter, Path target) { + System.out.println("Formatting file: " + target + " in Thread " + Thread.currentThread().getName()); + String targetContent = ThrowingEx.get(() -> Files.readString(target, Charset.defaultCharset())); // TODO charset! + + String computed = formatter.compute(targetContent, target.toFile()); + // computed is null if file already up to date + return new Result(target, true, computed); + } + + private void writeBack(Result result) { + if (result.updated != null) { + ThrowingEx.run(() -> Files.writeString(result.target, result.updated, Charset.defaultCharset())); // TODO charset! + } + // System.out.println("Writing back to file:" + result.target + " with content:\n" + result.updated); + } + + private static final class Result { + private final Path target; + private final boolean success; + private final String updated; + + public Result(Path target, boolean success, String updated) { + this.target = target; + this.success = success; + this.updated = updated; + } } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java index 3aab896097..ab9643b0dd 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java @@ -84,6 +84,7 @@ private ThrowingEx.Supplier headerSource() { } private String delimiter() { + // TODO (simschla, 01.11.2024): here should somehow be automatically determined which type is needed (e.g. by file extension of files to be formatted) if (licenseHeaderDelimiterOption == null) { return DefaultDelimiterType.JAVA.delimiterExpression; } 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..0e196447dd --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java @@ -0,0 +1,26 @@ +/* + * 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 picocli.CommandLine; + +public class SpotlessCLIVersionProvider implements CommandLine.IVersionProvider { + + @Override + public String[] getVersion() throws Exception { + return new String[]{"Spotless CLI 1.0.0", "TODO"}; + } +} From dc424ecb4d6d57ef46de93b09219c4b5d189dce5 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 7 Nov 2024 19:48:07 +0100 Subject: [PATCH 04/21] feat(build): poc cli --- cli/CHANGES.md | 8 ++++++++ cli/build.gradle | 8 ++++++++ settings.gradle | 2 ++ 3 files changed, 18 insertions(+) diff --git a/cli/CHANGES.md b/cli/CHANGES.md index e69de29bb2..2eb62859e9 100644 --- a/cli/CHANGES.md +++ b/cli/CHANGES.md @@ -0,0 +1,8 @@ +# spotless-cli releases + +We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format + +## [Unreleased] + +## [0.0.1] - 2024-11-06 +Anchor version number. diff --git a/cli/build.gradle b/cli/build.gradle index ad8916d277..6268007edd 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -1,5 +1,7 @@ plugins { id 'org.graalvm.buildtools.native' + id 'application' + id 'com.gradleup.shadow' } apply from: rootProject.file('gradle/changelog.gradle') ext.artifactId = project.artifactIdGradle @@ -48,6 +50,12 @@ tasks.withType(org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask).configu notCompatibleWithConfigurationCache('https://github.com/britter/maven-plugin-development/issues/8') } +application { + mainClass = 'com.diffplug.spotless.cli.SpotlessCLI' + applicationName = 'spotless' + archivesBaseName = 'spotless-cli' +} + // use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image graalvmNative { binaries { diff --git a/settings.gradle b/settings.gradle index f3a5230be3..71574372c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,8 @@ plugins { 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 { From 3d2b0ee05318a0d6018c6a2e672e57deee09ae5b Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 7 Nov 2024 19:49:15 +0100 Subject: [PATCH 05/21] feat: poc cli --- .../diffplug/spotless/cli/SpotlessCLI.java | 18 ++++++++++--- .../spotless/cli/help/OptionConstants.java | 27 +++++++++++++++++++ .../subcommands/SpotlessActionSubCommand.java | 5 ++-- 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 40a106f731..7cee446da7 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -15,9 +15,12 @@ */ package com.diffplug.spotless.cli; +import java.nio.charset.Charset; import java.util.List; +import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; +import com.diffplug.spotless.cli.help.OptionConstants; import com.diffplug.spotless.cli.subcommands.SpotlessApply; import com.diffplug.spotless.cli.subcommands.SpotlessCheck; import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; @@ -37,10 +40,19 @@ public class SpotlessCLI implements SpotlessCommand { @CommandLine.Option(names = {"--target", "-t"}, required = true, arity = "1..*", description = "The target files to format", scope = CommandLine.ScopeType.INHERIT) public List targets; + @CommandLine.Option(names = {"--encoding", "-e"}, defaultValue = "ISO8859-1", description = "The encoding of the files to format." + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) + public Charset encoding; + + @CommandLine.Option(names = {"--line-ending", "-l"}, defaultValue = "UNIX", description = "The line ending of the files to format." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) + public LineEnding lineEnding; + public static void main(String... args) { - // args = new String[]{"--version"}; - // args = new String[]{"apply", "license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; - args = new String[]{"apply", "--target", "src/poc/java/**/*.java", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; + if (args.length == 0) { + // args = new String[]{"--version"}; + // args = new String[]{"apply", "license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; + args = new String[]{"--version"}; + } + // args = new String[]{"apply", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; int exitCode = new CommandLine(new SpotlessCLI()) .setExecutionStrategy(new SpotlessExecutionStrategy()) .setCaseInsensitiveEnumValuesAllowed(true) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java b/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java new file mode 100644 index 0000000000..e3aace8028 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java @@ -0,0 +1,27 @@ +/* + * 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.help; + +public final class OptionConstants { + + public static final String VALID_VALUES_SUFFIX = " One of: ${COMPLETION-CANDIDATES}"; + + public static final String DEFAULT_VALUE_SUFFIX = " (default: ${DEFAULT-VALUE})"; + + private OptionConstants() { + // no instance + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java index e9b18f545c..4271b41a07 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java @@ -28,7 +28,6 @@ import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.cli.SpotlessCLI; import com.diffplug.spotless.cli.core.TargetResolver; @@ -55,8 +54,8 @@ public Integer executeSpotlessAction(@Nonnull List formatterSteps TargetResolver targetResolver = new TargetResolver(parent.targets); try (Formatter formatter = Formatter.builder() - .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) - .encoding(Charset.defaultCharset()) // TODO charset! + .lineEndingsPolicy(parent.lineEnding.createPolicy()) + .encoding(parent.encoding) .rootDir(Paths.get(".")) // TODO root dir? .steps(formatterSteps) .exceptionPolicy(new FormatExceptionPolicyStrict()) From b934acf5bf1c93e366d7ed5d61062010445e125e Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 7 Nov 2024 20:06:22 +0100 Subject: [PATCH 06/21] refactor: turn spotless action into an option instead of subcommand apply/check we now have --mode=APPLY or --mode=CHECK --- ...ActionCommand.java => SpotlessAction.java} | 5 +- .../diffplug/spotless/cli/SpotlessCLI.java | 94 ++++++++++++++-- .../execution/SpotlessExecutionStrategy.java | 10 +- .../steps/SpotlessCLIFormatterStep.java | 2 +- .../steps/generic/LicenseHeader.java | 2 +- .../generic/RemoveMeLaterSubCommand.java | 2 +- .../SpotlessFormatterStepSubCommand.java | 4 +- .../subcommands/SpotlessActionSubCommand.java | 104 ------------------ .../cli/subcommands/SpotlessApply.java | 23 ---- .../cli/subcommands/SpotlessCheck.java | 23 ---- 10 files changed, 96 insertions(+), 173 deletions(-) rename cli/src/main/java/com/diffplug/spotless/cli/{subcommands/SpotlessActionCommand.java => SpotlessAction.java} (82%) rename cli/src/main/java/com/diffplug/spotless/cli/{subcommands => }/steps/SpotlessCLIFormatterStep.java (94%) rename cli/src/main/java/com/diffplug/spotless/cli/{subcommands => }/steps/generic/LicenseHeader.java (98%) rename cli/src/main/java/com/diffplug/spotless/cli/{subcommands => }/steps/generic/RemoveMeLaterSubCommand.java (95%) rename cli/src/main/java/com/diffplug/spotless/cli/{subcommands => }/steps/generic/SpotlessFormatterStepSubCommand.java (84%) delete mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java delete mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java delete mode 100644 cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessAction.java similarity index 82% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java rename to cli/src/main/java/com/diffplug/spotless/cli/SpotlessAction.java index c73c3ef3a8..4216fd98bd 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessAction.java @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.subcommands; +package com.diffplug.spotless.cli; import java.util.List; import javax.annotation.Nonnull; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.cli.SpotlessCommand; -public interface SpotlessActionCommand extends SpotlessCommand { +public interface SpotlessAction extends SpotlessCommand { Integer executeSpotlessAction(@Nonnull List formatterSteps); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 7cee446da7..c7deace49e 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -16,28 +16,44 @@ package com.diffplug.spotless.cli; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatExceptionPolicyStrict; +import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.cli.core.TargetResolver; import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; import com.diffplug.spotless.cli.help.OptionConstants; -import com.diffplug.spotless.cli.subcommands.SpotlessApply; -import com.diffplug.spotless.cli.subcommands.SpotlessCheck; +import com.diffplug.spotless.cli.steps.generic.LicenseHeader; +import com.diffplug.spotless.cli.steps.generic.RemoveMeLaterSubCommand; import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; import picocli.CommandLine; import picocli.CommandLine.Command; -@Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", subcommands = {SpotlessCheck.class, SpotlessApply.class}) -public class SpotlessCLI implements SpotlessCommand { +@Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", subcommandsRepeatable = true, subcommands = { + LicenseHeader.class, + RemoveMeLaterSubCommand.class}) +public class SpotlessCLI implements SpotlessAction, SpotlessCommand { - @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit") + @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit.") boolean versionRequested; - @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "display this help message") + @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display this help message and exit.") boolean usageHelpRequested; - @CommandLine.Option(names = {"--target", "-t"}, required = true, arity = "1..*", description = "The target files to format", scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = {"--mode", "-m"}, defaultValue = "APPLY", description = "The mode to run spotless in." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) + SpotlessMode spotlessMode; + + @CommandLine.Option(names = {"--target", "-t"}, required = true, arity = "1..*", description = "The target files to format.", scope = CommandLine.ScopeType.INHERIT) public List targets; @CommandLine.Option(names = {"--encoding", "-e"}, defaultValue = "ISO8859-1", description = "The encoding of the files to format." + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) @@ -46,17 +62,75 @@ public class SpotlessCLI implements SpotlessCommand { @CommandLine.Option(names = {"--line-ending", "-l"}, defaultValue = "UNIX", description = "The line ending of the files to format." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) public LineEnding lineEnding; + @Override + public Integer executeSpotlessAction(@Nonnull List formatterSteps) { + TargetResolver targetResolver = new TargetResolver(targets); + + try (Formatter formatter = Formatter.builder() + .lineEndingsPolicy(lineEnding.createPolicy()) + .encoding(encoding) + .rootDir(Paths.get(".")) // TODO root dir? + .steps(formatterSteps) + .exceptionPolicy(new FormatExceptionPolicyStrict()) + .build()) { + + boolean success = targetResolver.resolveTargets() + .parallel() // needed? + .map(target -> this.executeFormatter(formatter, target)) + .filter(result -> result.success && result.updated != null) + .peek(this::writeBack) + .allMatch(result -> result.success); + System.out.println("Hello " + getClass().getSimpleName() + ", abc! Files: " + new TargetResolver(targets).resolveTargets().collect(Collectors.toList())); + System.out.println("success: " + success); + formatterSteps.forEach(step -> System.out.println("Step: " + step)); + return 0; + } + } + + private Result executeFormatter(Formatter formatter, Path target) { + System.out.println("Formatting file: " + target + " in Thread " + Thread.currentThread().getName()); + String targetContent = ThrowingEx.get(() -> Files.readString(target, Charset.defaultCharset())); // TODO charset! + + String computed = formatter.compute(targetContent, target.toFile()); + // computed is null if file already up to date + return new Result(target, true, computed); + } + + private void writeBack(Result result) { + if (result.updated != null) { + ThrowingEx.run(() -> Files.writeString(result.target, result.updated, Charset.defaultCharset())); // TODO charset! + } + // System.out.println("Writing back to file:" + result.target + " with content:\n" + result.updated); + } + public static void main(String... args) { if (args.length == 0) { // args = new String[]{"--version"}; - // args = new String[]{"apply", "license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; - args = new String[]{"--version"}; + // args = new String[]{"license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; + + args = new String[]{"--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; + // args = new String[]{"--version"}; } - // args = new String[]{"apply", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; int exitCode = new CommandLine(new SpotlessCLI()) .setExecutionStrategy(new SpotlessExecutionStrategy()) .setCaseInsensitiveEnumValuesAllowed(true) .execute(args); System.exit(exitCode); } + + private enum SpotlessMode { + CHECK, APPLY + } + + private static final class Result { + private final Path target; + private final boolean success; + private final String updated; + + public Result(Path target, boolean success, String updated) { + this.target = target; + this.success = success; + this.updated = updated; + } + } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java index ff58d66f35..3db8b2f0f6 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java @@ -21,8 +21,8 @@ import java.util.stream.Collectors; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.cli.subcommands.SpotlessActionCommand; -import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIFormatterStep; +import com.diffplug.spotless.cli.SpotlessAction; +import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; @@ -56,10 +56,10 @@ private List prepareFormatterSteps(CommandLine.ParseResult parseR private Integer executeSpotlessAction(CommandLine.ParseResult parseResult, List steps) { return parseResult.asCommandLineList().stream() .map(CommandLine::getCommand) - .filter(command -> command instanceof SpotlessActionCommand) - .map(SpotlessActionCommand.class::cast) + .filter(command -> command instanceof SpotlessAction) + .map(SpotlessAction.class::cast) .findFirst() - .map(spotlessActionCommand -> spotlessActionCommand.executeSpotlessAction(steps)) + .map(spotlessAction -> spotlessAction.executeSpotlessAction(steps)) .orElse(-1); } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIFormatterStep.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java similarity index 94% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIFormatterStep.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java index 2de6719983..f451e444fb 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/SpotlessCLIFormatterStep.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.subcommands.steps; +package com.diffplug.spotless.cli.steps; import java.util.List; diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java similarity index 98% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java index ab9643b0dd..d4b998a566 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.subcommands.steps.generic; +package com.diffplug.spotless.cli.steps.generic; import java.io.File; import java.nio.file.Files; diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/RemoveMeLaterSubCommand.java similarity index 95% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/generic/RemoveMeLaterSubCommand.java index ea87534dee..3b3096c4f7 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/RemoveMeLaterSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/RemoveMeLaterSubCommand.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.subcommands.steps.generic; +package com.diffplug.spotless.cli.steps.generic; import java.io.File; import java.util.List; diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessFormatterStepSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java similarity index 84% rename from cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessFormatterStepSubCommand.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java index 7ff70f635e..b6ea2e3843 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/steps/generic/SpotlessFormatterStepSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.subcommands.steps.generic; +package com.diffplug.spotless.cli.steps.generic; -import com.diffplug.spotless.cli.subcommands.steps.SpotlessCLIFormatterStep; +import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java deleted file mode 100644 index 4271b41a07..0000000000 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessActionSubCommand.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.subcommands; - -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; - -import javax.annotation.Nonnull; -import javax.xml.transform.Result; - -import com.diffplug.spotless.FormatExceptionPolicyStrict; -import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.ThrowingEx; -import com.diffplug.spotless.cli.SpotlessCLI; -import com.diffplug.spotless.cli.core.TargetResolver; -import com.diffplug.spotless.cli.subcommands.steps.generic.LicenseHeader; -import com.diffplug.spotless.cli.subcommands.steps.generic.RemoveMeLaterSubCommand; - -import picocli.CommandLine; - -// repeatable subcommands: https://picocli.info/#_repeatable_subcommands_specification - -// access command spec: https://picocli.info/#spec-annotation - -@CommandLine.Command(mixinStandardHelpOptions = true, subcommandsRepeatable = true, subcommands = { - LicenseHeader.class, - RemoveMeLaterSubCommand.class -}) -public abstract class SpotlessActionSubCommand implements SpotlessActionCommand { - - @CommandLine.ParentCommand - SpotlessCLI parent; - - @Override - public Integer executeSpotlessAction(@Nonnull List formatterSteps) { - TargetResolver targetResolver = new TargetResolver(parent.targets); - - try (Formatter formatter = Formatter.builder() - .lineEndingsPolicy(parent.lineEnding.createPolicy()) - .encoding(parent.encoding) - .rootDir(Paths.get(".")) // TODO root dir? - .steps(formatterSteps) - .exceptionPolicy(new FormatExceptionPolicyStrict()) - .build()) { - - boolean success = targetResolver.resolveTargets() - .parallel() // needed? - .map(target -> this.executeFormatter(formatter, target)) - .filter(result -> result.success && result.updated != null) - .peek(this::writeBack) - .allMatch(result -> result.success); - System.out.println("Hello " + getClass().getSimpleName() + ", abc! Files: " + new TargetResolver(parent.targets).resolveTargets().collect(Collectors.toList())); - System.out.println("success: " + success); - formatterSteps.forEach(step -> System.out.println("Step: " + step)); - return 0; - } - } - - private Result executeFormatter(Formatter formatter, Path target) { - System.out.println("Formatting file: " + target + " in Thread " + Thread.currentThread().getName()); - String targetContent = ThrowingEx.get(() -> Files.readString(target, Charset.defaultCharset())); // TODO charset! - - String computed = formatter.compute(targetContent, target.toFile()); - // computed is null if file already up to date - return new Result(target, true, computed); - } - - private void writeBack(Result result) { - if (result.updated != null) { - ThrowingEx.run(() -> Files.writeString(result.target, result.updated, Charset.defaultCharset())); // TODO charset! - } - // System.out.println("Writing back to file:" + result.target + " with content:\n" + result.updated); - } - - private static final class Result { - private final Path target; - private final boolean success; - private final String updated; - - public Result(Path target, boolean success, String updated) { - this.target = target; - this.success = success; - this.updated = updated; - } - } -} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java deleted file mode 100644 index ccecfdd62e..0000000000 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessApply.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.subcommands; - -import picocli.CommandLine; - -@CommandLine.Command(name = "apply", description = "Runs spotless apply") -public class SpotlessApply extends SpotlessActionSubCommand { - -} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java b/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java deleted file mode 100644 index e1217ab26b..0000000000 --- a/cli/src/main/java/com/diffplug/spotless/cli/subcommands/SpotlessCheck.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.subcommands; - -import picocli.CommandLine; - -@CommandLine.Command(name = "check", description = "Runs spotless check") -public class SpotlessCheck extends SpotlessActionSubCommand { - -} From 403c427fe680f1940e0ca7733e4c98b8a919adf7 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 7 Nov 2024 20:15:37 +0100 Subject: [PATCH 07/21] fix: adapt api after rebuild --- cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index c7deace49e..948663c701 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -18,13 +18,11 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nonnull; -import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; @@ -69,9 +67,7 @@ public Integer executeSpotlessAction(@Nonnull List formatterSteps try (Formatter formatter = Formatter.builder() .lineEndingsPolicy(lineEnding.createPolicy()) .encoding(encoding) - .rootDir(Paths.get(".")) // TODO root dir? .steps(formatterSteps) - .exceptionPolicy(new FormatExceptionPolicyStrict()) .build()) { boolean success = targetResolver.resolveTargets() From dfc91c70de892211b3ad4dc3b5db292c10691423 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Fri, 8 Nov 2024 16:37:38 +0100 Subject: [PATCH 08/21] feat: allow dynamic version info --- cli/build.gradle | 30 +++++++++++++++++-- .../version/SpotlessCLIVersionProvider.java | 8 ++++- cli/src/main/resources/application.properties | 1 + 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 cli/src/main/resources/application.properties diff --git a/cli/build.gradle b/cli/build.gradle index 6268007edd..f7cac306a2 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -38,8 +38,11 @@ dependencies { } compileJava { + // options for picocli codegen + // https://github.com/remkop/picocli/tree/main/picocli-codegen#222-other-options options.compilerArgs += [ - "-Aproject=${project.group}/${project.name}" + "-Aproject=${project.group}/${project.name}", + "-Aother.resource.bundles=application" ] } @@ -50,6 +53,29 @@ tasks.withType(org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask).configu notCompatibleWithConfigurationCache('https://github.com/britter/maven-plugin-development/issues/8') } +tasks.withType(ProcessResources).configureEach(new ApplicationPropertiesProcessResourcesAction(project.version)) + +class ApplicationPropertiesProcessResourcesAction implements Action { + + private final String cliVersion + + ApplicationPropertiesProcessResourcesAction(String cliVersion) { + this.cliVersion = cliVersion + } + + @Override + void execute(ProcessResources processResources) { + processResources.filesMatching("application.properties") { + filter( + org.apache.tools.ant.filters.ReplaceTokens, + tokens: [ + 'cli.version': cliVersion + ] + ) + } + } +} + application { mainClass = 'com.diffplug.spotless.cli.SpotlessCLI' applicationName = 'spotless' @@ -63,8 +89,6 @@ graalvmNative { imageName = 'spotless' mainClass = 'com.diffplug.spotless.cli.SpotlessCLI' sharedLibrary = false - - runtimeArgs.add('--user=ABC') } } } 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 index 0e196447dd..32dad8b32d 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/version/SpotlessCLIVersionProvider.java @@ -15,12 +15,18 @@ */ 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 { - return new String[]{"Spotless CLI 1.0.0", "TODO"}; + // 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@ From 62da16e2d4b74b14e9a603cfb83abf832ef68a36 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Fri, 8 Nov 2024 16:39:31 +0100 Subject: [PATCH 09/21] feat: adapt to use new linting api --- .../diffplug/spotless/cli/SpotlessCLI.java | 112 +++++++++++++----- .../spotless/cli/core/TargetResolver.java | 3 +- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 948663c701..c900d7c44f 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -16,9 +16,9 @@ package com.diffplug.spotless.cli; import java.nio.charset.Charset; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -26,6 +26,7 @@ import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintState; import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.cli.core.TargetResolver; import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; @@ -70,41 +71,70 @@ public Integer executeSpotlessAction(@Nonnull List formatterSteps .steps(formatterSteps) .build()) { - boolean success = targetResolver.resolveTargets() + ResultType resultType = targetResolver.resolveTargets() // TODO result .parallel() // needed? - .map(target -> this.executeFormatter(formatter, target)) - .filter(result -> result.success && result.updated != null) - .peek(this::writeBack) - .allMatch(result -> result.success); + .map(path -> ThrowingEx.get(() -> new Result(path, LintState.of(formatter, path.toFile())))) // TODO handle suppressions, see SpotlessTaskImpl + .map(result -> this.handleResult(formatter, result)) + .reduce(ResultType.CLEAN, ResultType::combineWith); System.out.println("Hello " + getClass().getSimpleName() + ", abc! Files: " + new TargetResolver(targets).resolveTargets().collect(Collectors.toList())); - System.out.println("success: " + success); + System.out.println("result: " + resultType); formatterSteps.forEach(step -> System.out.println("Step: " + step)); return 0; } } - private Result executeFormatter(Formatter formatter, Path target) { - System.out.println("Formatting file: " + target + " in Thread " + Thread.currentThread().getName()); - String targetContent = ThrowingEx.get(() -> Files.readString(target, Charset.defaultCharset())); // TODO charset! - - String computed = formatter.compute(targetContent, target.toFile()); - // computed is null if file already up to date - return new Result(target, true, computed); - } - - private void writeBack(Result result) { - if (result.updated != null) { - ThrowingEx.run(() -> Files.writeString(result.target, result.updated, Charset.defaultCharset())); // TODO charset! + private ResultType handleResult(Formatter formatter, Result result) { + if (result.lintState.isClean()) { + System.out.println("File is clean: " + result.target.toFile().getName()); + return ResultType.CLEAN; + } + if (result.lintState.getDirtyState().didNotConverge()) { + System.out.println("File did not converge: " + result.target.toFile().getName()); + return ResultType.DID_NOT_CONVERGE; + } + this.spotlessMode.action.accept(formatter, result); + return ResultType.DIRTY; + + /* + if (lintState.getDirtyState().isClean()) { + // Remove previous output if it exists + Files.deleteIfExists(cleanFile.toPath()); + } else if (lintState.getDirtyState().didNotConverge()) { + getLogger().warn("Skipping '{}' because it does not converge. Run {@code spotlessDiagnose} to understand why", relativePath); + } else { + Path parentDir = cleanFile.toPath().getParent(); + if (parentDir == null) { + throw new IllegalStateException("Every file has a parent folder. But not: " + cleanFile); + } + Files.createDirectories(parentDir); + // Need to copy the original file to the tmp location just to remember the file attributes + Files.copy(input.toPath(), cleanFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + + getLogger().info(String.format("Writing clean file: %s", cleanFile)); + lintState.getDirtyState().writeCanonicalTo(cleanFile); + } + if (!lintState.isHasLints()) { + Files.deleteIfExists(lintFile.toPath()); + } else { + LinkedHashMap> lints = lintState.getLintsByStep(formatter); + SerializableMisc.toFile(lints, lintFile); } - // System.out.println("Writing back to file:" + result.target + " with content:\n" + result.updated); + */ } + // private void writeBack(Result result) { + // if (result.updated != null) { + // ThrowingEx.run(() -> Files.writeString(result.target, result.updated, Charset.defaultCharset())); // TODO charset! + // } + // System.out.println("Writing back to file:" + result.target + " with content:\n" + result.updated); + // } + public static void main(String... args) { if (args.length == 0) { // args = new String[]{"--version"}; // args = new String[]{"license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; - args = new String[]{"--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; + args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; // args = new String[]{"--version"}; } int exitCode = new CommandLine(new SpotlessCLI()) @@ -115,18 +145,46 @@ public static void main(String... args) { } private enum SpotlessMode { - CHECK, APPLY + CHECK(((formatter, result) -> { + if (result.lintState.isHasLints()) { + result.lintState.asStringOneLine(result.target.toFile(), formatter); + } else { + System.out.println(String.format("%s is violating formatting rules.", result.target)); + } + })), APPLY(((formatter, result) -> ThrowingEx.run(() -> result.lintState.getDirtyState().writeCanonicalTo(result.target.toFile())))); + + private final BiConsumer action; + + SpotlessMode(BiConsumer action) { + this.action = action; + } + + } + + private enum ResultType { + CLEAN, DIRTY, DID_NOT_CONVERGE; + + ResultType combineWith(ResultType other) { + if (this == other) { + return this; + } + if (this == DID_NOT_CONVERGE || other == DID_NOT_CONVERGE) { + return DID_NOT_CONVERGE; + } + if (this == DIRTY || other == DIRTY) { + return DIRTY; + } + throw new IllegalStateException("Unexpected combination of result types: " + this + " and " + other); + } } private static final class Result { private final Path target; - private final boolean success; - private final String updated; + private final LintState lintState; - public Result(Path target, boolean success, String updated) { + public Result(Path target, LintState lintState) { this.target = target; - this.success = success; - this.updated = updated; + this.lintState = lintState; } } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java index fe79a078d6..0f8ca965e1 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java @@ -101,7 +101,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { } })); return collected.stream() - .map(Path::toAbsolutePath); + .map(Path::normalize); + // .map(Path::toAbsolutePath); } private static boolean isGlobPathPart(String part) { From 6680e1ba329b2d1762ec678995d76e4f8768eb8c Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Fri, 8 Nov 2024 21:41:13 +0100 Subject: [PATCH 10/21] feat: extend framework services - allow filetype to be inferred - calculate exit code based on result --- .../cli/SpotlessActionContextProvider.java | 23 ++++ .../diffplug/spotless/cli/SpotlessCLI.java | 115 +++++++++-------- .../cli/core/SpotlessActionContext.java | 34 +++++ .../cli/core/TargetFileTypeInferer.java | 122 ++++++++++++++++++ .../execution/SpotlessExecutionStrategy.java | 25 +++- .../cli/steps/SpotlessCLIFormatterStep.java | 5 +- .../cli/steps/generic/LicenseHeader.java | 60 +++++---- .../SpotlessFormatterStepSubCommand.java | 19 ++- 8 files changed, 313 insertions(+), 90 deletions(-) create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/TargetFileTypeInferer.java diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java new file mode 100644 index 0000000000..556d5385ff --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java @@ -0,0 +1,23 @@ +/* + * 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 com.diffplug.spotless.cli.core.SpotlessActionContext; + +public interface SpotlessActionContextProvider { + + SpotlessActionContext spotlessActionContext(); +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index c900d7c44f..a9dabbd486 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -18,8 +18,6 @@ import java.nio.charset.Charset; import java.nio.file.Path; import java.util.List; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -28,6 +26,8 @@ import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.LintState; import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.cli.core.TargetFileTypeInferer; import com.diffplug.spotless.cli.core.TargetResolver; import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; import com.diffplug.spotless.cli.help.OptionConstants; @@ -41,7 +41,7 @@ @Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", subcommandsRepeatable = true, subcommands = { LicenseHeader.class, RemoveMeLaterSubCommand.class}) -public class SpotlessCLI implements SpotlessAction, SpotlessCommand { +public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider { @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit.") boolean versionRequested; @@ -71,70 +71,46 @@ public Integer executeSpotlessAction(@Nonnull List formatterSteps .steps(formatterSteps) .build()) { - ResultType resultType = targetResolver.resolveTargets() // TODO result + ResultType resultType = targetResolver.resolveTargets() .parallel() // needed? .map(path -> ThrowingEx.get(() -> new Result(path, LintState.of(formatter, path.toFile())))) // TODO handle suppressions, see SpotlessTaskImpl .map(result -> this.handleResult(formatter, result)) .reduce(ResultType.CLEAN, ResultType::combineWith); - System.out.println("Hello " + getClass().getSimpleName() + ", abc! Files: " + new TargetResolver(targets).resolveTargets().collect(Collectors.toList())); - System.out.println("result: " + resultType); - formatterSteps.forEach(step -> System.out.println("Step: " + step)); - return 0; + return spotlessMode.translateResultTypeToExitCode(resultType); } } private ResultType handleResult(Formatter formatter, Result result) { if (result.lintState.isClean()) { - System.out.println("File is clean: " + result.target.toFile().getName()); + // System.out.println("File is clean: " + result.target.toFile().getName()); return ResultType.CLEAN; } if (result.lintState.getDirtyState().didNotConverge()) { - System.out.println("File did not converge: " + result.target.toFile().getName()); + System.err.println("File did not converge: " + result.target.toFile().getName()); // TODO: where to print the output to? return ResultType.DID_NOT_CONVERGE; } - this.spotlessMode.action.accept(formatter, result); + this.spotlessMode.handleResult(formatter, result); return ResultType.DIRTY; - /* - if (lintState.getDirtyState().isClean()) { - // Remove previous output if it exists - Files.deleteIfExists(cleanFile.toPath()); - } else if (lintState.getDirtyState().didNotConverge()) { - getLogger().warn("Skipping '{}' because it does not converge. Run {@code spotlessDiagnose} to understand why", relativePath); - } else { - Path parentDir = cleanFile.toPath().getParent(); - if (parentDir == null) { - throw new IllegalStateException("Every file has a parent folder. But not: " + cleanFile); - } - Files.createDirectories(parentDir); - // Need to copy the original file to the tmp location just to remember the file attributes - Files.copy(input.toPath(), cleanFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + } - getLogger().info(String.format("Writing clean file: %s", cleanFile)); - lintState.getDirtyState().writeCanonicalTo(cleanFile); - } - if (!lintState.isHasLints()) { - Files.deleteIfExists(lintFile.toPath()); - } else { - LinkedHashMap> lints = lintState.getLintsByStep(formatter); - SerializableMisc.toFile(lints, lintFile); - } - */ + private TargetResolver targetResolver() { + return new TargetResolver(targets); } - // private void writeBack(Result result) { - // if (result.updated != null) { - // ThrowingEx.run(() -> Files.writeString(result.target, result.updated, Charset.defaultCharset())); // TODO charset! - // } - // System.out.println("Writing back to file:" + result.target + " with content:\n" + result.updated); - // } + @Override + public SpotlessActionContext spotlessActionContext() { + TargetResolver targetResolver = targetResolver(); + TargetFileTypeInferer targetFileTypeInferer = new TargetFileTypeInferer(targetResolver); + return new SpotlessActionContext(targetFileTypeInferer.inferTargetFileType()); + } public static void main(String... args) { if (args.length == 0) { // args = new String[]{"--version"}; // args = new String[]{"license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; - args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "--delimiter-for", "java", "license-header", "--header-file", "TestHeader.txt"}; + args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "license-header", "--header-file", "TestHeader.txt"}; // args = new String[]{"--version"}; } int exitCode = new CommandLine(new SpotlessCLI()) @@ -145,19 +121,54 @@ public static void main(String... args) { } private enum SpotlessMode { - CHECK(((formatter, result) -> { - if (result.lintState.isHasLints()) { - result.lintState.asStringOneLine(result.target.toFile(), formatter); - } else { - System.out.println(String.format("%s is violating formatting rules.", result.target)); + CHECK { + @Override + void handleResult(Formatter formatter, Result result) { + if (result.lintState.isHasLints()) { + result.lintState.asStringOneLine(result.target.toFile(), formatter); + } else { + System.out.println(String.format("%s is violating formatting rules.", result.target)); + } } - })), APPLY(((formatter, result) -> ThrowingEx.run(() -> result.lintState.getDirtyState().writeCanonicalTo(result.target.toFile())))); - private final BiConsumer action; + @Override + Integer translateResultTypeToExitCode(ResultType resultType) { + if (resultType == ResultType.CLEAN) { + return 0; + } + if (resultType == ResultType.DIRTY) { + return 1; + } + if (resultType == ResultType.DID_NOT_CONVERGE) { + return -1; + } + throw new IllegalStateException("Unexpected result type: " + resultType); + } + }, + APPLY { + @Override + void handleResult(Formatter formatter, Result result) { + ThrowingEx.run(() -> result.lintState.getDirtyState().writeCanonicalTo(result.target.toFile())); + } - SpotlessMode(BiConsumer action) { - this.action = action; - } + @Override + Integer translateResultTypeToExitCode(ResultType resultType) { + if (resultType == ResultType.CLEAN) { + return 0; + } + if (resultType == ResultType.DIRTY) { + return 0; + } + if (resultType == ResultType.DID_NOT_CONVERGE) { + return -1; + } + throw new IllegalStateException("Unexpected result type: " + resultType); + } + }; + + abstract void handleResult(Formatter formatter, Result result); + + abstract Integer translateResultTypeToExitCode(ResultType resultType); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java new file mode 100644 index 0000000000..caeaa007b0 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java @@ -0,0 +1,34 @@ +/* + * 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 java.util.Objects; + +import javax.annotation.Nonnull; + +public class SpotlessActionContext { + + private final TargetFileTypeInferer.TargetFileType targetFileType; + + public SpotlessActionContext(@Nonnull TargetFileTypeInferer.TargetFileType targetFileType) { + this.targetFileType = Objects.requireNonNull(targetFileType); + } + + @Nonnull + public TargetFileTypeInferer.TargetFileType targetFileType() { + return targetFileType; + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetFileTypeInferer.java b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetFileTypeInferer.java new file mode 100644 index 0000000000..18b32986d6 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetFileTypeInferer.java @@ -0,0 +1,122 @@ +/* + * 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 java.util.Arrays; +import java.util.Objects; + +import javax.annotation.Nonnull; + +public class TargetFileTypeInferer { + + private final TargetResolver targetResolver; + + public TargetFileTypeInferer(TargetResolver targetResolver) { + this.targetResolver = Objects.requireNonNull(targetResolver); + } + + public TargetFileType inferTargetFileType() { + return targetResolver.resolveTargets() + .limit(5) // only check the first n files + .map(this::inferFileType) + .reduce(this::reduceFileType) + .orElseGet(TargetFileType::unknown); + } + + private TargetFileType reduceFileType(TargetFileType fileType1, TargetFileType fileType2) { + if (Objects.equals(fileType1, fileType2)) { + return fileType1; + } + return TargetFileType.unknown(); + } + + private TargetFileType inferFileType(@Nonnull java.nio.file.Path path) { + String fileName = path.getFileName().toString(); + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex == -1) { + return TargetFileType.unknown(); + } + String fileExtension = fileName.substring(lastDotIndex + 1); + return new TargetFileType(fileExtension); + } + + public final static class TargetFileType { + private final String fileExtension; + + private TargetFileType(String fileExtension) { + this.fileExtension = fileExtension; + } + + public String fileExtension() { + return fileExtension; + } + + public FileType fileType() { + return FileType.fromFileExtension(fileExtension); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TargetFileType that = (TargetFileType) o; + return Objects.equals(fileExtension, that.fileExtension); + } + + @Override + public int hashCode() { + return Objects.hashCode(fileExtension); + } + + static TargetFileType unknown() { + return new TargetFileType(null); + } + + static TargetFileType fromExtension(String fileExtension) { + return new TargetFileType(fileExtension); + } + } + + public enum FileType { + JAVA, CPP, ANTLR4("g4"), GROOVY, PROTOBUF("proto"), KOTLIN("kt"), UNDETERMINED(""); + + private final String fileExtensionOverride; + + FileType() { + this.fileExtensionOverride = null; + } + + FileType(String fileExtensionOverride) { + this.fileExtensionOverride = fileExtensionOverride; + } + + public String fileExtension() { + return fileExtensionOverride == null ? name().toLowerCase() : fileExtensionOverride; + } + + public static FileType fromFileExtension(String fileExtension) { + if (fileExtension == null || fileExtension.isEmpty()) { + return UNDETERMINED; + } + return Arrays.stream(values()) + .filter(fileType -> fileType.fileExtension().equalsIgnoreCase(fileExtension)) + .findFirst() + .orElse(UNDETERMINED); + } + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java index 3db8b2f0f6..71606cba57 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java @@ -22,6 +22,8 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.SpotlessAction; +import com.diffplug.spotless.cli.SpotlessActionContextProvider; +import com.diffplug.spotless.cli.core.SpotlessActionContext; import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; @@ -37,19 +39,32 @@ public int execute(CommandLine.ParseResult parseResult) throws CommandLine.Execu } private Integer runSpotlessActions(CommandLine.ParseResult parseResult) { - // 1. run setup (for combining steps handled as subcommands) - List steps = prepareFormatterSteps(parseResult); + // 1. prepare context + SpotlessActionContext context = provideSpotlessActionContext(parseResult); - // 2. run spotless steps + // 2. run setup (for combining steps handled as subcommands) + List steps = prepareFormatterSteps(parseResult, context); + + // 3. run spotless steps return executeSpotlessAction(parseResult, steps); } - private List prepareFormatterSteps(CommandLine.ParseResult parseResult) { + private SpotlessActionContext provideSpotlessActionContext(CommandLine.ParseResult parseResult) { + return parseResult.asCommandLineList().stream() + .map(CommandLine::getCommand) + .filter(command -> command instanceof SpotlessActionContextProvider) + .map(SpotlessActionContextProvider.class::cast) + .findFirst() + .map(SpotlessActionContextProvider::spotlessActionContext) + .orElseThrow(() -> new IllegalStateException("No SpotlessActionContextProvider found")); + } + + private List prepareFormatterSteps(CommandLine.ParseResult parseResult, SpotlessActionContext context) { return parseResult.asCommandLineList().stream() .map(CommandLine::getCommand) .filter(command -> command instanceof SpotlessCLIFormatterStep) .map(SpotlessCLIFormatterStep.class::cast) - .flatMap(step -> step.prepareFormatterSteps().stream()) + .flatMap(step -> step.prepareFormatterSteps(context).stream()) .collect(Collectors.toList()); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java index f451e444fb..26d75cd0dd 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessCLIFormatterStep.java @@ -21,8 +21,11 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.SpotlessCommand; +import com.diffplug.spotless.cli.core.SpotlessActionContext; public interface SpotlessCLIFormatterStep extends SpotlessCommand { + @Nonnull - List prepareFormatterSteps(); + List prepareFormatterSteps(SpotlessActionContext context); + } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java index d4b998a566..046e74e47a 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java @@ -24,6 +24,8 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.antlr4.Antlr4Defaults; +import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.cli.core.TargetFileTypeInferer; import com.diffplug.spotless.cpp.CppDefaults; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.kotlin.KotlinConstants; @@ -37,8 +39,8 @@ public class LicenseHeader extends SpotlessFormatterStepSubCommand { @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") LicenseHeaderSourceOption licenseHeaderSourceOption; - @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") - LicenseHeaderDelimiterOption licenseHeaderDelimiterOption; + @CommandLine.Option(names = {"--delimiter", "-d"}, required = false, description = "The delimiter to use for the license header. If not provided, the default delimiter for the file type will be used (if available, otherwise java is assumed).") + String delimiter; static class LicenseHeaderSourceOption { @CommandLine.Option(names = {"--header", "-H"}, required = true) @@ -47,29 +49,10 @@ static class LicenseHeaderSourceOption { File headerFile; } - static class LicenseHeaderDelimiterOption { - - @CommandLine.Option(names = {"--delimiter", "-d"}, required = true) - String delimiter; - - @CommandLine.Option(names = {"--delimiter-for", "-D"}, required = true) - DefaultDelimiterType defaultDelimiterType; - } - - enum DefaultDelimiterType { - JAVA(LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER), CPP(CppDefaults.DELIMITER_EXPR), ANTLR4(Antlr4Defaults.licenseHeaderDelimiter()), GROOVY(LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER), PROTOBUF(ProtobufConstants.LICENSE_HEADER_DELIMITER), KOTLIN(KotlinConstants.LICENSE_HEADER_DELIMITER); - - private final String delimiterExpression; - - DefaultDelimiterType(String delimiterExpression) { - this.delimiterExpression = delimiterExpression; - } - } - @Nonnull @Override - public List prepareFormatterSteps() { - FormatterStep licenseHeaderStep = LicenseHeaderStep.headerDelimiter(headerSource(), delimiter()) + public List prepareFormatterSteps(SpotlessActionContext context) { + FormatterStep licenseHeaderStep = LicenseHeaderStep.headerDelimiter(headerSource(), delimiter(context.targetFileType())) // TODO add more config options .build(); return List.of(licenseHeaderStep); @@ -83,15 +66,30 @@ private ThrowingEx.Supplier headerSource() { } } - private String delimiter() { - // TODO (simschla, 01.11.2024): here should somehow be automatically determined which type is needed (e.g. by file extension of files to be formatted) - if (licenseHeaderDelimiterOption == null) { - return DefaultDelimiterType.JAVA.delimiterExpression; - } - if (licenseHeaderDelimiterOption.delimiter != null) { - return licenseHeaderDelimiterOption.delimiter; + private String delimiter(TargetFileTypeInferer.TargetFileType inferredFileType) { + if (delimiter != null) { + return delimiter; } else { - return licenseHeaderDelimiterOption.defaultDelimiterType.delimiterExpression; + return inferredDelimiterType(inferredFileType); + } + } + + private String inferredDelimiterType(TargetFileTypeInferer.TargetFileType inferredFileType) { + switch (inferredFileType.fileType()) { + case JAVA: + // fall through + case GROOVY: + return LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; + case CPP: + return CppDefaults.DELIMITER_EXPR; + case ANTLR4: + return Antlr4Defaults.licenseHeaderDelimiter(); + case PROTOBUF: + return ProtobufConstants.LICENSE_HEADER_DELIMITER; + case KOTLIN: + return KotlinConstants.LICENSE_HEADER_DELIMITER; + default: + return LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; } } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java index b6ea2e3843..1828cf4ff7 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java @@ -15,9 +15,26 @@ */ package com.diffplug.spotless.cli.steps.generic; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.cli.core.SpotlessActionContext; import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; @CommandLine.Command(mixinStandardHelpOptions = true) -public abstract class SpotlessFormatterStepSubCommand implements SpotlessCLIFormatterStep {} +public abstract class SpotlessFormatterStepSubCommand implements SpotlessCLIFormatterStep { + + @Nonnull + @Override + public List prepareFormatterSteps(SpotlessActionContext context) { + return prepareFormatterSteps(); + } + + protected List prepareFormatterSteps() { + throw new IllegalStateException("This method must be overridden or not be called"); + } +} From 5c464e811f893588667b7a2b601207a306fd7433 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Sat, 9 Nov 2024 23:26:26 +0100 Subject: [PATCH 11/21] feat: setup testing env for cli --- cli/build.gradle | 6 +- .../diffplug/spotless/cli/SpotlessCLI.java | 46 +++-- .../spotless/cli/core/FileResolver.java | 45 +++++ .../cli/core/SpotlessActionContext.java | 14 +- .../spotless/cli/core/TargetResolver.java | 22 ++- .../cli/steps/generic/LicenseHeader.java | 6 +- .../spotless/cli/CLIIntegrationHarness.java | 49 +++++ .../cli/SpotlessCLIHelpAndVersionTest.java | 36 ++++ .../spotless/cli/SpotlessCLIRunner.java | 172 ++++++++++++++++++ .../steps/LicenseHeaderSubcommandTest.java | 79 ++++++++ 10 files changed, 454 insertions(+), 21 deletions(-) create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/FileResolver.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIHelpAndVersionTest.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderSubcommandTest.java diff --git a/cli/build.gradle b/cli/build.gradle index f7cac306a2..74d0c64724 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -1,6 +1,5 @@ plugins { id 'org.graalvm.buildtools.native' - id 'application' id 'com.gradleup.shadow' } apply from: rootProject.file('gradle/changelog.gradle') @@ -37,6 +36,11 @@ dependencies { annotationProcessor "info.picocli:picocli-codegen:${VER_PICOCLI}" } +apply from: rootProject.file('gradle/special-tests.gradle') +tasks.withType(Test).configureEach { + testLogging.showStandardStreams = true +} + compileJava { // options for picocli codegen // https://github.com/remkop/picocli/tree/main/picocli-codegen#222-other-options diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index a9dabbd486..8292e0aeca 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -15,6 +15,7 @@ */ package com.diffplug.spotless.cli; +import java.io.File; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.List; @@ -26,6 +27,7 @@ import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.LintState; import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.cli.core.FileResolver; import com.diffplug.spotless.cli.core.SpotlessActionContext; import com.diffplug.spotless.cli.core.TargetFileTypeInferer; import com.diffplug.spotless.cli.core.TargetResolver; @@ -52,6 +54,9 @@ public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessAct @CommandLine.Option(names = {"--mode", "-m"}, defaultValue = "APPLY", description = "The mode to run spotless in." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) SpotlessMode spotlessMode; + @CommandLine.Option(names = {"--basedir"}, hidden = true, description = "The base directory to run spotless in. Intended for testing purposes only.") + Path baseDir; + @CommandLine.Option(names = {"--target", "-t"}, required = true, arity = "1..*", description = "The target files to format.", scope = CommandLine.ScopeType.INHERIT) public List targets; @@ -63,7 +68,7 @@ public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessAct @Override public Integer executeSpotlessAction(@Nonnull List formatterSteps) { - TargetResolver targetResolver = new TargetResolver(targets); + TargetResolver targetResolver = targetResolver(); try (Formatter formatter = Formatter.builder() .lineEndingsPolicy(lineEnding.createPolicy()) @@ -89,20 +94,22 @@ private ResultType handleResult(Formatter formatter, Result result) { System.err.println("File did not converge: " + result.target.toFile().getName()); // TODO: where to print the output to? return ResultType.DID_NOT_CONVERGE; } - this.spotlessMode.handleResult(formatter, result); - return ResultType.DIRTY; - + return this.spotlessMode.handleResult(formatter, result); } private TargetResolver targetResolver() { - return new TargetResolver(targets); + return new TargetResolver(baseDir == null ? Path.of(File.separator) : baseDir, targets); + } + + private Path baseDir() { + return baseDir == null ? Path.of(File.separator) : baseDir; } @Override public SpotlessActionContext spotlessActionContext() { TargetResolver targetResolver = targetResolver(); TargetFileTypeInferer targetFileTypeInferer = new TargetFileTypeInferer(targetResolver); - return new SpotlessActionContext(targetFileTypeInferer.inferTargetFileType()); + return new SpotlessActionContext(targetFileTypeInferer.inferTargetFileType(), new FileResolver(baseDir())); } public static void main(String... args) { @@ -113,22 +120,31 @@ public static void main(String... args) { args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "license-header", "--header-file", "TestHeader.txt"}; // args = new String[]{"--version"}; } - int exitCode = new CommandLine(new SpotlessCLI()) - .setExecutionStrategy(new SpotlessExecutionStrategy()) - .setCaseInsensitiveEnumValuesAllowed(true) + int exitCode = createCommandLine(createInstance()) .execute(args); System.exit(exitCode); } + static SpotlessCLI createInstance() { + return new SpotlessCLI(); + } + + static CommandLine createCommandLine(SpotlessCLI spotlessCLI) { + return new CommandLine(spotlessCLI) + .setExecutionStrategy(new SpotlessExecutionStrategy()) + .setCaseInsensitiveEnumValuesAllowed(true); + } + private enum SpotlessMode { CHECK { @Override - void handleResult(Formatter formatter, Result result) { + ResultType handleResult(Formatter formatter, Result result) { if (result.lintState.isHasLints()) { result.lintState.asStringOneLine(result.target.toFile(), formatter); } else { System.out.println(String.format("%s is violating formatting rules.", result.target)); } + return ResultType.DIRTY; } @Override @@ -147,8 +163,14 @@ Integer translateResultTypeToExitCode(ResultType resultType) { }, APPLY { @Override - void handleResult(Formatter formatter, Result result) { + ResultType handleResult(Formatter formatter, Result result) { + if (result.lintState.isHasLints()) { + // something went wrong, we should not apply the changes + System.err.println("File has lints: " + result.target.toFile().getName()); + return ResultType.DIRTY; + } ThrowingEx.run(() -> result.lintState.getDirtyState().writeCanonicalTo(result.target.toFile())); + return ResultType.CLEAN; } @Override @@ -166,7 +188,7 @@ Integer translateResultTypeToExitCode(ResultType resultType) { } }; - abstract void handleResult(Formatter formatter, Result result); + abstract ResultType handleResult(Formatter formatter, Result result); abstract Integer translateResultTypeToExitCode(ResultType resultType); diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/FileResolver.java b/cli/src/main/java/com/diffplug/spotless/cli/core/FileResolver.java new file mode 100644 index 0000000000..6f4c453a89 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/FileResolver.java @@ -0,0 +1,45 @@ +/* + * 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 java.io.File; +import java.nio.file.Path; +import java.util.Objects; + +import javax.annotation.Nonnull; + +public class FileResolver { + private final Path baseDir; + + public FileResolver(@Nonnull Path baseDir) { + this.baseDir = Objects.requireNonNull(baseDir); + } + + Path baseDir() { + return baseDir; + } + + public File resolveFile(File file) { + return resolvePath(file.toPath()).toFile(); + } + + public Path resolvePath(Path path) { + if (path.isAbsolute()) { + return path; + } + return baseDir.resolve(path); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java index caeaa007b0..8c719b1685 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java @@ -15,6 +15,8 @@ */ package com.diffplug.spotless.cli.core; +import java.io.File; +import java.nio.file.Path; import java.util.Objects; import javax.annotation.Nonnull; @@ -22,13 +24,23 @@ public class SpotlessActionContext { private final TargetFileTypeInferer.TargetFileType targetFileType; + private final FileResolver fileResolver; - public SpotlessActionContext(@Nonnull TargetFileTypeInferer.TargetFileType targetFileType) { + public SpotlessActionContext(@Nonnull TargetFileTypeInferer.TargetFileType targetFileType, @Nonnull FileResolver fileResolver) { this.targetFileType = Objects.requireNonNull(targetFileType); + this.fileResolver = fileResolver; } @Nonnull public TargetFileTypeInferer.TargetFileType targetFileType() { return targetFileType; } + + public File resolveFile(File file) { + return fileResolver.resolveFile(file); + } + + public Path resolvePath(Path path) { + return fileResolver.resolvePath(path); + } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java index 0f8ca965e1..2b02689b36 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java @@ -29,17 +29,23 @@ import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nonnull; + import com.diffplug.spotless.ThrowingEx; public class TargetResolver { private final List targets; - public TargetResolver(List targets) { - this.targets = targets; + private final FileResolver fileResolver; + + public TargetResolver(@Nonnull Path baseDir, @Nonnull List targets) { + this.fileResolver = new FileResolver(baseDir); + this.targets = Objects.requireNonNull(targets); } public Stream resolveTargets() { @@ -54,7 +60,15 @@ private Stream resolveTarget(String target) { if (isGlob) { return resolveGlob(target); } - return resolveDir(Path.of(target)); + Path targetPath = fileResolver.resolvePath(Path.of(target)); + if (Files.isReadable(targetPath)) { + return Stream.of(targetPath); + } + if (Files.isDirectory(targetPath)) { + return resolveDir(targetPath); + } + // TODO log warn? + return Stream.empty(); } private Stream resolveDir(Path startDir) { @@ -81,7 +95,7 @@ private Stream resolveGlob(String glob) { .takeWhile(not(TargetResolver::isGlobPathPart)) .collect(Collectors.toList()); - startDir = Path.of(glob.startsWith(File.separator) ? File.separator : "", startDirParts.toArray(String[]::new)); + startDir = Path.of(glob.startsWith(File.separator) ? File.separator : fileResolver.baseDir().toString(), startDirParts.toArray(String[]::new)); globPart = Stream.of(parts) .skip(startDirParts.size()) .collect(Collectors.joining(File.separator)); diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java index 046e74e47a..0c802dd1ef 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java @@ -52,17 +52,17 @@ static class LicenseHeaderSourceOption { @Nonnull @Override public List prepareFormatterSteps(SpotlessActionContext context) { - FormatterStep licenseHeaderStep = LicenseHeaderStep.headerDelimiter(headerSource(), delimiter(context.targetFileType())) + FormatterStep licenseHeaderStep = LicenseHeaderStep.headerDelimiter(headerSource(context), delimiter(context.targetFileType())) // TODO add more config options .build(); return List.of(licenseHeaderStep); } - private ThrowingEx.Supplier headerSource() { + private ThrowingEx.Supplier headerSource(SpotlessActionContext context) { if (licenseHeaderSourceOption.header != null) { return () -> licenseHeaderSourceOption.header; } else { - return () -> ThrowingEx.get(() -> Files.readString(licenseHeaderSourceOption.headerFile.toPath())); + return () -> ThrowingEx.get(() -> Files.readString(context.resolveFile(licenseHeaderSourceOption.headerFile).toPath())); } } 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..ef022d6bbf --- /dev/null +++ b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java @@ -0,0 +1,49 @@ +/* + * 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; + +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 SpotlessCLIRunner.create() + .withWorkingDir(rootFolder()); + } +} 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..0be1f19473 --- /dev/null +++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java @@ -0,0 +1,172 @@ +/* + * 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.io.PrintWriter; +import java.io.StringWriter; +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 class SpotlessCLIRunner { + + private File workingDir = new File("."); + + private final List args = new ArrayList<>(); + + public static SpotlessCLIRunner create() { + return new SpotlessCLIRunner(); + } + + public SpotlessCLIRunner withWorkingDir(@NotNull File workingDir) { + this.workingDir = Objects.requireNonNull(workingDir); + return withOption("--basedir", workingDir.getAbsolutePath()); + } + + 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 stepClass) { + String stepName = determineStepName(stepClass); + return withStep(stepName); + } + + private String determineStepName(Class 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(); + 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(); + 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()); + } + + private Result executeCommand() { + 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(args.toArray(new String[0])); + } catch (Exception e) { + executionException = e; + } + + // finalize + outWriter.flush(); + errWriter.flush(); + return new Result(exitCode, executionException, out.toString(), err.toString()); + } + } + + public static class Result { + + private final Integer exitCode; + private final String stdOut; + private final String stdErr; + private final Exception executionException; + + private 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/steps/LicenseHeaderSubcommandTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderSubcommandTest.java new file mode 100644 index 0000000000..8aaaa45f60 --- /dev/null +++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderSubcommandTest.java @@ -0,0 +1,79 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import com.diffplug.spotless.cli.CLIIntegrationHarness; +import com.diffplug.spotless.cli.SpotlessCLIRunner; +import com.diffplug.spotless.cli.steps.generic.LicenseHeader; + +public class LicenseHeaderSubcommandTest 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 {}"); + } +} From 3b3380460769906b44bb4851bc3ded81a411254c Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Sun, 10 Nov 2024 20:59:13 +0100 Subject: [PATCH 12/21] feat: add support to run tests externally either via launching separate jvm or launching native image --- cli/build.gradle | 11 ++++ .../diffplug/spotless/cli/SpotlessCLI.java | 5 +- .../spotless/cli/CLIIntegrationHarness.java | 16 ++++- .../spotless/cli/SpotlessCLIRunner.java | 52 ++++++--------- ...potlessCLIRunnerInExternalJavaProcess.java | 65 +++++++++++++++++++ ...tlessCLIRunnerInNativeExternalProcess.java | 62 ++++++++++++++++++ .../cli/SpotlessCLIRunnerInSameThread.java | 60 +++++++++++++++++ .../steps/LicenseHeaderJavaProcessTest.java | 21 ++++++ .../steps/LicenseHeaderNativeProcessTest.java | 21 ++++++ ...ommandTest.java => LicenseHeaderTest.java} | 2 +- gradle/special-tests.gradle | 4 +- .../diffplug/spotless/tag/CliNativeTest.java | 30 +++++++++ .../diffplug/spotless/tag/CliProcessTest.java | 30 +++++++++ 13 files changed, 340 insertions(+), 39 deletions(-) create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInExternalJavaProcess.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInNativeExternalProcess.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunnerInSameThread.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderJavaProcessTest.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderNativeProcessTest.java rename cli/src/test/java/com/diffplug/spotless/cli/steps/{LicenseHeaderSubcommandTest.java => LicenseHeaderTest.java} (97%) create mode 100644 testlib/src/main/java/com/diffplug/spotless/tag/CliNativeTest.java create mode 100644 testlib/src/main/java/com/diffplug/spotless/tag/CliProcessTest.java diff --git a/cli/build.gradle b/cli/build.gradle index 74d0c64724..92ef19d8c2 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -96,3 +96,14 @@ graalvmNative { } } } + +tasks.withType(Test).configureEach { + if (it.name == 'testCliProcess') { + it.dependsOn('shadowJar') + it.systemProperty 'spotless.cli.shadowJar', tasks.shadowJar.archiveFile.get().asFile + } + if (it.name == 'testCliNative') { + it.dependsOn('nativeCompile') + it.systemProperty 'spotless.cli.nativeImage', tasks.nativeCompile.outputFile.get().asFile + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 8292e0aeca..156eb73af5 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -15,7 +15,6 @@ */ package com.diffplug.spotless.cli; -import java.io.File; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.List; @@ -98,11 +97,11 @@ private ResultType handleResult(Formatter formatter, Result result) { } private TargetResolver targetResolver() { - return new TargetResolver(baseDir == null ? Path.of(File.separator) : baseDir, targets); + return new TargetResolver(baseDir(), targets); } private Path baseDir() { - return baseDir == null ? Path.of(File.separator) : baseDir; + return baseDir == null ? Path.of(System.getProperty("user.dir")) : baseDir; } @Override diff --git a/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java index ef022d6bbf..7e652a8a81 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.BeforeEach; import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.tag.CliNativeTest; +import com.diffplug.spotless.tag.CliProcessTest; public abstract class CLIIntegrationHarness extends ResourceHarness { @@ -43,7 +45,19 @@ void gitAttributes() throws IOException { } protected SpotlessCLIRunner cliRunner() { - return SpotlessCLIRunner.create() + return createRunnerForTag() .withWorkingDir(rootFolder()); } + + private SpotlessCLIRunner createRunnerForTag() { + CliProcessTest cliProcessTest = getClass().getAnnotation(CliProcessTest.class); + if (cliProcessTest != null) { + return SpotlessCLIRunner.createExternalProcess(); + } + CliNativeTest cliNativeTest = getClass().getAnnotation(CliNativeTest.class); + if (cliNativeTest != null) { + return SpotlessCLIRunner.createNative(); + } + return SpotlessCLIRunner.create(); + } } diff --git a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java index 0be1f19473..1a57901271 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/SpotlessCLIRunner.java @@ -16,8 +16,6 @@ package com.diffplug.spotless.cli; import java.io.File; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -29,19 +27,31 @@ import picocli.CommandLine; -public class SpotlessCLIRunner { +public abstract class SpotlessCLIRunner { private File workingDir = new File("."); private final List args = new ArrayList<>(); public static SpotlessCLIRunner create() { - return new SpotlessCLIRunner(); + 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 withOption("--basedir", workingDir.getAbsolutePath()); + return this; + } + + protected File workingDir() { + return workingDir; } public SpotlessCLIRunner withOption(@NotNull String option) { @@ -80,7 +90,7 @@ private String determineStepName(Class stepC } public Result run() { - Result result = executeCommand(); + Result result = executeCommand(args); if (result.executionException() != null) { throwRuntimeException("Error while executing Spotless CLI command", result); } @@ -91,7 +101,7 @@ public Result run() { } public Result runAndFail() { - Result result = executeCommand(); + Result result = executeCommand(args); if (result.executionException() != null) { throwRuntimeException("Error while executing Spotless CLI command", result); } @@ -113,31 +123,7 @@ private void throwRuntimeException(String message, Result result) { throw new RuntimeException(sb.toString()); } - private Result executeCommand() { - 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(args.toArray(new String[0])); - } catch (Exception e) { - executionException = e; - } - - // finalize - outWriter.flush(); - errWriter.flush(); - return new Result(exitCode, executionException, out.toString(), err.toString()); - } - } + protected abstract Result executeCommand(List args); public static class Result { @@ -146,7 +132,7 @@ public static class Result { private final String stdErr; private final Exception executionException; - private Result(@Nullable Integer exitCode, @Nullable Exception executionException, @NotNull String stdOut, @NotNull String stdErr) { + 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); 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/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/LicenseHeaderSubcommandTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java similarity index 97% rename from cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderSubcommandTest.java rename to cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java index 8aaaa45f60..abeed73f28 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderSubcommandTest.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java @@ -23,7 +23,7 @@ import com.diffplug.spotless.cli.SpotlessCLIRunner; import com.diffplug.spotless.cli.steps.generic.LicenseHeader; -public class LicenseHeaderSubcommandTest extends CLIIntegrationHarness { +public class LicenseHeaderTest extends CLIIntegrationHarness { @Test void assertHeaderMustBeSpecified() { diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle index 650c04b84b..4d4da7424c 100644 --- a/gradle/special-tests.gradle +++ b/gradle/special-tests.gradle @@ -7,7 +7,9 @@ def special = [ 'clang', 'gofmt', 'npm', - 'shfmt' + 'shfmt', + 'cliProcess', + 'cliNative' ] boolean isCiServer = System.getenv().containsKey("CI") 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/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 {} From 7a8f7cfe448c107d6dad99b3393cfef8ed440219 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Tue, 12 Nov 2024 19:39:30 +0100 Subject: [PATCH 13/21] feat: poc for invoking GJF in native cli (still very hacky but working) --- cli/build.gradle | 38 +++++++++++++ .../diffplug/spotless/cli/SpotlessCLI.java | 15 +++-- .../spotless/cli/core/CliJarProvisioner.java | 55 ++++++++++++++++++ .../cli/core/SpotlessActionContext.java | 6 ++ .../spotless/cli/core/TargetResolver.java | 15 +++-- .../spotless/cli/steps/GoogleJavaFormat.java | 57 +++++++++++++++++++ .../steps/{generic => }/LicenseHeader.java | 6 +- ...aterSubCommand.java => RemoveMeLater.java} | 4 +- ...ommand.java => SpotlessFormatterStep.java} | 5 +- .../GoogleJavaFormatJavaProcessTest.java | 21 +++++++ .../GoogleJavaFormatNativeProcessTest.java | 24 ++++++++ .../cli/steps/GoogleJavaFormatTest.java | 38 +++++++++++++ .../spotless/cli/steps/LicenseHeaderTest.java | 1 - .../java/com/diffplug/spotless/JarState.java | 13 +++++ 14 files changed, 280 insertions(+), 18 deletions(-) create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/CliJarProvisioner.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java rename cli/src/main/java/com/diffplug/spotless/cli/steps/{generic => }/LicenseHeader.java (96%) rename cli/src/main/java/com/diffplug/spotless/cli/steps/{generic/RemoveMeLaterSubCommand.java => RemoveMeLater.java} (90%) rename cli/src/main/java/com/diffplug/spotless/cli/steps/{generic/SpotlessFormatterStepSubCommand.java => SpotlessFormatterStep.java} (84%) create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatJavaProcessTest.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatNativeProcessTest.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/GoogleJavaFormatTest.java diff --git a/cli/build.gradle b/cli/build.gradle index 92ef19d8c2..f658717eac 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -36,6 +36,14 @@ dependencies { annotationProcessor "info.picocli:picocli-codegen:${VER_PICOCLI}" } +dependencies { + [ + 'com.google.googlejavaformat:google-java-format:1.24.0' + ].each { + implementation it + } +} + apply from: rootProject.file('gradle/special-tests.gradle') tasks.withType(Test).configureEach { testLogging.showStandardStreams = true @@ -88,15 +96,45 @@ application { // use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image graalvmNative { + agent { + enabled = true + defaultMode = "standard" + metadataCopy { + inputTaskNames.add('test') + mergeWithExisting = true + } + } binaries { main { imageName = 'spotless' mainClass = 'com.diffplug.spotless.cli.SpotlessCLI' sharedLibrary = false + useFatJar = true // use shadowJar as input to have same classpath + + // optimizations, see https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/ + //buildArgs.add('-O3') // on production builds + + // the following options are required for GJF + // see: + buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED') + buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED') + buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED') + buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED') + buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED') + + buildArgs.add('--initialize-at-build-time=com.sun.tools.javac.file.Locations') + + buildArgs.add('-H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler') + buildArgs.add('-H:IncludeResourceBundles=com.sun.tools.javac.resources.javac') } } } +tasks.named('nativeCompile') { + dependsOn('shadowJar') + classpathJar = tasks.shadowJar.archiveFile.get().asFile +} + tasks.withType(Test).configureEach { if (it.name == 'testCliProcess') { it.dependsOn('shadowJar') diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 156eb73af5..7f35e25319 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -32,8 +32,9 @@ import com.diffplug.spotless.cli.core.TargetResolver; import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; import com.diffplug.spotless.cli.help.OptionConstants; -import com.diffplug.spotless.cli.steps.generic.LicenseHeader; -import com.diffplug.spotless.cli.steps.generic.RemoveMeLaterSubCommand; +import com.diffplug.spotless.cli.steps.GoogleJavaFormat; +import com.diffplug.spotless.cli.steps.LicenseHeader; +import com.diffplug.spotless.cli.steps.RemoveMeLater; import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; import picocli.CommandLine; @@ -41,7 +42,8 @@ @Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", subcommandsRepeatable = true, subcommands = { LicenseHeader.class, - RemoveMeLaterSubCommand.class}) + RemoveMeLater.class, + GoogleJavaFormat.class}) public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider { @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit.") @@ -76,7 +78,8 @@ public Integer executeSpotlessAction(@Nonnull List formatterSteps .build()) { ResultType resultType = targetResolver.resolveTargets() - .parallel() // needed? + .parallel() + .peek(path -> System.out.printf("%s: formatting %s%n", Thread.currentThread().getName(), path)) .map(path -> ThrowingEx.get(() -> new Result(path, LintState.of(formatter, path.toFile())))) // TODO handle suppressions, see SpotlessTaskImpl .map(result -> this.handleResult(formatter, result)) .reduce(ResultType.CLEAN, ResultType::combineWith); @@ -116,7 +119,8 @@ public static void main(String... args) { // args = new String[]{"--version"}; // args = new String[]{"license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java", "license-header", "--header", "abc"}; - args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "license-header", "--header-file", "TestHeader.txt"}; + // args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "license-header", "--header", "abc", "license-header", "--header-file", "TestHeader.txt"}; + args = new String[]{"--basedir", "cli", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "google-java-format"}; // args = new String[]{"--version"}; } int exitCode = createCommandLine(createInstance()) @@ -166,6 +170,7 @@ ResultType handleResult(Formatter formatter, Result result) { if (result.lintState.isHasLints()) { // something went wrong, we should not apply the changes System.err.println("File has lints: " + result.target.toFile().getName()); + System.err.println("lint:\n" + result.lintState.asStringDetailed(result.target.toFile(), formatter)); return ResultType.DIRTY; } ThrowingEx.run(() -> result.lintState.getDirtyState().writeCanonicalTo(result.target.toFile())); diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/CliJarProvisioner.java b/cli/src/main/java/com/diffplug/spotless/cli/core/CliJarProvisioner.java new file mode 100644 index 0000000000..413f586369 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/CliJarProvisioner.java @@ -0,0 +1,55 @@ +/* + * 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 java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class CliJarProvisioner implements Provisioner { + + public static final CliJarProvisioner INSTANCE = new CliJarProvisioner(); + + public static final File OWN_JAR = createSentinelFile(); + + public CliJarProvisioner() { + JarState.setOverrideClassLoader(getClass().getClassLoader()); // use the classloader of this class + // TODO (simschla, 11.11.2024): THIS IS A HACK, replace with proper solution + } + + private static File createSentinelFile() { + try { + File file = File.createTempFile("spotless-cli", ".jar"); + Files.write(file.toPath(), List.of("@@@@PLACEHOLDER_FOR_OWN_JAR@@@@"), StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING); + file.deleteOnExit(); + return file; + } catch (Exception e) { + throw new RuntimeException("Could not create sentinel file", e); + } + } + + @Override + public Set provisionWithTransitives(boolean withTransitives, Collection mavenCoordinates) { + return Set.of(OWN_JAR); + } + +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java index 8c719b1685..8e584dc175 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java @@ -21,6 +21,8 @@ import javax.annotation.Nonnull; +import com.diffplug.spotless.Provisioner; + public class SpotlessActionContext { private final TargetFileTypeInferer.TargetFileType targetFileType; @@ -43,4 +45,8 @@ public File resolveFile(File file) { public Path resolvePath(Path path) { return fileResolver.resolvePath(path); } + + public Provisioner provisioner() { + return CliJarProvisioner.INSTANCE; + } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java index 2b02689b36..4977b77d8f 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/TargetResolver.java @@ -49,13 +49,16 @@ public TargetResolver(@Nonnull Path baseDir, @Nonnull List targets) { } public Stream resolveTargets() { - return targets.stream() - .flatMap(this::resolveTarget); + return targets.parallelStream() + .map(this::resolveTarget) + .reduce(Stream::concat) // beware! when using flatmap, the stream goes to sequential + .orElse(Stream.empty()); } private Stream resolveTarget(String target) { final boolean isGlob = target.contains("*") || target.contains("?"); + System.out.println("isGlob: " + isGlob + " target: " + target); if (isGlob) { return resolveGlob(target); @@ -83,7 +86,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { return FileVisitResult.CONTINUE; } })); - return collected.stream(); + return collected.parallelStream(); } private Stream resolveGlob(String glob) { @@ -108,13 +111,15 @@ private Stream resolveGlob(String glob) { new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (matcher.matches(file)) { + Path relativeFile = startDir.relativize(file); + if (matcher.matches(relativeFile)) { + System.out.println("Matched: " + file); collected.add(file); } return FileVisitResult.CONTINUE; } })); - return collected.stream() + return collected.parallelStream() .map(Path::normalize); // .map(Path::toAbsolutePath); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java new file mode 100644 index 0000000000..f3ac4ebe22 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java @@ -0,0 +1,57 @@ +/* + * 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.util.List; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.cli.help.OptionConstants; +import com.diffplug.spotless.java.GoogleJavaFormatStep; + +import picocli.CommandLine; + +@CommandLine.Command(name = "google-java-format", description = "Runs google java format") +public class GoogleJavaFormat extends SpotlessFormatterStep { + + @CommandLine.Option(names = {"--style", "-s"}, required = false, defaultValue = "GOOGLE", description = "The style to use for the google java format." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX) + Style style; + + @CommandLine.Option(names = {"--reflow-long-strings", "-r"}, required = false, defaultValue = "false", description = "Reflow long strings." + OptionConstants.DEFAULT_VALUE_SUFFIX) + boolean reflowLongStrings; + + @CommandLine.Option(names = {"--reorder-imports", "-i"}, required = false, defaultValue = "false", description = "Reorder imports." + OptionConstants.DEFAULT_VALUE_SUFFIX) + boolean reorderImports; + + @CommandLine.Option(names = {"--format-javadoc", "-j"}, required = false, defaultValue = "true", description = "Format javadoc." + OptionConstants.DEFAULT_VALUE_SUFFIX) + boolean formatJavadoc; + + @Override + public List prepareFormatterSteps(SpotlessActionContext context) { + return List.of(GoogleJavaFormatStep.create( + GoogleJavaFormatStep.defaultGroupArtifact(), + GoogleJavaFormatStep.defaultVersion(), + style.name(), + context.provisioner(), + reflowLongStrings, + reorderImports, + formatJavadoc)); + } + + public enum Style { + AOSP, GOOGLE + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java similarity index 96% rename from cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java index 0c802dd1ef..08c648e22d 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.steps.generic; +package com.diffplug.spotless.cli.steps; import java.io.File; import java.nio.file.Files; @@ -34,7 +34,7 @@ import picocli.CommandLine; @CommandLine.Command(name = "license-header", description = "Runs license header") -public class LicenseHeader extends SpotlessFormatterStepSubCommand { +public class LicenseHeader extends SpotlessFormatterStep { @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") LicenseHeaderSourceOption licenseHeaderSourceOption; @@ -49,6 +49,8 @@ static class LicenseHeaderSourceOption { File headerFile; } + // TODO add more config options + @Nonnull @Override public List prepareFormatterSteps(SpotlessActionContext context) { diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/RemoveMeLaterSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java similarity index 90% rename from cli/src/main/java/com/diffplug/spotless/cli/steps/generic/RemoveMeLaterSubCommand.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java index 3b3096c4f7..51e332be15 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/RemoveMeLaterSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.steps.generic; +package com.diffplug.spotless.cli.steps; import java.io.File; import java.util.List; @@ -25,7 +25,7 @@ import picocli.CommandLine; @CommandLine.Command(name = "ignoreme") -public class RemoveMeLaterSubCommand extends SpotlessFormatterStepSubCommand { +public class RemoveMeLater extends SpotlessFormatterStep { @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") LicenseHeaderOption licenseHeaderOption; diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessFormatterStep.java similarity index 84% rename from cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java rename to cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessFormatterStep.java index 1828cf4ff7..fdd40fedf4 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/generic/SpotlessFormatterStepSubCommand.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/SpotlessFormatterStep.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.cli.steps.generic; +package com.diffplug.spotless.cli.steps; import java.util.List; @@ -21,12 +21,11 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.core.SpotlessActionContext; -import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; import picocli.CommandLine; @CommandLine.Command(mixinStandardHelpOptions = true) -public abstract class SpotlessFormatterStepSubCommand implements SpotlessCLIFormatterStep { +public abstract class SpotlessFormatterStep implements SpotlessCLIFormatterStep { @Nonnull @Override 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/LicenseHeaderTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java index abeed73f28..4362ffa235 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java @@ -21,7 +21,6 @@ import com.diffplug.spotless.cli.CLIIntegrationHarness; import com.diffplug.spotless.cli.SpotlessCLIRunner; -import com.diffplug.spotless.cli.steps.generic.LicenseHeader; public class LicenseHeaderTest extends CLIIntegrationHarness { 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); } } From ecbc88634cd15af6315a156d963e6c2c11160bde Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Tue, 12 Nov 2024 20:38:19 +0100 Subject: [PATCH 14/21] feat: auto-generate reflection info for nativeCompile --- cli/build.gradle | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/cli/build.gradle b/cli/build.gradle index f658717eac..682f7fd66b 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -94,6 +94,9 @@ application { archivesBaseName = 'spotless-cli' } + +def nativeCompileMetaDir = project.layout.buildDirectory.dir('nativeCompile/src/main/resources/native-image/' + project.group + '/' + project.name) + // use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image graalvmNative { agent { @@ -101,8 +104,16 @@ graalvmNative { defaultMode = "standard" metadataCopy { inputTaskNames.add('test') - mergeWithExisting = true + mergeWithExisting = false + outputDirectories.add(nativeCompileMetaDir.get().asFile.path) } + tasksToInstrumentPredicate = new java.util.function.Predicate() { + @Override + boolean test(Task task) { + println ("Instrumenting task: " + task.name + " " + task.name == 'test') + return task.name == 'test' + } + } } binaries { main { @@ -130,12 +141,38 @@ graalvmNative { } } + +tasks.named('metadataCopy') { + dependsOn('test') +} + tasks.named('nativeCompile') { dependsOn('shadowJar') classpathJar = tasks.shadowJar.archiveFile.get().asFile } + +tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + dependsOn('metadataCopy') // produces graalvm agent info + from(nativeCompileMetaDir.get().asFile.path) { + into('META-INF/native-image/' + project.group + '/' + project.name) + } +} + +gradle.taskGraph.whenReady { graph -> + if (graph.hasTask('nativeCompile') || graph.hasTask('metadataCopy') || graph.hasTask('shadowJar')) { + // enable graalvm agent using property here instead of command line `-Pagent=standard` + // this collects information about reflective access and resources used by the application (e.g. GJF) + project.property('agent', 'standard') + } +} + tasks.withType(Test).configureEach { + if (it.name == 'test') { + if (project.hasProperty('agent')) { + it.inputs.property('agent', project.property('agent')) // make sure to re-run tests if agent changes + } + } if (it.name == 'testCliProcess') { it.dependsOn('shadowJar') it.systemProperty 'spotless.cli.shadowJar', tasks.shadowJar.archiveFile.get().asFile From 8eb720d3351c6336887fa7757d0e4ffea630b894 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Wed, 13 Nov 2024 06:20:36 +0100 Subject: [PATCH 15/21] chore: cleanup help options (already included with mixin) --- .../main/java/com/diffplug/spotless/cli/SpotlessCLI.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 7f35e25319..972d368fca 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -46,12 +46,6 @@ GoogleJavaFormat.class}) public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider { - @CommandLine.Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit.") - boolean versionRequested; - - @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display this help message and exit.") - boolean usageHelpRequested; - @CommandLine.Option(names = {"--mode", "-m"}, defaultValue = "APPLY", description = "The mode to run spotless in." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) SpotlessMode spotlessMode; From ea84f733546099d62c403154155328b62dc80b83 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Wed, 13 Nov 2024 07:32:35 +0100 Subject: [PATCH 16/21] chore: cleanup and optimize usage help --- .../diffplug/spotless/cli/SpotlessCLI.java | 23 +++++++--- .../spotless/cli/help/OptionConstants.java | 8 +++- .../spotless/cli/steps/GoogleJavaFormat.java | 2 +- .../spotless/cli/steps/LicenseHeader.java | 4 +- .../spotless/cli/steps/RemoveMeLater.java | 45 ------------------- 5 files changed, 25 insertions(+), 57 deletions(-) delete mode 100644 cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 972d368fca..7df5a16c85 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -34,35 +34,37 @@ import com.diffplug.spotless.cli.help.OptionConstants; import com.diffplug.spotless.cli.steps.GoogleJavaFormat; import com.diffplug.spotless.cli.steps.LicenseHeader; -import com.diffplug.spotless.cli.steps.RemoveMeLater; import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; import picocli.CommandLine; import picocli.CommandLine.Command; -@Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", subcommandsRepeatable = true, subcommands = { +@Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", synopsisSubcommandLabel = "[FORMATTING_STEPS]", commandListHeading = "%nAvailable formatting steps:%n", subcommandsRepeatable = true, subcommands = { LicenseHeader.class, - RemoveMeLater.class, GoogleJavaFormat.class}) public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider { - @CommandLine.Option(names = {"--mode", "-m"}, defaultValue = "APPLY", description = "The mode to run spotless in." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Spec + CommandLine.Model.CommandSpec spec; // injected by picocli + + @CommandLine.Option(names = {"--mode", "-m"}, defaultValue = "APPLY", description = "The mode to run spotless in." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) SpotlessMode spotlessMode; @CommandLine.Option(names = {"--basedir"}, hidden = true, description = "The base directory to run spotless in. Intended for testing purposes only.") Path baseDir; - @CommandLine.Option(names = {"--target", "-t"}, required = true, arity = "1..*", description = "The target files to format.", scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = {"--target", "-t"}, description = "The target files to format.") public List targets; - @CommandLine.Option(names = {"--encoding", "-e"}, defaultValue = "ISO8859-1", description = "The encoding of the files to format." + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = {"--encoding", "-e"}, defaultValue = "UTF-8", description = "The encoding of the files to format." + OptionConstants.DEFAULT_VALUE_SUFFIX) public Charset encoding; - @CommandLine.Option(names = {"--line-ending", "-l"}, defaultValue = "UNIX", description = "The line ending of the files to format." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX, scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = {"--line-ending", "-l"}, defaultValue = "UNIX", description = "The line ending of the files to format." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) public LineEnding lineEnding; @Override public Integer executeSpotlessAction(@Nonnull List formatterSteps) { + validateTargets(); TargetResolver targetResolver = targetResolver(); try (Formatter formatter = Formatter.builder() @@ -81,6 +83,12 @@ public Integer executeSpotlessAction(@Nonnull List formatterSteps } } + private void validateTargets() { + if (targets == null || targets.isEmpty()) { // cannot use `required = true` because of the subcommands + throw new CommandLine.ParameterException(spec.commandLine(), "Error: Missing required argument (specify one of these): (--target= | -t)"); + } + } + private ResultType handleResult(Formatter formatter, Result result) { if (result.lintState.isClean()) { // System.out.println("File is clean: " + result.target.toFile().getName()); @@ -103,6 +111,7 @@ private Path baseDir() { @Override public SpotlessActionContext spotlessActionContext() { + validateTargets(); TargetResolver targetResolver = targetResolver(); TargetFileTypeInferer targetFileTypeInferer = new TargetFileTypeInferer(targetResolver); return new SpotlessActionContext(targetFileTypeInferer.inferTargetFileType(), new FileResolver(baseDir())); diff --git a/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java b/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java index e3aace8028..0070917469 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/help/OptionConstants.java @@ -17,9 +17,13 @@ public final class OptionConstants { - public static final String VALID_VALUES_SUFFIX = " One of: ${COMPLETION-CANDIDATES}"; + public static final String NEW_LINE = "%n "; - public static final String DEFAULT_VALUE_SUFFIX = " (default: ${DEFAULT-VALUE})"; + public static final String VALID_VALUES_SUFFIX = NEW_LINE + "One of: ${COMPLETION-CANDIDATES}"; + + public static final String DEFAULT_VALUE_SUFFIX = NEW_LINE + "(default: ${DEFAULT-VALUE})"; + + public static final String VALID_AND_DEFAULT_VALUES_SUFFIX = VALID_VALUES_SUFFIX + DEFAULT_VALUE_SUFFIX; private OptionConstants() { // no instance diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java index f3ac4ebe22..63ee56773f 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/GoogleJavaFormat.java @@ -27,7 +27,7 @@ @CommandLine.Command(name = "google-java-format", description = "Runs google java format") public class GoogleJavaFormat extends SpotlessFormatterStep { - @CommandLine.Option(names = {"--style", "-s"}, required = false, defaultValue = "GOOGLE", description = "The style to use for the google java format." + OptionConstants.VALID_VALUES_SUFFIX + OptionConstants.DEFAULT_VALUE_SUFFIX) + @CommandLine.Option(names = {"--style", "-s"}, required = false, defaultValue = "GOOGLE", description = "The style to use for the google java format." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) Style style; @CommandLine.Option(names = {"--reflow-long-strings", "-r"}, required = false, defaultValue = "false", description = "Reflow long strings." + OptionConstants.DEFAULT_VALUE_SUFFIX) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java index 08c648e22d..110f26b41b 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java @@ -43,9 +43,9 @@ public class LicenseHeader extends SpotlessFormatterStep { String delimiter; static class LicenseHeaderSourceOption { - @CommandLine.Option(names = {"--header", "-H"}, required = true) + @CommandLine.Option(names = {"--header", "-H"}, required = true, description = "The license header content to apply. May contain @|YELLOW $YEAR|@ as placeholder.") String header; - @CommandLine.Option(names = {"--header-file", "-f"}, required = true) + @CommandLine.Option(names = {"--header-file", "-f"}, required = true, description = "The license header content in a file to apply.%n May contain @|YELLOW $YEAR|@ as placeholder.") File headerFile; } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java deleted file mode 100644 index 51e332be15..0000000000 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/RemoveMeLater.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.File; -import java.util.List; - -import javax.annotation.Nonnull; - -import com.diffplug.spotless.FormatterStep; - -import picocli.CommandLine; - -@CommandLine.Command(name = "ignoreme") -public class RemoveMeLater extends SpotlessFormatterStep { - - @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") - LicenseHeaderOption licenseHeaderOption; - - static class LicenseHeaderOption { - @CommandLine.Option(names = {"--header", "-H"}, required = true) - String header; - @CommandLine.Option(names = {"--header-file", "-f"}, required = true) - File headerFile; - } - - @Nonnull - @Override - public List prepareFormatterSteps() { - return List.of(); - } -} From 83dbeb536b1a877dc3603e8bd3f8a64de9eaa266 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Sat, 16 Nov 2024 20:18:26 +0100 Subject: [PATCH 17/21] feat: implement more options for licenceHeader --- .../spotless/cli/steps/LicenseHeader.java | 20 ++++- .../spotless/cli/steps/LicenseHeaderTest.java | 89 +++++++++++++++++++ .../spotless/generic/LicenseHeaderStep.java | 2 +- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java index 110f26b41b..cfdcab452b 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/LicenseHeader.java @@ -26,6 +26,7 @@ import com.diffplug.spotless.antlr4.Antlr4Defaults; import com.diffplug.spotless.cli.core.SpotlessActionContext; import com.diffplug.spotless.cli.core.TargetFileTypeInferer; +import com.diffplug.spotless.cli.help.OptionConstants; import com.diffplug.spotless.cpp.CppDefaults; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.kotlin.KotlinConstants; @@ -39,7 +40,7 @@ public class LicenseHeader extends SpotlessFormatterStep { @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") LicenseHeaderSourceOption licenseHeaderSourceOption; - @CommandLine.Option(names = {"--delimiter", "-d"}, required = false, description = "The delimiter to use for the license header. If not provided, the default delimiter for the file type will be used (if available, otherwise java is assumed).") + @CommandLine.Option(names = {"--delimiter", "-d"}, required = false, description = "The delimiter to use for the license header. If not provided, the delimiter will be guessed based on the first few files we find. Otherwise, 'java' will be assumed.") String delimiter; static class LicenseHeaderSourceOption { @@ -49,13 +50,26 @@ static class LicenseHeaderSourceOption { File headerFile; } - // TODO add more config options + @CommandLine.Option(names = {"--year-mode", "-m"}, required = false, defaultValue = "PRESERVE", description = "How and if the year in the copyright header should be updated." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) + LicenseHeaderStep.YearMode yearMode; + + @CommandLine.Option(names = {"--year-separator", "-Y"}, required = false, defaultValue = LicenseHeaderStep.DEFAULT_YEAR_DELIMITER, description = "The separator to use for the year range in the license header." + OptionConstants.DEFAULT_VALUE_SUFFIX) + String yearSeparator; + + @CommandLine.Option(names = {"--skip-lines-matching", "-s"}, required = false, description = "Skip lines matching the given regex pattern before inserting the licence header.") + String skipLinesMatching; + + @CommandLine.Option(names = {"--content-pattern", "-c"}, required = false, description = "The pattern to match the content of the file before inserting the licence header. (If the file content does not match the pattern, the header will not be inserted/updated.)") + String contentPattern; @Nonnull @Override public List prepareFormatterSteps(SpotlessActionContext context) { FormatterStep licenseHeaderStep = LicenseHeaderStep.headerDelimiter(headerSource(context), delimiter(context.targetFileType())) - // TODO add more config options + .withYearMode(yearMode) + .withYearSeparator(yearSeparator) + .withSkipLinesMatching(skipLinesMatching) + .withContentPattern(contentPattern) .build(); return List.of(licenseHeaderStep); } 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 index 4362ffa235..69702caa11 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/LicenseHeaderTest.java @@ -17,10 +17,13 @@ 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 { @@ -75,4 +78,90 @@ void assertDelimiterIsApplied() { 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/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( From 9564bf2c0c181831848ada1bd088d179d569ea3e Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 21 Nov 2024 13:21:19 +0100 Subject: [PATCH 18/21] feat: add support for prettier calling from cli --- cli/build.gradle | 19 +- .../cli/SpotlessActionContextProvider.java | 3 +- .../diffplug/spotless/cli/SpotlessCLI.java | 13 +- .../spotless/cli/core/ChecksumCalculator.java | 129 ++++++++++ .../spotless/cli/core/ExecutionLayout.java | 108 ++++++++ .../spotless/cli/core/FilePathUtil.java | 45 ++++ .../cli/core/SpotlessActionContext.java | 39 ++- .../cli/core/SpotlessCommandLineStream.java | 70 ++++++ .../execution/SpotlessExecutionStrategy.java | 37 +-- .../cli/steps/BuildDirGloballyReusable.java | 18 ++ .../spotless/cli/steps/OptionDefaultUse.java | 44 ++++ .../diffplug/spotless/cli/steps/Prettier.java | 215 ++++++++++++++++ .../cli/core/ChecksumCalculatorTest.java | 234 ++++++++++++++++++ .../spotless/cli/steps/PrettierTest.java | 24 ++ .../spotless/npm/PrettierFormatterStep.java | 29 ++- 15 files changed, 990 insertions(+), 37 deletions(-) create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/ChecksumCalculator.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/FilePathUtil.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessCommandLineStream.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/steps/BuildDirGloballyReusable.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/steps/OptionDefaultUse.java create mode 100644 cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/core/ChecksumCalculatorTest.java create mode 100644 cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java diff --git a/cli/build.gradle b/cli/build.gradle index 682f7fd66b..8399051d3d 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -54,7 +54,9 @@ compileJava { // https://github.com/remkop/picocli/tree/main/picocli-codegen#222-other-options options.compilerArgs += [ "-Aproject=${project.group}/${project.name}", - "-Aother.resource.bundles=application" + "-Aother.resource.bundles=application", + // patterns require double-escaping (one escape is removed by groovy, the other one is needed in the resulting json file) + "-Aother.resource.patterns=.*\\\\.properties,.*\\\\.json,.*\\\\.js" ] } @@ -100,7 +102,7 @@ def nativeCompileMetaDir = project.layout.buildDirectory.dir('nativeCompile/src/ // use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image graalvmNative { agent { - enabled = true + enabled = true // we would love to make this dynamic, but it's not possible defaultMode = "standard" metadataCopy { inputTaskNames.add('test') @@ -110,8 +112,11 @@ graalvmNative { tasksToInstrumentPredicate = new java.util.function.Predicate() { @Override boolean test(Task task) { - println ("Instrumenting task: " + task.name + " " + task.name == 'test') + // if (project.hasProperty('agent')) { + println ("Instrumenting task: " + task.name + " " + task.name == 'test' + "proj: " + task.project.hasProperty('agent')) return task.name == 'test' + // } + // return false } } } @@ -159,16 +164,18 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow } } -gradle.taskGraph.whenReady { graph -> - if (graph.hasTask('nativeCompile') || graph.hasTask('metadataCopy') || graph.hasTask('shadowJar')) { +gradle.taskGraph.whenReady { TaskExecutionGraph graph -> + // println "Graph: " + graph.allTasks*.name + if (graph.hasTask(':cli:nativeCompile') || graph.hasTask(':cli:metadataCopy') || graph.hasTask(':cli:shadowJar')) { // enable graalvm agent using property here instead of command line `-Pagent=standard` // this collects information about reflective access and resources used by the application (e.g. GJF) - project.property('agent', 'standard') + project.ext.agent = 'standard' } } tasks.withType(Test).configureEach { if (it.name == 'test') { + it.outputs.dir(nativeCompileMetaDir) if (project.hasProperty('agent')) { it.inputs.property('agent', project.property('agent')) // make sure to re-run tests if agent changes } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java index 556d5385ff..974cb9dd12 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java @@ -16,8 +16,9 @@ package com.diffplug.spotless.cli; import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.cli.core.SpotlessCommandLineStream; public interface SpotlessActionContextProvider { - SpotlessActionContext spotlessActionContext(); + SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream); } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index 7df5a16c85..80e551711b 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -28,12 +28,14 @@ import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.cli.core.FileResolver; import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.cli.core.SpotlessCommandLineStream; import com.diffplug.spotless.cli.core.TargetFileTypeInferer; import com.diffplug.spotless.cli.core.TargetResolver; import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy; import com.diffplug.spotless.cli.help.OptionConstants; import com.diffplug.spotless.cli.steps.GoogleJavaFormat; import com.diffplug.spotless.cli.steps.LicenseHeader; +import com.diffplug.spotless.cli.steps.Prettier; import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; import picocli.CommandLine; @@ -41,7 +43,8 @@ @Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", synopsisSubcommandLabel = "[FORMATTING_STEPS]", commandListHeading = "%nAvailable formatting steps:%n", subcommandsRepeatable = true, subcommands = { LicenseHeader.class, - GoogleJavaFormat.class}) + GoogleJavaFormat.class, + Prettier.class}) public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider { @CommandLine.Spec @@ -110,11 +113,15 @@ private Path baseDir() { } @Override - public SpotlessActionContext spotlessActionContext() { + public SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream) { validateTargets(); TargetResolver targetResolver = targetResolver(); TargetFileTypeInferer targetFileTypeInferer = new TargetFileTypeInferer(targetResolver); - return new SpotlessActionContext(targetFileTypeInferer.inferTargetFileType(), new FileResolver(baseDir())); + return SpotlessActionContext.builder() + .targetFileType(targetFileTypeInferer.inferTargetFileType()) + .fileResolver(new FileResolver(baseDir())) + .commandLineStream(commandLineStream) + .build(); } public static void main(String... args) { diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/ChecksumCalculator.java b/cli/src/main/java/com/diffplug/spotless/cli/core/ChecksumCalculator.java new file mode 100644 index 0000000000..652b277201 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/ChecksumCalculator.java @@ -0,0 +1,129 @@ +/* + * 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 java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import com.diffplug.common.hash.Hashing; +import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.cli.SpotlessAction; +import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; + +import picocli.CommandLine; + +public class ChecksumCalculator { + + public String calculateChecksum(SpotlessCLIFormatterStep step) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + writeObjectDataTo(step, out); + return toHashedHexBytes(out.toByteArray()); + } catch (Exception e) { + throw ThrowingEx.asRuntime(e); + } + } + + private void writeObjectDataTo(Object object, OutputStream outputStream) { + ThrowingEx.run(() -> outputStream.write(object.getClass().getName().getBytes(StandardCharsets.UTF_8))); + options(object) + .map(Object::toString) + .map(str -> str.getBytes(StandardCharsets.UTF_8)) + .forEachOrdered(bytes -> ThrowingEx.run(() -> outputStream.write(bytes))); + + } + + public String calculateChecksum(SpotlessCommandLineStream commandLineStream) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + calculateChecksumOfActions(commandLineStream.actions(), out); + + calculateChecksumOfSteps(commandLineStream.formatterSteps(), out); + return toHashedHexBytes(out.toByteArray()); + } catch (Exception e) { + throw ThrowingEx.asRuntime(e); + } + } + + private void calculateChecksumOfSteps(Stream spotlessCLIFormatterStepStream, ByteArrayOutputStream out) { + spotlessCLIFormatterStepStream.forEachOrdered(step -> writeObjectDataTo(step, out)); + } + + private void calculateChecksumOfActions(Stream actions, ByteArrayOutputStream out) { + actions.forEachOrdered(action -> writeObjectDataTo(action, out)); + } + + private static Stream options(Object step) { + List> classHierarchy = classHierarchy(step); + return classHierarchy.stream() + .flatMap(clazz -> Arrays.stream(clazz.getDeclaredFields())) + .flatMap(field -> expandOptionField(field, step)) + .map(FieldOnObject::getValue) + .filter(Objects::nonNull); + } + + private static List> classHierarchy(Object obj) { + List> hierarchy = new ArrayList<>(); + Class clazz = obj.getClass(); + while (clazz != null) { + hierarchy.add(clazz); + clazz = clazz.getSuperclass(); + } + return hierarchy; + } + + private static Stream expandOptionField(Field field, Object obj) { + if (field.isAnnotationPresent(CommandLine.Option.class) || field.isAnnotationPresent(CommandLine.Parameters.class)) { + return Stream.of(new FieldOnObject(field, obj)); + } + if (field.isAnnotationPresent(CommandLine.ArgGroup.class)) { + Object fieldValue = new FieldOnObject(field, obj).getValue(); + return Arrays.stream(fieldValue.getClass().getDeclaredFields()) + .flatMap(subField -> expandOptionField(subField, fieldValue)); + } + return Stream.empty(); // nothing to expand + } + + private static String toHashedHexBytes(byte[] bytes) { + byte[] hash = Hashing.murmur3_128().hashBytes(bytes).asBytes(); + StringBuilder builder = new StringBuilder(); + for (byte b : hash) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } + + private static class FieldOnObject { + private final Field field; + private final Object obj; + + FieldOnObject(Field field, Object obj) { + this.field = field; + this.obj = obj; + } + + Object getValue() { + ThrowingEx.run(() -> field.setAccessible(true)); + return ThrowingEx.get(() -> field.get(obj)); + } + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java b/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java new file mode 100644 index 0000000000..83fedd4d65 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java @@ -0,0 +1,108 @@ +/* + * 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 java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.cli.steps.BuildDirGloballyReusable; +import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; + +public class ExecutionLayout { + + private final FileResolver fileResolver; + private final SpotlessCommandLineStream commandLineStream; + private final ChecksumCalculator checksumCalculator; + + private ExecutionLayout(@Nonnull FileResolver fileResolver, SpotlessCommandLineStream commandLineStream) { + this.fileResolver = Objects.requireNonNull(fileResolver); + this.commandLineStream = commandLineStream; + this.checksumCalculator = new ChecksumCalculator(); + } + + public static ExecutionLayout create(FileResolver fileResolver, SpotlessCommandLineStream commandLineStream) { + return new ExecutionLayout(fileResolver, commandLineStream); + } + + public Optional find(Path searchPath) { + Path found = fileResolver.resolvePath(searchPath); + if (found.toFile().canRead()) { + return Optional.of(found); + } + if (searchPath.toFile().canRead()) { + return Optional.of(searchPath); + } + return Optional.empty(); + } + + public Path baseDir() { + return fileResolver.baseDir(); + } + + public Path buildDir() { + // gradle? + if (isGradleDirectory()) { + return gradleBuildDir(); + } + if (isMavenDirectory()) { + return mavenBuildDir(); + } + return tempBuildDir(); + } + + private boolean isGradleDirectory() { + return List.of("build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts").stream() + .map(Paths::get) + .map(this::find) + .anyMatch(Optional::isPresent); + } + + private Path gradleBuildDir() { + return fileResolver.resolvePath(Paths.get("build", "spotless-cli")); + } + + private boolean isMavenDirectory() { + return List.of("pom.xml").stream() + .map(Paths::get) + .map(this::find) + .anyMatch(Optional::isPresent); + } + + private Path mavenBuildDir() { + return fileResolver.resolvePath(Paths.get("target", "spotless-cli")); + } + + private Path tempBuildDir() { + String tmpDir = System.getProperty("java.io.tmpdir"); + return Path.of(tmpDir, "spotless-cli"); + } + + public Path buildDirFor(@Nonnull SpotlessCLIFormatterStep step) { + Objects.requireNonNull(step); + Path buildDir = buildDir(); + String checksum = checksumCalculator.calculateChecksum(step); + if (step instanceof BuildDirGloballyReusable) { + return buildDir.resolve(checksum); + } + String commandLineChecksum = checksumCalculator.calculateChecksum(commandLineStream); + return buildDir.resolve(checksum + "-" + commandLineChecksum); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/FilePathUtil.java b/cli/src/main/java/com/diffplug/spotless/cli/core/FilePathUtil.java new file mode 100644 index 0000000000..e83d691ff2 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/FilePathUtil.java @@ -0,0 +1,45 @@ +/* + * 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 java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public final class FilePathUtil { + + private FilePathUtil() { + // no instance + } + + public static File asFile(Path path) { + return path == null ? null : path.toFile(); + } + + public static List asFiles(List paths) { + return paths == null ? null : paths.stream().map(Path::toFile).collect(Collectors.toList()); + } + + public static List assertDirectoryExists(File... files) { + return assertDirectoryExists(Arrays.asList(files)); + } + + public static List assertDirectoryExists(List files) { + return files.stream().map(f -> f != null && f.mkdirs()).collect(Collectors.toList()); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java index 8e584dc175..894a73c919 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessActionContext.java @@ -27,10 +27,12 @@ public class SpotlessActionContext { private final TargetFileTypeInferer.TargetFileType targetFileType; private final FileResolver fileResolver; + private final ExecutionLayout executionLayout; - public SpotlessActionContext(@Nonnull TargetFileTypeInferer.TargetFileType targetFileType, @Nonnull FileResolver fileResolver) { + private SpotlessActionContext(@Nonnull TargetFileTypeInferer.TargetFileType targetFileType, @Nonnull FileResolver fileResolver, @Nonnull SpotlessCommandLineStream commandLineStream) { this.targetFileType = Objects.requireNonNull(targetFileType); - this.fileResolver = fileResolver; + this.fileResolver = Objects.requireNonNull(fileResolver); + this.executionLayout = ExecutionLayout.create(fileResolver, Objects.requireNonNull(commandLineStream)); } @Nonnull @@ -49,4 +51,37 @@ public Path resolvePath(Path path) { public Provisioner provisioner() { return CliJarProvisioner.INSTANCE; } + + public ExecutionLayout executionLayout() { + return executionLayout; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private TargetFileTypeInferer.TargetFileType targetFileType; + private FileResolver fileResolver; + private SpotlessCommandLineStream commandLineStream; + + public Builder targetFileType(TargetFileTypeInferer.TargetFileType targetFileType) { + this.targetFileType = targetFileType; + return this; + } + + public Builder fileResolver(FileResolver fileResolver) { + this.fileResolver = fileResolver; + return this; + } + + public Builder commandLineStream(SpotlessCommandLineStream commandLineStream) { + this.commandLineStream = commandLineStream; + return this; + } + + public SpotlessActionContext build() { + return new SpotlessActionContext(targetFileType, fileResolver, commandLineStream); + } + } } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessCommandLineStream.java b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessCommandLineStream.java new file mode 100644 index 0000000000..7303e4f3b8 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/SpotlessCommandLineStream.java @@ -0,0 +1,70 @@ +/* + * 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 java.util.stream.Stream; + +import com.diffplug.spotless.cli.SpotlessAction; +import com.diffplug.spotless.cli.SpotlessActionContextProvider; +import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; + +import picocli.CommandLine; + +public interface SpotlessCommandLineStream { // todo turn into an interface + + static SpotlessCommandLineStream of(CommandLine.ParseResult parseResult) { + return new DefaultSpotlessCommandLineStream(parseResult); + } + + Stream formatterSteps(); + + Stream contextProviders(); + + Stream actions(); + + class DefaultSpotlessCommandLineStream implements SpotlessCommandLineStream { + + private final CommandLine.ParseResult parseResult; + + private DefaultSpotlessCommandLineStream(CommandLine.ParseResult parseResult) { + this.parseResult = parseResult; + } + + @Override + public Stream formatterSteps() { + return parseResult.asCommandLineList().stream() + .map(CommandLine::getCommand) + .filter(command -> command instanceof SpotlessCLIFormatterStep) + .map(SpotlessCLIFormatterStep.class::cast); + } + + @Override + public Stream contextProviders() { + return parseResult.asCommandLineList().stream() + .map(CommandLine::getCommand) + .filter(command -> command instanceof SpotlessActionContextProvider) + .map(SpotlessActionContextProvider.class::cast); + } + + @Override + public Stream actions() { + return parseResult.asCommandLineList().stream() + .map(CommandLine::getCommand) + .filter(command -> command instanceof SpotlessAction) + .map(SpotlessAction.class::cast); + } + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java index 71606cba57..081c86aed8 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/execution/SpotlessExecutionStrategy.java @@ -21,10 +21,8 @@ import java.util.stream.Collectors; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.cli.SpotlessAction; -import com.diffplug.spotless.cli.SpotlessActionContextProvider; import com.diffplug.spotless.cli.core.SpotlessActionContext; -import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; +import com.diffplug.spotless.cli.core.SpotlessCommandLineStream; import picocli.CommandLine; @@ -35,44 +33,35 @@ public int execute(CommandLine.ParseResult parseResult) throws CommandLine.Execu if (helpResult != null) { return helpResult; } - return runSpotlessActions(parseResult); + return runSpotlessActions(SpotlessCommandLineStream.of(parseResult)); } - private Integer runSpotlessActions(CommandLine.ParseResult parseResult) { + private Integer runSpotlessActions(SpotlessCommandLineStream commandLineStream) { // 1. prepare context - SpotlessActionContext context = provideSpotlessActionContext(parseResult); + SpotlessActionContext context = provideSpotlessActionContext(commandLineStream); // 2. run setup (for combining steps handled as subcommands) - List steps = prepareFormatterSteps(parseResult, context); + List steps = prepareFormatterSteps(commandLineStream, context); // 3. run spotless steps - return executeSpotlessAction(parseResult, steps); + return executeSpotlessAction(commandLineStream, steps); } - private SpotlessActionContext provideSpotlessActionContext(CommandLine.ParseResult parseResult) { - return parseResult.asCommandLineList().stream() - .map(CommandLine::getCommand) - .filter(command -> command instanceof SpotlessActionContextProvider) - .map(SpotlessActionContextProvider.class::cast) + private SpotlessActionContext provideSpotlessActionContext(SpotlessCommandLineStream commandLineStream) { + return commandLineStream.contextProviders() .findFirst() - .map(SpotlessActionContextProvider::spotlessActionContext) + .map(provider -> provider.spotlessActionContext(commandLineStream)) .orElseThrow(() -> new IllegalStateException("No SpotlessActionContextProvider found")); } - private List prepareFormatterSteps(CommandLine.ParseResult parseResult, SpotlessActionContext context) { - return parseResult.asCommandLineList().stream() - .map(CommandLine::getCommand) - .filter(command -> command instanceof SpotlessCLIFormatterStep) - .map(SpotlessCLIFormatterStep.class::cast) + private List prepareFormatterSteps(SpotlessCommandLineStream commandLineStream, SpotlessActionContext context) { + return commandLineStream.formatterSteps() .flatMap(step -> step.prepareFormatterSteps(context).stream()) .collect(Collectors.toList()); } - private Integer executeSpotlessAction(CommandLine.ParseResult parseResult, List steps) { - return parseResult.asCommandLineList().stream() - .map(CommandLine::getCommand) - .filter(command -> command instanceof SpotlessAction) - .map(SpotlessAction.class::cast) + private Integer executeSpotlessAction(SpotlessCommandLineStream commandLineStream, List steps) { + return commandLineStream.actions() .findFirst() .map(spotlessAction -> spotlessAction.executeSpotlessAction(steps)) .orElse(-1); diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/BuildDirGloballyReusable.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/BuildDirGloballyReusable.java new file mode 100644 index 0000000000..117adf558e --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/BuildDirGloballyReusable.java @@ -0,0 +1,18 @@ +/* + * 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; + +public interface BuildDirGloballyReusable {} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/OptionDefaultUse.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/OptionDefaultUse.java new file mode 100644 index 0000000000..5b9ef69fb6 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/OptionDefaultUse.java @@ -0,0 +1,44 @@ +/* + * 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.util.function.Supplier; + +import javax.annotation.Nullable; + +import com.diffplug.common.base.Suppliers; + +public class OptionDefaultUse { + + @Nullable + private final T obj; + + private OptionDefaultUse(@Nullable T obj) { + this.obj = obj; + } + + public static OptionDefaultUse use(@Nullable T obj) { + return new OptionDefaultUse<>(obj); + } + + public T orIfNullGet(Supplier supplier) { + return obj != null ? obj : supplier.get(); + } + + public T orIfNull(T other) { + return orIfNullGet(Suppliers.ofInstance(other)); + } +} diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java new file mode 100644 index 0000000000..3eb5c2eb99 --- /dev/null +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java @@ -0,0 +1,215 @@ +/* + * 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 com.diffplug.spotless.cli.core.FilePathUtil.asFile; +import static com.diffplug.spotless.cli.core.FilePathUtil.asFiles; +import static com.diffplug.spotless.cli.core.FilePathUtil.assertDirectoryExists; +import static com.diffplug.spotless.cli.steps.OptionDefaultUse.use; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.cli.core.ExecutionLayout; +import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.npm.NpmPathResolver; +import com.diffplug.spotless.npm.PrettierConfig; +import com.diffplug.spotless.npm.PrettierFormatterStep; + +import picocli.CommandLine; + +@CommandLine.Command(name = "prettier", description = "Runs prettier") +public class Prettier extends SpotlessFormatterStep { + + @CommandLine.Option(names = {"--dev-dependency", "-D"}, description = "The devDependencies to use for Prettier.") + Map devDependencies; + + @CommandLine.Option(names = {"--cache-dir", "-C"}, description = "The directory to use for caching Prettier.") + Path cacheDir; + + @CommandLine.Option(names = {"--npm-exec", "-n"}, description = "The explicit path to the npm executable.") + Path explicitNpmExecutable; + + @CommandLine.Option(names = {"--node-exec", "-N"}, description = "The explicit path to the node executable.") + Path explicitNodeExecutable; + + @CommandLine.Option(names = {"--npmrc-file", "-R"}, description = "The explicit path to the .npmrc file.") + Path explicitNpmrcFile; + + @CommandLine.Option(names = {"--additional-npmrc-location", "-A"}, description = "Additional locations to search for .npmrc files.") + List additionalNpmrcLocations; + + @CommandLine.Option(names = {"--prettier-config-path", "-P"}, description = "The path to the Prettier configuration file.") + Path prettierConfigPath; + + @CommandLine.Option(names = {"--prettier-config-option", "-c"}, description = "The Prettier configuration options.") + Map prettierConfigOptions; + + @Nonnull + @Override + public List prepareFormatterSteps(SpotlessActionContext context) { + FormatterStep prettierFormatterStep = builder(context) + .withDevDependencies(devDependencies()) + .withCacheDir(cacheDir) + .withExplicitNpmExecutable(explicitNpmExecutable) + .withExplicitNodeExecutable(explicitNodeExecutable) + .withExplicitNpmrcFile(explicitNpmrcFile) + .withAdditionalNpmrcLocations(additionalNpmrcLocations()) + .withPrettierConfigOptions(prettierConfigOptions) + .withPrettierConfigPath(prettierConfigPath) + .build(); + + // return List.of(adapt(prettierFormatterStep)); + + return List.of(prettierFormatterStep); + } + + private static FormatterStep adapt(FormatterStep step) { + return new FormatterStep() { + @Override + public String getName() { + return step.getName(); + } + + @Nullable + @Override + public String format(String rawUnix, File file) throws Exception { + return step.format(rawUnix, file); + } + + @Override + public void close() throws Exception { + step.close(); + } + }; + } + + private Map devDependencies() { + return use(devDependencies).orIfNullGet(PrettierFormatterStep::defaultDevDependencies); + } + + private List additionalNpmrcLocations() { + return use(additionalNpmrcLocations).orIfNullGet(Collections::emptyList); + } + + private PrettierFormatterStepBuilder builder(@Nonnull SpotlessActionContext context) { + return new PrettierFormatterStepBuilder(context); + } + + private class PrettierFormatterStepBuilder { + + @Nonnull + private final SpotlessActionContext context; + + private Map devDependencies; + + private Path cacheDir = null; + + // npmPathResolver + private Path explicitNpmExecutable; + + private Path explicitNodeExecutable; + + private Path explicitNpmrcFile; + + private List additionalNpmrcLocations; + + // prettierConfig + + private Map prettierConfigOptions; + + private Path prettierConfigPath; + + private PrettierFormatterStepBuilder(@Nonnull SpotlessActionContext context) { + this.context = Objects.requireNonNull(context); + } + + public PrettierFormatterStepBuilder withDevDependencies(Map devDependencies) { + this.devDependencies = devDependencies; + return this; + } + + public PrettierFormatterStepBuilder withCacheDir(Path cacheDir) { + this.cacheDir = cacheDir; + return this; + } + + public PrettierFormatterStepBuilder withExplicitNpmExecutable(Path explicitNpmExecutable) { + this.explicitNpmExecutable = explicitNpmExecutable; + return this; + } + + public PrettierFormatterStepBuilder withExplicitNodeExecutable(Path explicitNodeExecutable) { + this.explicitNodeExecutable = explicitNodeExecutable; + return this; + } + + public PrettierFormatterStepBuilder withExplicitNpmrcFile(Path explicitNpmrcFile) { + this.explicitNpmrcFile = explicitNpmrcFile; + return this; + } + + public PrettierFormatterStepBuilder withAdditionalNpmrcLocations(List additionalNpmrcLocations) { + this.additionalNpmrcLocations = additionalNpmrcLocations; + return this; + } + + public PrettierFormatterStepBuilder withPrettierConfigOptions(Map prettierConfigOptions) { + this.prettierConfigOptions = prettierConfigOptions; + return this; + } + + public PrettierFormatterStepBuilder withPrettierConfigPath(Path prettierConfigPath) { + this.prettierConfigPath = prettierConfigPath; + return this; + } + + public FormatterStep build() { + ExecutionLayout layout = context.executionLayout(); + File projectDirFile = asFile(layout.find(Path.of("package.json")) // project dir + .map(Path::getParent) + .orElseGet(layout::baseDir)); + File buildDirFile = asFile(layout.buildDirFor(Prettier.this)); + File cacheDirFile = asFile(cacheDir); + assertDirectoryExists(projectDirFile, buildDirFile, cacheDirFile); + FormatterStep step = PrettierFormatterStep.create( + use(devDependencies).orIfNullGet(PrettierFormatterStep::defaultDevDependencies), + context.provisioner(), + projectDirFile, + buildDirFile, + cacheDirFile, + new NpmPathResolver( + asFile(explicitNpmExecutable), + asFile(explicitNodeExecutable), + asFile(explicitNpmrcFile), + asFiles(additionalNpmrcLocations)), + new PrettierConfig( + asFile(prettierConfigPath), + prettierConfigOptions)); + return step; + } + + } + +} 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/PrettierTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java new file mode 100644 index 0000000000..c544eaad53 --- /dev/null +++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.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.cli.CLIIntegrationHarness; + +public class PrettierTest extends CLIIntegrationHarness { + + // TODO + +} 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 { From caf8ddea27977741f06b3e34b598a632c060bbe9 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 5 Dec 2024 20:37:54 +0100 Subject: [PATCH 19/21] feat: add support for npm based cli tests --- cli/build.gradle | 13 +++--- .../spotless/cli/core/ExecutionLayout.java | 12 +++-- .../diffplug/spotless/cli/steps/Prettier.java | 46 +++++++++++-------- .../spotless/cli/CLIIntegrationHarness.java | 8 +++- .../spotless/cli/steps/PrettierTest.java | 35 ++++++++++++++ gradle/special-tests.gradle | 4 +- .../spotless/tag/CliNativeNpmTest.java | 30 ++++++++++++ .../spotless/tag/CliProcessNpmTest.java | 30 ++++++++++++ 8 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 testlib/src/main/java/com/diffplug/spotless/tag/CliNativeNpmTest.java create mode 100644 testlib/src/main/java/com/diffplug/spotless/tag/CliProcessNpmTest.java diff --git a/cli/build.gradle b/cli/build.gradle index 8399051d3d..1fee39cb4c 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -102,10 +102,11 @@ def nativeCompileMetaDir = project.layout.buildDirectory.dir('nativeCompile/src/ // use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image graalvmNative { agent { - enabled = true // we would love to make this dynamic, but it's not possible + enabled = !project.hasProperty('skipGraalAgent') // we would love to make this dynamic, but it's not possible defaultMode = "standard" metadataCopy { inputTaskNames.add('test') + inputTaskNames.add('testNpm') mergeWithExisting = false outputDirectories.add(nativeCompileMetaDir.get().asFile.path) } @@ -114,7 +115,7 @@ graalvmNative { boolean test(Task task) { // if (project.hasProperty('agent')) { println ("Instrumenting task: " + task.name + " " + task.name == 'test' + "proj: " + task.project.hasProperty('agent')) - return task.name == 'test' + return task.name == 'test' || task.name == 'testNpm' // } // return false } @@ -148,7 +149,7 @@ graalvmNative { tasks.named('metadataCopy') { - dependsOn('test') + dependsOn('test', 'testNpm') } tasks.named('nativeCompile') { @@ -174,17 +175,17 @@ gradle.taskGraph.whenReady { TaskExecutionGraph graph -> } tasks.withType(Test).configureEach { - if (it.name == 'test') { + if (it.name == 'test' || it.name == 'testNpm') { it.outputs.dir(nativeCompileMetaDir) if (project.hasProperty('agent')) { it.inputs.property('agent', project.property('agent')) // make sure to re-run tests if agent changes } } - if (it.name == 'testCliProcess') { + if (it.name == 'testCliProcess' || it.name == 'testCliProcessNpm') { it.dependsOn('shadowJar') it.systemProperty 'spotless.cli.shadowJar', tasks.shadowJar.archiveFile.get().asFile } - if (it.name == 'testCliNative') { + if (it.name == 'testCliNative' || it.name == 'testCliNativeNpm') { it.dependsOn('nativeCompile') it.systemProperty 'spotless.cli.nativeImage', tasks.nativeCompile.outputFile.get().asFile } diff --git a/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java b/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java index 83fedd4d65..bf170296b2 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/core/ExecutionLayout.java @@ -22,6 +22,7 @@ import java.util.Optional; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import com.diffplug.spotless.cli.steps.BuildDirGloballyReusable; import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep; @@ -32,17 +33,20 @@ public class ExecutionLayout { private final SpotlessCommandLineStream commandLineStream; private final ChecksumCalculator checksumCalculator; - private ExecutionLayout(@Nonnull FileResolver fileResolver, SpotlessCommandLineStream commandLineStream) { + private ExecutionLayout(@Nonnull FileResolver fileResolver, @Nonnull SpotlessCommandLineStream commandLineStream) { this.fileResolver = Objects.requireNonNull(fileResolver); - this.commandLineStream = commandLineStream; + this.commandLineStream = Objects.requireNonNull(commandLineStream); this.checksumCalculator = new ChecksumCalculator(); } - public static ExecutionLayout create(FileResolver fileResolver, SpotlessCommandLineStream commandLineStream) { + public static ExecutionLayout create(@Nonnull FileResolver fileResolver, @Nonnull SpotlessCommandLineStream commandLineStream) { return new ExecutionLayout(fileResolver, commandLineStream); } - public Optional find(Path searchPath) { + public Optional find(@Nullable Path searchPath) { + if (searchPath == null) { + return Optional.empty(); + } Path found = fileResolver.resolvePath(searchPath); if (found.toFile().canRead()) { return Optional.of(found); diff --git a/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java b/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java index 3eb5c2eb99..b10ae4f8c0 100644 --- a/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java +++ b/cli/src/main/java/com/diffplug/spotless/cli/steps/Prettier.java @@ -23,12 +23,12 @@ import java.io.File; import java.nio.file.Path; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.core.ExecutionLayout; @@ -64,7 +64,7 @@ public class Prettier extends SpotlessFormatterStep { Path prettierConfigPath; @CommandLine.Option(names = {"--prettier-config-option", "-c"}, description = "The Prettier configuration options.") - Map prettierConfigOptions; + Map prettierConfigOptions; @Nonnull @Override @@ -76,7 +76,7 @@ public List prepareFormatterSteps(SpotlessActionContext context) .withExplicitNodeExecutable(explicitNodeExecutable) .withExplicitNpmrcFile(explicitNpmrcFile) .withAdditionalNpmrcLocations(additionalNpmrcLocations()) - .withPrettierConfigOptions(prettierConfigOptions) + .withPrettierConfigOptions(prettierConfigOptions()) .withPrettierConfigPath(prettierConfigPath) .build(); @@ -85,24 +85,30 @@ public List prepareFormatterSteps(SpotlessActionContext context) return List.of(prettierFormatterStep); } - private static FormatterStep adapt(FormatterStep step) { - return new FormatterStep() { - @Override - public String getName() { - return step.getName(); - } - - @Nullable - @Override - public String format(String rawUnix, File file) throws Exception { - return step.format(rawUnix, file); + private Map prettierConfigOptions() { + if (prettierConfigOptions == null) { + return Collections.emptyMap(); + } + Map normalized = new LinkedHashMap<>(); + prettierConfigOptions.forEach((key, value) -> { + if (value == null) { + normalized.put(key, null); + } else { + normalized.put(key, normalizePrettierOption(key, value)); } + }); + return normalized; + } - @Override - public void close() throws Exception { - step.close(); - } - }; + private Object normalizePrettierOption(String key, String value) { + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return Boolean.parseBoolean(value); + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return value; + } } private Map devDependencies() { @@ -205,7 +211,7 @@ public FormatterStep build() { asFile(explicitNpmrcFile), asFiles(additionalNpmrcLocations)), new PrettierConfig( - asFile(prettierConfigPath), + asFile(prettierConfigPath != null ? layout.find(prettierConfigPath).orElseThrow() : null), prettierConfigOptions)); return step; } diff --git a/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java index 7e652a8a81..02ec270a8c 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/CLIIntegrationHarness.java @@ -20,7 +20,9 @@ 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 { @@ -51,11 +53,13 @@ protected SpotlessCLIRunner cliRunner() { private SpotlessCLIRunner createRunnerForTag() { CliProcessTest cliProcessTest = getClass().getAnnotation(CliProcessTest.class); - if (cliProcessTest != null) { + CliProcessNpmTest cliProcessNpmTest = getClass().getAnnotation(CliProcessNpmTest.class); + if (cliProcessTest != null || cliProcessNpmTest != null) { return SpotlessCLIRunner.createExternalProcess(); } CliNativeTest cliNativeTest = getClass().getAnnotation(CliNativeTest.class); - if (cliNativeTest != null) { + 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/steps/PrettierTest.java b/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java index c544eaad53..2fea1ef9df 100644 --- a/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java +++ b/cli/src/test/java/com/diffplug/spotless/cli/steps/PrettierTest.java @@ -15,10 +15,45 @@ */ 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/special-tests.gradle b/gradle/special-tests.gradle index 4d4da7424c..eff60c4bfb 100644 --- a/gradle/special-tests.gradle +++ b/gradle/special-tests.gradle @@ -9,7 +9,9 @@ def special = [ 'npm', 'shfmt', 'cliProcess', - 'cliNative' + 'cliProcessNpm', + 'cliNative', + 'cliNativeNpm' ] boolean isCiServer = System.getenv().containsKey("CI") 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/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 {} From 417571f8c3d6f03243fa3e069b7f5ec822394e61 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Thu, 5 Dec 2024 20:45:34 +0100 Subject: [PATCH 20/21] fix: prevent issues with groovy-decorations in build --- cli/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/build.gradle b/cli/build.gradle index 1fee39cb4c..4135d56481 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -79,11 +79,12 @@ class ApplicationPropertiesProcessResourcesAction implements Action Date: Fri, 7 Feb 2025 21:46:50 +0100 Subject: [PATCH 21/21] doc: adding first readme documentation steps --- README.md | 8 ++++++++ cli/README.md | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 cli/README.md diff --git a/README.md b/README.md index a40b9edb0d..7ac14b3b3f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Gradle Plugin](https://img.shields.io/gradle-plugin-portal/v/com.diffplug.spotless?color=blue&label=gradle%20plugin)](plugin-gradle) [![Maven Plugin](https://img.shields.io/maven-central/v/com.diffplug.spotless/spotless-maven-plugin?color=blue&label=maven%20plugin)](plugin-maven) [![SBT Plugin](https://img.shields.io/badge/sbt%20plugin-0.1.3-blue)](https://github.com/moznion/sbt-spotless) +[![CLI](https://img.shields.io/badge/cli-0.0.1-blue)](cli) Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | shell | sql | typeScript | vue | yaml | anything> using <gradle | maven | sbt | anything>. @@ -41,6 +42,13 @@ user@machine repo % mvn spotless:check ``` ## [❇️ Spotless for SBT (external for now)](https://github.com/moznion/sbt-spotless) + +## [❇️ Spotless Command Line Interface (CLI)](cli) + +```console +user@machine repo % spotless --target '**/src/**/*.java' license-header --header='/* Myself $YEAR */' google-java-format +``` + ## [Other build systems](CONTRIBUTING.md#how-to-add-a-new-plugin-for-a-build-system) ## How it works (for potential contributors) diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000000..12d0f2fb5d --- /dev/null +++ b/cli/README.md @@ -0,0 +1,25 @@ +# Spotless Command Line Interface CLI +*Keep your code Spotless with Gradle* + + +[![Changelog](https://img.shields.io/badge/changelog-0.0.1-blue.svg)](CHANGES.md) + +[![OS Win](https://img.shields.io/badge/OS-Windows-blueviolet.svg)](README.md) +[![OS Linux](https://img.shields.io/badge/OS-Linux-blueviolet.svg)](README.md) +[![OS macOS](https://img.shields.io/badge/OS-macOS-blueviolet.svg)](README.md) + + +`spotless` is a command line interface (CLI) for the [spotless code formatter](../README.md). +It intends to be a simple alternative to its siblings: the plugins for [gradle](../plugin-gradle/README.md), [maven](../plugin-maven/README.md) +and others. + +- TODO: add usage and examples +- TBD: can usage be generated automatically e.g. via freshmark?