Skip to content

Commit 9564bf2

Browse files
committed
feat: add support for prettier calling from cli
1 parent 83dbeb5 commit 9564bf2

File tree

15 files changed

+990
-37
lines changed

15 files changed

+990
-37
lines changed

cli/build.gradle

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ compileJava {
5454
// https://github.com/remkop/picocli/tree/main/picocli-codegen#222-other-options
5555
options.compilerArgs += [
5656
"-Aproject=${project.group}/${project.name}",
57-
"-Aother.resource.bundles=application"
57+
"-Aother.resource.bundles=application",
58+
// patterns require double-escaping (one escape is removed by groovy, the other one is needed in the resulting json file)
59+
"-Aother.resource.patterns=.*\\\\.properties,.*\\\\.json,.*\\\\.js"
5860
]
5961
}
6062

@@ -100,7 +102,7 @@ def nativeCompileMetaDir = project.layout.buildDirectory.dir('nativeCompile/src/
100102
// use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image
101103
graalvmNative {
102104
agent {
103-
enabled = true
105+
enabled = true // we would love to make this dynamic, but it's not possible
104106
defaultMode = "standard"
105107
metadataCopy {
106108
inputTaskNames.add('test')
@@ -110,8 +112,11 @@ graalvmNative {
110112
tasksToInstrumentPredicate = new java.util.function.Predicate<Task>() {
111113
@Override
112114
boolean test(Task task) {
113-
println ("Instrumenting task: " + task.name + " " + task.name == 'test')
115+
// if (project.hasProperty('agent')) {
116+
println ("Instrumenting task: " + task.name + " " + task.name == 'test' + "proj: " + task.project.hasProperty('agent'))
114117
return task.name == 'test'
118+
// }
119+
// return false
115120
}
116121
}
117122
}
@@ -159,16 +164,18 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
159164
}
160165
}
161166

162-
gradle.taskGraph.whenReady { graph ->
163-
if (graph.hasTask('nativeCompile') || graph.hasTask('metadataCopy') || graph.hasTask('shadowJar')) {
167+
gradle.taskGraph.whenReady { TaskExecutionGraph graph ->
168+
// println "Graph: " + graph.allTasks*.name
169+
if (graph.hasTask(':cli:nativeCompile') || graph.hasTask(':cli:metadataCopy') || graph.hasTask(':cli:shadowJar')) {
164170
// enable graalvm agent using property here instead of command line `-Pagent=standard`
165171
// this collects information about reflective access and resources used by the application (e.g. GJF)
166-
project.property('agent', 'standard')
172+
project.ext.agent = 'standard'
167173
}
168174
}
169175

170176
tasks.withType(Test).configureEach {
171177
if (it.name == 'test') {
178+
it.outputs.dir(nativeCompileMetaDir)
172179
if (project.hasProperty('agent')) {
173180
it.inputs.property('agent', project.property('agent')) // make sure to re-run tests if agent changes
174181
}

cli/src/main/java/com/diffplug/spotless/cli/SpotlessActionContextProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
package com.diffplug.spotless.cli;
1717

1818
import com.diffplug.spotless.cli.core.SpotlessActionContext;
19+
import com.diffplug.spotless.cli.core.SpotlessCommandLineStream;
1920

2021
public interface SpotlessActionContextProvider {
2122

22-
SpotlessActionContext spotlessActionContext();
23+
SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream);
2324
}

cli/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,23 @@
2828
import com.diffplug.spotless.ThrowingEx;
2929
import com.diffplug.spotless.cli.core.FileResolver;
3030
import com.diffplug.spotless.cli.core.SpotlessActionContext;
31+
import com.diffplug.spotless.cli.core.SpotlessCommandLineStream;
3132
import com.diffplug.spotless.cli.core.TargetFileTypeInferer;
3233
import com.diffplug.spotless.cli.core.TargetResolver;
3334
import com.diffplug.spotless.cli.execution.SpotlessExecutionStrategy;
3435
import com.diffplug.spotless.cli.help.OptionConstants;
3536
import com.diffplug.spotless.cli.steps.GoogleJavaFormat;
3637
import com.diffplug.spotless.cli.steps.LicenseHeader;
38+
import com.diffplug.spotless.cli.steps.Prettier;
3739
import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider;
3840

3941
import picocli.CommandLine;
4042
import picocli.CommandLine.Command;
4143

4244
@Command(name = "spotless", mixinStandardHelpOptions = true, versionProvider = SpotlessCLIVersionProvider.class, description = "Runs spotless", synopsisSubcommandLabel = "[FORMATTING_STEPS]", commandListHeading = "%nAvailable formatting steps:%n", subcommandsRepeatable = true, subcommands = {
4345
LicenseHeader.class,
44-
GoogleJavaFormat.class})
46+
GoogleJavaFormat.class,
47+
Prettier.class})
4548
public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider {
4649

4750
@CommandLine.Spec
@@ -110,11 +113,15 @@ private Path baseDir() {
110113
}
111114

112115
@Override
113-
public SpotlessActionContext spotlessActionContext() {
116+
public SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream) {
114117
validateTargets();
115118
TargetResolver targetResolver = targetResolver();
116119
TargetFileTypeInferer targetFileTypeInferer = new TargetFileTypeInferer(targetResolver);
117-
return new SpotlessActionContext(targetFileTypeInferer.inferTargetFileType(), new FileResolver(baseDir()));
120+
return SpotlessActionContext.builder()
121+
.targetFileType(targetFileTypeInferer.inferTargetFileType())
122+
.fileResolver(new FileResolver(baseDir()))
123+
.commandLineStream(commandLineStream)
124+
.build();
118125
}
119126

120127
public static void main(String... args) {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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.core;
17+
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.OutputStream;
20+
import java.lang.reflect.Field;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.Objects;
26+
import java.util.stream.Stream;
27+
28+
import com.diffplug.common.hash.Hashing;
29+
import com.diffplug.spotless.ThrowingEx;
30+
import com.diffplug.spotless.cli.SpotlessAction;
31+
import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep;
32+
33+
import picocli.CommandLine;
34+
35+
public class ChecksumCalculator {
36+
37+
public String calculateChecksum(SpotlessCLIFormatterStep step) {
38+
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
39+
writeObjectDataTo(step, out);
40+
return toHashedHexBytes(out.toByteArray());
41+
} catch (Exception e) {
42+
throw ThrowingEx.asRuntime(e);
43+
}
44+
}
45+
46+
private void writeObjectDataTo(Object object, OutputStream outputStream) {
47+
ThrowingEx.run(() -> outputStream.write(object.getClass().getName().getBytes(StandardCharsets.UTF_8)));
48+
options(object)
49+
.map(Object::toString)
50+
.map(str -> str.getBytes(StandardCharsets.UTF_8))
51+
.forEachOrdered(bytes -> ThrowingEx.run(() -> outputStream.write(bytes)));
52+
53+
}
54+
55+
public String calculateChecksum(SpotlessCommandLineStream commandLineStream) {
56+
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
57+
58+
calculateChecksumOfActions(commandLineStream.actions(), out);
59+
60+
calculateChecksumOfSteps(commandLineStream.formatterSteps(), out);
61+
return toHashedHexBytes(out.toByteArray());
62+
} catch (Exception e) {
63+
throw ThrowingEx.asRuntime(e);
64+
}
65+
}
66+
67+
private void calculateChecksumOfSteps(Stream<SpotlessCLIFormatterStep> spotlessCLIFormatterStepStream, ByteArrayOutputStream out) {
68+
spotlessCLIFormatterStepStream.forEachOrdered(step -> writeObjectDataTo(step, out));
69+
}
70+
71+
private void calculateChecksumOfActions(Stream<SpotlessAction> actions, ByteArrayOutputStream out) {
72+
actions.forEachOrdered(action -> writeObjectDataTo(action, out));
73+
}
74+
75+
private static Stream<Object> options(Object step) {
76+
List<Class<?>> classHierarchy = classHierarchy(step);
77+
return classHierarchy.stream()
78+
.flatMap(clazz -> Arrays.stream(clazz.getDeclaredFields()))
79+
.flatMap(field -> expandOptionField(field, step))
80+
.map(FieldOnObject::getValue)
81+
.filter(Objects::nonNull);
82+
}
83+
84+
private static List<Class<?>> classHierarchy(Object obj) {
85+
List<Class<?>> hierarchy = new ArrayList<>();
86+
Class<?> clazz = obj.getClass();
87+
while (clazz != null) {
88+
hierarchy.add(clazz);
89+
clazz = clazz.getSuperclass();
90+
}
91+
return hierarchy;
92+
}
93+
94+
private static Stream<FieldOnObject> expandOptionField(Field field, Object obj) {
95+
if (field.isAnnotationPresent(CommandLine.Option.class) || field.isAnnotationPresent(CommandLine.Parameters.class)) {
96+
return Stream.of(new FieldOnObject(field, obj));
97+
}
98+
if (field.isAnnotationPresent(CommandLine.ArgGroup.class)) {
99+
Object fieldValue = new FieldOnObject(field, obj).getValue();
100+
return Arrays.stream(fieldValue.getClass().getDeclaredFields())
101+
.flatMap(subField -> expandOptionField(subField, fieldValue));
102+
}
103+
return Stream.empty(); // nothing to expand
104+
}
105+
106+
private static String toHashedHexBytes(byte[] bytes) {
107+
byte[] hash = Hashing.murmur3_128().hashBytes(bytes).asBytes();
108+
StringBuilder builder = new StringBuilder();
109+
for (byte b : hash) {
110+
builder.append(String.format("%02x", b));
111+
}
112+
return builder.toString();
113+
}
114+
115+
private static class FieldOnObject {
116+
private final Field field;
117+
private final Object obj;
118+
119+
FieldOnObject(Field field, Object obj) {
120+
this.field = field;
121+
this.obj = obj;
122+
}
123+
124+
Object getValue() {
125+
ThrowingEx.run(() -> field.setAccessible(true));
126+
return ThrowingEx.get(() -> field.get(obj));
127+
}
128+
}
129+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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.core;
17+
18+
import java.nio.file.Path;
19+
import java.nio.file.Paths;
20+
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.Optional;
23+
24+
import javax.annotation.Nonnull;
25+
26+
import com.diffplug.spotless.cli.steps.BuildDirGloballyReusable;
27+
import com.diffplug.spotless.cli.steps.SpotlessCLIFormatterStep;
28+
29+
public class ExecutionLayout {
30+
31+
private final FileResolver fileResolver;
32+
private final SpotlessCommandLineStream commandLineStream;
33+
private final ChecksumCalculator checksumCalculator;
34+
35+
private ExecutionLayout(@Nonnull FileResolver fileResolver, SpotlessCommandLineStream commandLineStream) {
36+
this.fileResolver = Objects.requireNonNull(fileResolver);
37+
this.commandLineStream = commandLineStream;
38+
this.checksumCalculator = new ChecksumCalculator();
39+
}
40+
41+
public static ExecutionLayout create(FileResolver fileResolver, SpotlessCommandLineStream commandLineStream) {
42+
return new ExecutionLayout(fileResolver, commandLineStream);
43+
}
44+
45+
public Optional<Path> find(Path searchPath) {
46+
Path found = fileResolver.resolvePath(searchPath);
47+
if (found.toFile().canRead()) {
48+
return Optional.of(found);
49+
}
50+
if (searchPath.toFile().canRead()) {
51+
return Optional.of(searchPath);
52+
}
53+
return Optional.empty();
54+
}
55+
56+
public Path baseDir() {
57+
return fileResolver.baseDir();
58+
}
59+
60+
public Path buildDir() {
61+
// gradle?
62+
if (isGradleDirectory()) {
63+
return gradleBuildDir();
64+
}
65+
if (isMavenDirectory()) {
66+
return mavenBuildDir();
67+
}
68+
return tempBuildDir();
69+
}
70+
71+
private boolean isGradleDirectory() {
72+
return List.of("build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts").stream()
73+
.map(Paths::get)
74+
.map(this::find)
75+
.anyMatch(Optional::isPresent);
76+
}
77+
78+
private Path gradleBuildDir() {
79+
return fileResolver.resolvePath(Paths.get("build", "spotless-cli"));
80+
}
81+
82+
private boolean isMavenDirectory() {
83+
return List.of("pom.xml").stream()
84+
.map(Paths::get)
85+
.map(this::find)
86+
.anyMatch(Optional::isPresent);
87+
}
88+
89+
private Path mavenBuildDir() {
90+
return fileResolver.resolvePath(Paths.get("target", "spotless-cli"));
91+
}
92+
93+
private Path tempBuildDir() {
94+
String tmpDir = System.getProperty("java.io.tmpdir");
95+
return Path.of(tmpDir, "spotless-cli");
96+
}
97+
98+
public Path buildDirFor(@Nonnull SpotlessCLIFormatterStep step) {
99+
Objects.requireNonNull(step);
100+
Path buildDir = buildDir();
101+
String checksum = checksumCalculator.calculateChecksum(step);
102+
if (step instanceof BuildDirGloballyReusable) {
103+
return buildDir.resolve(checksum);
104+
}
105+
String commandLineChecksum = checksumCalculator.calculateChecksum(commandLineStream);
106+
return buildDir.resolve(checksum + "-" + commandLineChecksum);
107+
}
108+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.core;
17+
18+
import java.io.File;
19+
import java.nio.file.Path;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
24+
public final class FilePathUtil {
25+
26+
private FilePathUtil() {
27+
// no instance
28+
}
29+
30+
public static File asFile(Path path) {
31+
return path == null ? null : path.toFile();
32+
}
33+
34+
public static List<File> asFiles(List<Path> paths) {
35+
return paths == null ? null : paths.stream().map(Path::toFile).collect(Collectors.toList());
36+
}
37+
38+
public static List<Boolean> assertDirectoryExists(File... files) {
39+
return assertDirectoryExists(Arrays.asList(files));
40+
}
41+
42+
public static List<Boolean> assertDirectoryExists(List<File> files) {
43+
return files.stream().map(f -> f != null && f.mkdirs()).collect(Collectors.toList());
44+
}
45+
}

0 commit comments

Comments
 (0)