Skip to content

Commit b2cbba2

Browse files
authored
Linting - API (#2149)
2 parents b307c97 + 14cbc52 commit b2cbba2

32 files changed

+748
-207
lines changed

CHANGES.md

+8
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1111

1212
## [Unreleased]
1313
### Added
14+
* APIs to support linting. (implemented in [#2148](https://github.com/diffplug/spotless/pull/2148) and [#2149](https://github.com/diffplug/spotless/pull/2149))
15+
* Spotless is still primarily a formatter, not a linter. But when formatting fails, it's more flexible to model those failures as lints so that the formatting can continue and ideally we can also capture the line numbers causing the failure.
16+
* `Lint` models a single change. A `FormatterStep` can create a lint by:
17+
* throwing an exception during formatting, ideally `throw Lint.atLine(127, "code", "Well what happened was...")`
18+
* or by implementing the `List<Lint> lint(String content, File file)` method to create multiple of them
1419
* Support for line ending policy `PRESERVE` which just takes the first line ending of every given file as setting (no matter if `\n`, `\r\n` or `\r`) ([#2304](https://github.com/diffplug/spotless/pull/2304))
20+
### Changes
21+
* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148))
22+
* **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`.
1523
### Fixed
1624
* `ktlint` steps now read from the `string` instead of the `file` so they don't clobber earlier steps. (fixes [#1599](https://github.com/diffplug/spotless/issues/1599))
1725

CONTRIBUTING.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ Here's a checklist for creating a new step for Spotless:
9393
- [ ] Class name ends in Step, `SomeNewStep`.
9494
- [ ] Class has a public static method named `create` that returns a `FormatterStep`.
9595
- [ ] Has a test class named `SomeNewStepTest` that uses `StepHarness` or `StepHarnessWithFile` to test the step.
96-
- [ ] Start with `StepHarness.forStep(myStep).supportsRoundTrip(false)`, and then add round trip support as described in the next section.
9796
- [ ] Test class has test methods to verify behavior.
9897
- [ ] Test class has a test method `equality()` which tests equality using `StepEqualityTester` (see existing methods for examples).
9998

@@ -137,6 +136,15 @@ There are many great formatters (prettier, clang-format, black, etc.) which live
137136

138137
Because of Spotless' up-to-date checking and [git ratcheting](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet), Spotless actually doesn't have to call formatters very often, so even an expensive shell call for every single invocation isn't that bad. Anything that works is better than nothing, and we can always speed things up later if it feels too slow (but it probably won't).
139138

139+
## Lints
140+
141+
Spotless is primarily a formatter, not a linter. But, if something goes wrong during formatting, it's better to model that as a lint with line numbers rather than just a naked exception. There are two ways to go about this:
142+
143+
- at any point during the formatting process, you can throw a `Lint.atLine(int line, ...)` exception. This will be caught and turned into a lint.
144+
- or you can override the `default List<Lint> lint(String content, File file)` method. This method will only run if the step did not already throw an exception.
145+
146+
Don't go lint crazy! By default, all lints are build failures. Users have to suppress them explicitly if they want to continue.
147+
140148
## How to add a new plugin for a build system
141149

142150
The gist of it is that you will have to:

gradle.properties

+1
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ VER_JGIT=6.10.0.202406032230-r
3232
VER_JUNIT=5.11.2
3333
VER_ASSERTJ=3.26.3
3434
VER_MOCKITO=5.14.2
35+
VER_SELFIE=2.4.1

gradle/special-tests.gradle

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ tasks.withType(Test).configureEach {
1818
maxFailures = 10
1919
}
2020
}
21-
21+
// selfie https://selfie.dev/jvm/get-started#gradle
22+
environment project.properties.subMap([
23+
"selfie"
24+
]) // optional, see "Overwrite everything" below
25+
inputs.files(fileTree("src/test") {
26+
// optional, improves up-to-date checking
27+
include "**/*.ss"
28+
})
2229
// https://docs.gradle.org/8.8/userguide/performance.html#execute_tests_in_parallel
2330
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
2431
}

lib/src/main/java/com/diffplug/spotless/DirtyState.java

+15-5
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public boolean didNotConverge() {
4646
return this == didNotConverge;
4747
}
4848

49-
private byte[] canonicalBytes() {
49+
byte[] canonicalBytes() {
5050
if (canonicalBytes == null) {
5151
throw new IllegalStateException("First make sure that {@code !isClean()} and {@code !didNotConverge()}");
5252
}
@@ -74,7 +74,17 @@ public static DirtyState of(Formatter formatter, File file) throws IOException {
7474
}
7575

7676
public static DirtyState of(Formatter formatter, File file, byte[] rawBytes) {
77-
String raw = new String(rawBytes, formatter.getEncoding());
77+
return of(formatter, file, rawBytes, new String(rawBytes, formatter.getEncoding()));
78+
}
79+
80+
public static DirtyState of(Formatter formatter, File file, byte[] rawBytes, String raw) {
81+
var valuePerStep = new ValuePerStep<Throwable>(formatter);
82+
DirtyState state = of(formatter, file, rawBytes, raw, valuePerStep);
83+
LintPolicy.legacyBehavior(formatter, file, valuePerStep);
84+
return state;
85+
}
86+
87+
static DirtyState of(Formatter formatter, File file, byte[] rawBytes, String raw, ValuePerStep<Throwable> exceptionPerStep) {
7888
// check that all characters were encodable
7989
String encodingError = EncodingErrorMsg.msg(raw, rawBytes, formatter.getEncoding());
8090
if (encodingError != null) {
@@ -84,7 +94,7 @@ public static DirtyState of(Formatter formatter, File file, byte[] rawBytes) {
8494
String rawUnix = LineEnding.toUnix(raw);
8595

8696
// enforce the format
87-
String formattedUnix = formatter.compute(rawUnix, file);
97+
String formattedUnix = formatter.computeWithLint(rawUnix, file, exceptionPerStep);
8898
// convert the line endings if necessary
8999
String formatted = formatter.computeLineEndings(formattedUnix, file);
90100

@@ -95,13 +105,13 @@ public static DirtyState of(Formatter formatter, File file, byte[] rawBytes) {
95105
}
96106

97107
// F(input) != input, so we'll do a padded check
98-
String doubleFormattedUnix = formatter.compute(formattedUnix, file);
108+
String doubleFormattedUnix = formatter.computeWithLint(formattedUnix, file, exceptionPerStep);
99109
if (doubleFormattedUnix.equals(formattedUnix)) {
100110
// most dirty files are idempotent-dirty, so this is a quick-short circuit for that common case
101111
return new DirtyState(formattedBytes);
102112
}
103113

104-
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix);
114+
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix, exceptionPerStep);
105115
if (!cell.isResolvable()) {
106116
return didNotConverge;
107117
}

lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 DiffPlug
2+
* Copyright 2016-2024 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
1616
package com.diffplug.spotless;
1717

1818
import java.io.File;
19+
import java.util.List;
1920
import java.util.Objects;
20-
import java.util.regex.Matcher;
2121
import java.util.regex.Pattern;
2222

2323
import javax.annotation.Nullable;
@@ -36,14 +36,24 @@ final class FilterByContentPatternFormatterStep extends DelegateFormatterStep {
3636
public @Nullable String format(String raw, File file) throws Exception {
3737
Objects.requireNonNull(raw, "raw");
3838
Objects.requireNonNull(file, "file");
39-
Matcher matcher = contentPattern.matcher(raw);
40-
if (matcher.find() == (onMatch == OnMatch.INCLUDE)) {
39+
if (contentPattern.matcher(raw).find() == (onMatch == OnMatch.INCLUDE)) {
4140
return delegateStep.format(raw, file);
4241
} else {
4342
return raw;
4443
}
4544
}
4645

46+
@Override
47+
public List<Lint> lint(String raw, File file) throws Exception {
48+
Objects.requireNonNull(raw, "raw");
49+
Objects.requireNonNull(file, "file");
50+
if (contentPattern.matcher(raw).find() == (onMatch == OnMatch.INCLUDE)) {
51+
return delegateStep.lint(raw, file);
52+
} else {
53+
return List.of();
54+
}
55+
}
56+
4757
@Override
4858
public boolean equals(Object o) {
4959
if (this == o) {

lib/src/main/java/com/diffplug/spotless/FilterByFileFormatterStep.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2022 DiffPlug
2+
* Copyright 2016-2024 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616
package com.diffplug.spotless;
1717

1818
import java.io.File;
19+
import java.util.List;
1920
import java.util.Objects;
2021

2122
import javax.annotation.Nullable;
@@ -39,6 +40,17 @@ final class FilterByFileFormatterStep extends DelegateFormatterStep {
3940
}
4041
}
4142

43+
@Override
44+
public List<Lint> lint(String content, File file) throws Exception {
45+
Objects.requireNonNull(content, "content");
46+
Objects.requireNonNull(file, "file");
47+
if (filter.accept(file)) {
48+
return delegateStep.lint(content, file);
49+
} else {
50+
return List.of();
51+
}
52+
}
53+
4254
@Override
4355
public boolean equals(Object o) {
4456
if (this == o) {

lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicy.java

-39
This file was deleted.

lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyStrict.java

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2024 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@
2323
* A policy for handling exceptions in the format. Any exceptions will
2424
* halt the build except for a specifically excluded path or step.
2525
*/
26-
public class FormatExceptionPolicyStrict extends NoLambda.EqualityBasedOnSerialization implements FormatExceptionPolicy {
26+
public class FormatExceptionPolicyStrict extends NoLambda.EqualityBasedOnSerialization {
2727
private static final long serialVersionUID = 1L;
2828

2929
private final Set<String> excludeSteps = new TreeSet<>();
@@ -39,18 +39,17 @@ public void excludePath(String relativePath) {
3939
excludePaths.add(Objects.requireNonNull(relativePath));
4040
}
4141

42-
@Override
4342
public void handleError(Throwable e, FormatterStep step, String relativePath) {
4443
Objects.requireNonNull(e, "e");
4544
Objects.requireNonNull(step, "step");
4645
Objects.requireNonNull(relativePath, "relativePath");
4746
if (excludeSteps.contains(step.getName())) {
48-
FormatExceptionPolicyLegacy.warning(e, step, relativePath);
47+
LintPolicy.warning(e, step, relativePath);
4948
} else {
5049
if (excludePaths.contains(relativePath)) {
51-
FormatExceptionPolicyLegacy.warning(e, step, relativePath);
50+
LintPolicy.warning(e, step, relativePath);
5251
} else {
53-
FormatExceptionPolicyLegacy.error(e, step, relativePath);
52+
LintPolicy.error(e, step, relativePath);
5453
throw ThrowingEx.asRuntimeRethrowError(e);
5554
}
5655
}

lib/src/main/java/com/diffplug/spotless/Formatter.java

+31-8
Original file line numberDiff line numberDiff line change
@@ -128,28 +128,51 @@ public String computeLineEndings(String unix, File file) {
128128
* is guaranteed to also have unix line endings.
129129
*/
130130
public String compute(String unix, File file) {
131+
ValuePerStep<Throwable> exceptionPerStep = new ValuePerStep<>(this);
132+
String result = computeWithLint(unix, file, exceptionPerStep);
133+
LintPolicy.legacyBehavior(this, file, exceptionPerStep);
134+
return result;
135+
}
136+
137+
/**
138+
* Returns the result of calling all of the FormatterSteps, while also
139+
* tracking any exceptions which are thrown.
140+
* <p>
141+
* The input must have unix line endings, and the output
142+
* is guaranteed to also have unix line endings.
143+
* <p>
144+
* It doesn't matter what is inside `ValuePerStep`, the value at every index will be overwritten
145+
* when the method returns.
146+
*/
147+
String computeWithLint(String unix, File file, ValuePerStep<Throwable> exceptionPerStep) {
131148
Objects.requireNonNull(unix, "unix");
132149
Objects.requireNonNull(file, "file");
133150

134-
for (FormatterStep step : steps) {
151+
for (int i = 0; i < steps.size(); ++i) {
152+
FormatterStep step = steps.get(i);
153+
Throwable storeForStep;
135154
try {
136155
String formatted = step.format(unix, file);
137156
if (formatted == null) {
138157
// This probably means it was a step that only checks
139158
// for errors and doesn't actually have any fixes.
140159
// No exception was thrown so we can just continue.
160+
storeForStep = LintState.formatStepCausedNoChange();
141161
} else {
142162
// Should already be unix-only, but some steps might misbehave.
143-
unix = LineEnding.toUnix(formatted);
163+
String clean = LineEnding.toUnix(formatted);
164+
if (clean.equals(unix)) {
165+
storeForStep = LintState.formatStepCausedNoChange();
166+
} else {
167+
storeForStep = null;
168+
unix = LineEnding.toUnix(formatted);
169+
}
144170
}
145171
} catch (Throwable e) {
146-
// TODO: this is bad, but it won't matter when add support for linting
147-
if (e instanceof RuntimeException) {
148-
throw (RuntimeException) e;
149-
} else {
150-
throw new RuntimeException(e);
151-
}
172+
// store the exception which was thrown and keep going
173+
storeForStep = e;
152174
}
175+
exceptionPerStep.set(i, storeForStep);
153176
}
154177
return unix;
155178
}

lib/src/main/java/com/diffplug/spotless/FormatterFunc.java

+26
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.diffplug.spotless;
1717

1818
import java.io.File;
19+
import java.util.List;
1920
import java.util.Objects;
2021

2122
/**
@@ -32,6 +33,14 @@ default String apply(String unix, File file) throws Exception {
3233
return apply(unix);
3334
}
3435

36+
/**
37+
* Calculates a list of lints against the given content.
38+
* By default, that's just an throwables thrown by the lint.
39+
*/
40+
default List<Lint> lint(String content, File file) throws Exception {
41+
return List.of();
42+
}
43+
3544
/**
3645
* {@code Function<String, String>} and {@code BiFunction<String, File, String>} whose implementation
3746
* requires a resource which should be released when the function is no longer needed.
@@ -74,6 +83,14 @@ public String apply(String unix) throws Exception {
7483
@FunctionalInterface
7584
interface ResourceFunc<T extends AutoCloseable> {
7685
String apply(T resource, String unix) throws Exception;
86+
87+
/**
88+
* Calculates a list of lints against the given content.
89+
* By default, that's just an throwables thrown by the lint.
90+
*/
91+
default List<Lint> lint(T resource, String unix) throws Exception {
92+
return List.of();
93+
}
7794
}
7895

7996
/** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the format function. */
@@ -101,6 +118,10 @@ public String apply(String unix) throws Exception {
101118
@FunctionalInterface
102119
interface ResourceFuncNeedsFile<T extends AutoCloseable> {
103120
String apply(T resource, String unix, File file) throws Exception;
121+
122+
default List<Lint> lint(T resource, String content, File file) throws Exception {
123+
return List.of();
124+
}
104125
}
105126

106127
/** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the file-dependent format function. */
@@ -123,6 +144,11 @@ public String apply(String unix, File file) throws Exception {
123144
public String apply(String unix) throws Exception {
124145
return apply(unix, Formatter.NO_FILE_SENTINEL);
125146
}
147+
148+
@Override
149+
public List<Lint> lint(String content, File file) throws Exception {
150+
return function.lint(resource, content, file);
151+
}
126152
};
127153
}
128154
}

0 commit comments

Comments
 (0)