Skip to content

Commit 53faf7f

Browse files
committed
feat: add poc implementation
Refs: diffplug/spotless#2419
1 parent 8b862de commit 53faf7f

File tree

53 files changed

+3350
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3350
-12
lines changed

Diff for: .idea/compiler.xml

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .idea/modules.xml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .idea/modules/testlib/spotless-cli.testlib.main.iml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: app/build.gradle

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ plugins {
1111
dependencies {
1212
implementation 'org.apache.commons:commons-text'
1313
implementation project(':utilities')
14+
testImplementation project(':testlib')
15+
implementation libs.bundles.spotless.libs
16+
17+
implementation libs.picocli
18+
annotationProcessor libs.picocli.codegen
1419
}
1520

1621
application {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.cli;
17+
18+
import java.util.List;
19+
20+
import org.jetbrains.annotations.NotNull;
21+
22+
import com.diffplug.spotless.FormatterStep;
23+
24+
public interface SpotlessAction extends SpotlessCommand {
25+
Integer executeSpotlessAction(@NotNull List<FormatterStep> formatterSteps);
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.cli;
17+
18+
import com.diffplug.spotless.cli.core.SpotlessActionContext;
19+
import com.diffplug.spotless.cli.core.SpotlessCommandLineStream;
20+
21+
public interface SpotlessActionContextProvider {
22+
23+
SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream);
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.cli;
17+
18+
import java.nio.charset.Charset;
19+
import java.nio.file.Path;
20+
import java.util.List;
21+
22+
import org.jetbrains.annotations.NotNull;
23+
24+
import com.diffplug.spotless.Formatter;
25+
import com.diffplug.spotless.FormatterStep;
26+
import com.diffplug.spotless.LineEnding;
27+
import com.diffplug.spotless.LintState;
28+
import com.diffplug.spotless.ThrowingEx;
29+
import com.diffplug.spotless.cli.core.FileResolver;
30+
import com.diffplug.spotless.cli.core.SpotlessActionContext;
31+
import com.diffplug.spotless.cli.core.SpotlessCommandLineStream;
32+
import com.diffplug.spotless.cli.core.TargetFileTypeInferer;
33+
import com.diffplug.spotless.cli.core.TargetResolver;
34+
import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy;
35+
import com.diffplug.spotless.cli.help.OptionConstants;
36+
import com.diffplug.spotless.cli.steps.GoogleJavaFormat;
37+
import com.diffplug.spotless.cli.steps.LicenseHeader;
38+
import com.diffplug.spotless.cli.steps.Prettier;
39+
import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider;
40+
41+
import picocli.CommandLine;
42+
import picocli.CommandLine.Command;
43+
44+
@Command(
45+
name = "spotless",
46+
mixinStandardHelpOptions = true,
47+
versionProvider = SpotlessCLIVersionProvider.class,
48+
description = "Runs spotless",
49+
synopsisSubcommandLabel = "[FORMATTING_STEPS]",
50+
commandListHeading = "%nAvailable formatting steps:%n",
51+
subcommandsRepeatable = true,
52+
subcommands = {LicenseHeader.class, GoogleJavaFormat.class, Prettier.class})
53+
public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider {
54+
55+
@CommandLine.Spec
56+
CommandLine.Model.CommandSpec spec; // injected by picocli
57+
58+
@CommandLine.Option(
59+
names = {"--mode", "-m"},
60+
defaultValue = "APPLY",
61+
description = "The mode to run spotless in." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX)
62+
SpotlessMode spotlessMode;
63+
64+
@CommandLine.Option(
65+
names = {"--basedir"},
66+
hidden = true,
67+
description = "The base directory to run spotless in. Intended for testing purposes only.")
68+
Path baseDir;
69+
70+
@CommandLine.Option(
71+
names = {"--target", "-t"},
72+
description = "The target files to format.")
73+
public List<String> targets;
74+
75+
@CommandLine.Option(
76+
names = {"--encoding", "-e"},
77+
defaultValue = "UTF-8",
78+
description = "The encoding of the files to format." + OptionConstants.DEFAULT_VALUE_SUFFIX)
79+
public Charset encoding;
80+
81+
@CommandLine.Option(
82+
names = {"--line-ending", "-l"},
83+
defaultValue = "UNIX",
84+
description = "The line ending of the files to format." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX)
85+
public LineEnding lineEnding;
86+
87+
@Override
88+
public Integer executeSpotlessAction(@NotNull List<FormatterStep> formatterSteps) {
89+
validateTargets();
90+
TargetResolver targetResolver = targetResolver();
91+
92+
try (Formatter formatter = Formatter.builder()
93+
.lineEndingsPolicy(lineEnding.createPolicy())
94+
.encoding(encoding)
95+
.steps(formatterSteps)
96+
.build()) {
97+
98+
ResultType resultType = targetResolver
99+
.resolveTargets()
100+
.parallel()
101+
.peek(path -> System.out.printf(
102+
"%s: formatting %s%n", Thread.currentThread().getName(), path))
103+
.map(path -> ThrowingEx.get(() -> new Result(
104+
path,
105+
LintState.of(formatter, path.toFile())))) // TODO handle suppressions, see SpotlessTaskImpl
106+
.map(result -> this.handleResult(formatter, result))
107+
.reduce(ResultType.CLEAN, ResultType::combineWith);
108+
return spotlessMode.translateResultTypeToExitCode(resultType);
109+
}
110+
}
111+
112+
private void validateTargets() {
113+
if (targets == null || targets.isEmpty()) { // cannot use `required = true` because of the subcommands
114+
throw new CommandLine.ParameterException(
115+
spec.commandLine(),
116+
"Error: Missing required argument (specify one of these): (--target=<targets> | -t)");
117+
}
118+
}
119+
120+
private ResultType handleResult(Formatter formatter, Result result) {
121+
if (result.lintState.isClean()) {
122+
// System.out.println("File is clean: " + result.target.toFile().getName());
123+
return ResultType.CLEAN;
124+
}
125+
if (result.lintState.getDirtyState().didNotConverge()) {
126+
System.err.println("File did not converge: "
127+
+ result.target.toFile().getName()); // TODO: where to print the output to?
128+
return ResultType.DID_NOT_CONVERGE;
129+
}
130+
return this.spotlessMode.handleResult(formatter, result);
131+
}
132+
133+
private TargetResolver targetResolver() {
134+
return new TargetResolver(baseDir(), targets);
135+
}
136+
137+
private Path baseDir() {
138+
return baseDir == null ? Path.of(System.getProperty("user.dir")) : baseDir;
139+
}
140+
141+
@Override
142+
public SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream) {
143+
validateTargets();
144+
TargetResolver targetResolver = targetResolver();
145+
TargetFileTypeInferer targetFileTypeInferer = new TargetFileTypeInferer(targetResolver);
146+
return SpotlessActionContext.builder()
147+
.targetFileType(targetFileTypeInferer.inferTargetFileType())
148+
.fileResolver(new FileResolver(baseDir()))
149+
.commandLineStream(commandLineStream)
150+
.build();
151+
}
152+
153+
public static void main(String... args) {
154+
if (args.length == 0) {
155+
// args = new String[]{"--version"};
156+
// args = new String[]{"license-header", "--header-file", "CHANGES.md", "--delimiter-for", "java",
157+
// "license-header", "--header", "abc"};
158+
159+
// args = new String[]{"--mode=CHECK", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8",
160+
// "license-header", "--header", "abc", "license-header", "--header-file", "TestHeader.txt"};
161+
args = new String[] {
162+
"--basedir", "cli", "--target", "src/poc/java/**/*.java", "--encoding=UTF-8", "google-java-format"
163+
};
164+
// args = new String[]{"--version"};
165+
}
166+
int exitCode = createCommandLine(createInstance()).execute(args);
167+
System.exit(exitCode);
168+
}
169+
170+
static SpotlessCLI createInstance() {
171+
return new SpotlessCLI();
172+
}
173+
174+
static CommandLine createCommandLine(SpotlessCLI spotlessCLI) {
175+
return new CommandLine(spotlessCLI)
176+
.setExecutionStrategy(new SpotlessExecutionStrategy())
177+
.setCaseInsensitiveEnumValuesAllowed(true);
178+
}
179+
180+
private enum SpotlessMode {
181+
CHECK {
182+
@Override
183+
ResultType handleResult(Formatter formatter, Result result) {
184+
if (result.lintState.isHasLints()) {
185+
result.lintState.asStringOneLine(result.target.toFile(), formatter);
186+
} else {
187+
System.out.println(String.format("%s is violating formatting rules.", result.target));
188+
}
189+
return ResultType.DIRTY;
190+
}
191+
192+
@Override
193+
Integer translateResultTypeToExitCode(ResultType resultType) {
194+
if (resultType == ResultType.CLEAN) {
195+
return 0;
196+
}
197+
if (resultType == ResultType.DIRTY) {
198+
return 1;
199+
}
200+
if (resultType == ResultType.DID_NOT_CONVERGE) {
201+
return -1;
202+
}
203+
throw new IllegalStateException("Unexpected result type: " + resultType);
204+
}
205+
},
206+
APPLY {
207+
@Override
208+
ResultType handleResult(Formatter formatter, Result result) {
209+
if (result.lintState.isHasLints()) {
210+
// something went wrong, we should not apply the changes
211+
System.err.println(
212+
"File has lints: " + result.target.toFile().getName());
213+
System.err.println(
214+
"lint:\n" + result.lintState.asStringDetailed(result.target.toFile(), formatter));
215+
return ResultType.DIRTY;
216+
}
217+
ThrowingEx.run(() -> result.lintState.getDirtyState().writeCanonicalTo(result.target.toFile()));
218+
return ResultType.CLEAN;
219+
}
220+
221+
@Override
222+
Integer translateResultTypeToExitCode(ResultType resultType) {
223+
if (resultType == ResultType.CLEAN) {
224+
return 0;
225+
}
226+
if (resultType == ResultType.DIRTY) {
227+
return 0;
228+
}
229+
if (resultType == ResultType.DID_NOT_CONVERGE) {
230+
return -1;
231+
}
232+
throw new IllegalStateException("Unexpected result type: " + resultType);
233+
}
234+
};
235+
236+
abstract ResultType handleResult(Formatter formatter, Result result);
237+
238+
abstract Integer translateResultTypeToExitCode(ResultType resultType);
239+
}
240+
241+
private enum ResultType {
242+
CLEAN,
243+
DIRTY,
244+
DID_NOT_CONVERGE;
245+
246+
ResultType combineWith(ResultType other) {
247+
if (this == other) {
248+
return this;
249+
}
250+
if (this == DID_NOT_CONVERGE || other == DID_NOT_CONVERGE) {
251+
return DID_NOT_CONVERGE;
252+
}
253+
if (this == DIRTY || other == DIRTY) {
254+
return DIRTY;
255+
}
256+
throw new IllegalStateException("Unexpected combination of result types: " + this + " and " + other);
257+
}
258+
}
259+
260+
private static final class Result {
261+
private final Path target;
262+
private final LintState lintState;
263+
264+
public Result(Path target, LintState lintState) {
265+
this.target = target;
266+
this.lintState = lintState;
267+
}
268+
}
269+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.cli;
17+
18+
public interface SpotlessCommand {}

0 commit comments

Comments
 (0)