Skip to content

Commit 1f41cb2

Browse files
authored
Adding ESLint as formatter step (#1453)
2 parents 6865449 + 0227e89 commit 1f41cb2

File tree

87 files changed

+2900
-259
lines changed

Some content is hidden

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

87 files changed

+2900
-259
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1616
* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441)).
1717
### Fixed
1818
* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444))
19+
* Added support for npm-based [ESLint](https://eslint.org/)-formatter for javascript and typescript ([#1453](https://github.com/diffplug/spotless/pull/1453))
1920

2021
### Changes
22+
* Bump default version for `prettier` from `2.0.5` to `2.8.1` ([#1453](https://github.com/diffplug/spotless/pull/1453))
2123
* Bump the dev version of Gradle from `7.5.1` to `7.6` ([#1409](https://github.com/diffplug/spotless/pull/1409))
2224
* We also removed the no-longer-required dependency `org.codehaus.groovy:groovy-xml`
2325
* Breaking changes to Spotless' internal testing infrastructure `testlib` ([#1443](https://github.com/diffplug/spotless/pull/1443))
@@ -29,6 +31,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
2931
* Switch our publishing infrastructure from CircleCI to GitHub Actions ([#1462](https://github.com/diffplug/spotless/pull/1462)).
3032
* Help wanted for moving our tests too ([#1472](https://github.com/diffplug/spotless/issues/1472))
3133

34+
3235
## [2.31.1] - 2023-01-02
3336
### Fixed
3437
* Improve memory usage when using git ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426))

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}}
6969
lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7070
lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
7171
lib('markdown.FlexmarkStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
72+
lib('npm.EslintFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7273
lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7374
lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7475
lib('pom.SortPomStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
@@ -113,6 +114,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
113114
| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
114115
| [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
115116
| [`markdown.FlexmarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
117+
| [`npm.EslintFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
116118
| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
117119
| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
118120
| [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |

lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 DiffPlug
2+
* Copyright 2020-2023 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.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2016-2023 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.npm;
17+
18+
abstract class BaseNpmRestService {
19+
20+
protected final SimpleRestClient restClient;
21+
22+
BaseNpmRestService(String baseUrl) {
23+
this.restClient = SimpleRestClient.forBaseUrl(baseUrl);
24+
}
25+
26+
public String shutdown() {
27+
return restClient.post("/shutdown");
28+
}
29+
30+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2016-2023 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.npm;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.Serializable;
21+
22+
import javax.annotation.Nullable;
23+
24+
import com.diffplug.spotless.FileSignature;
25+
import com.diffplug.spotless.ThrowingEx;
26+
27+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28+
29+
public class EslintConfig implements Serializable {
30+
31+
private static final long serialVersionUID = -6196834313082791248L;
32+
33+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
34+
@Nullable
35+
private final transient File eslintConfigPath;
36+
37+
@SuppressWarnings("unused")
38+
private final FileSignature eslintConfigPathSignature;
39+
40+
private final String eslintConfigJs;
41+
42+
public EslintConfig(@Nullable File eslintConfigPath, @Nullable String eslintConfigJs) {
43+
try {
44+
this.eslintConfigPath = eslintConfigPath;
45+
this.eslintConfigPathSignature = eslintConfigPath != null ? FileSignature.signAsList(this.eslintConfigPath) : FileSignature.signAsList();
46+
this.eslintConfigJs = eslintConfigJs;
47+
} catch (IOException e) {
48+
throw ThrowingEx.asRuntime(e);
49+
}
50+
}
51+
52+
public EslintConfig withEslintConfigPath(@Nullable File eslintConfigPath) {
53+
return new EslintConfig(eslintConfigPath, this.eslintConfigJs);
54+
}
55+
56+
@Nullable
57+
public File getEslintConfigPath() {
58+
return eslintConfigPath;
59+
}
60+
61+
@Nullable
62+
public String getEslintConfigJs() {
63+
return eslintConfigJs;
64+
}
65+
66+
public EslintConfig verify() {
67+
if (eslintConfigPath == null && eslintConfigJs == null) {
68+
throw new IllegalArgumentException("ESLint must be configured using either a configFile or a configJs - but both are null.");
69+
}
70+
return this;
71+
}
72+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2016-2023 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.npm;
17+
18+
import static java.util.Objects.requireNonNull;
19+
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.io.Serializable;
23+
import java.util.Collections;
24+
import java.util.HashMap;
25+
import java.util.LinkedHashMap;
26+
import java.util.Map;
27+
import java.util.Objects;
28+
import java.util.TreeMap;
29+
30+
import javax.annotation.Nonnull;
31+
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
import com.diffplug.spotless.FormatterFunc;
36+
import com.diffplug.spotless.FormatterFunc.Closeable;
37+
import com.diffplug.spotless.FormatterStep;
38+
import com.diffplug.spotless.Provisioner;
39+
import com.diffplug.spotless.ThrowingEx;
40+
import com.diffplug.spotless.npm.EslintRestService.FormatOption;
41+
42+
public class EslintFormatterStep {
43+
44+
private static final Logger logger = LoggerFactory.getLogger(EslintFormatterStep.class);
45+
46+
public static final String NAME = "eslint-format";
47+
48+
public static final String DEFAULT_ESLINT_VERSION = "^8.31.0";
49+
50+
public static Map<String, String> defaultDevDependenciesForTypescript() {
51+
return defaultDevDependenciesTypescriptWithEslint(DEFAULT_ESLINT_VERSION);
52+
}
53+
54+
public static Map<String, String> defaultDevDependenciesTypescriptWithEslint(String eslintVersion) {
55+
Map<String, String> dependencies = new LinkedHashMap<>();
56+
dependencies.put("@typescript-eslint/eslint-plugin", "^5.47.0");
57+
dependencies.put("@typescript-eslint/parser", "^5.47.0");
58+
dependencies.put("typescript", "^4.9.4");
59+
dependencies.put("eslint", Objects.requireNonNull(eslintVersion));
60+
return dependencies;
61+
}
62+
63+
public static Map<String, String> defaultDevDependencies() {
64+
return defaultDevDependenciesWithEslint(DEFAULT_ESLINT_VERSION);
65+
}
66+
67+
public static Map<String, String> defaultDevDependenciesWithEslint(String version) {
68+
return Collections.singletonMap("eslint", version);
69+
}
70+
71+
public static FormatterStep create(Map<String, String> devDependencies, Provisioner provisioner, File projectDir, File buildDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) {
72+
requireNonNull(devDependencies);
73+
requireNonNull(provisioner);
74+
requireNonNull(projectDir);
75+
requireNonNull(buildDir);
76+
return FormatterStep.createLazy(NAME,
77+
() -> new State(NAME, devDependencies, projectDir, buildDir, npmPathResolver, eslintConfig),
78+
State::createFormatterFunc);
79+
}
80+
81+
private static class State extends NpmFormatterStepStateBase implements Serializable {
82+
83+
private static final long serialVersionUID = -539537027004745812L;
84+
private final EslintConfig eslintConfig;
85+
86+
State(String stepName, Map<String, String> devDependencies, File projectDir, File buildDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) throws IOException {
87+
super(stepName,
88+
new NpmConfig(
89+
replaceDevDependencies(
90+
NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, "/com/diffplug/spotless/npm/eslint-package.json"),
91+
new TreeMap<>(devDependencies)),
92+
"eslint",
93+
NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class,
94+
"/com/diffplug/spotless/npm/common-serve.js",
95+
"/com/diffplug/spotless/npm/eslint-serve.js"),
96+
npmPathResolver.resolveNpmrcContent()),
97+
projectDir,
98+
buildDir,
99+
npmPathResolver.resolveNpmExecutable());
100+
this.eslintConfig = localCopyFiles(requireNonNull(eslintConfig));
101+
}
102+
103+
private EslintConfig localCopyFiles(EslintConfig orig) {
104+
if (orig.getEslintConfigPath() == null) {
105+
return orig.verify();
106+
}
107+
// If any config files are provided, we need to make sure they are at the same location as the node modules
108+
// as eslint will try to resolve plugin/config names relatively to the config file location and some
109+
// eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.)
110+
FormattedPrinter.SYSOUT.print("Copying config file <%s> to <%s> and using the copy", orig.getEslintConfigPath(), nodeModulesDir);
111+
File configFileCopy = NpmResourceHelper.copyFileToDir(orig.getEslintConfigPath(), nodeModulesDir);
112+
return orig.withEslintConfigPath(configFileCopy).verify();
113+
}
114+
115+
@Override
116+
@Nonnull
117+
public FormatterFunc createFormatterFunc() {
118+
try {
119+
FormattedPrinter.SYSOUT.print("creating formatter function (starting server)");
120+
ServerProcessInfo eslintRestServer = npmRunServer();
121+
EslintRestService restService = new EslintRestService(eslintRestServer.getBaseUrl());
122+
return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(projectDir, nodeModulesDir, eslintConfig, restService));
123+
} catch (IOException e) {
124+
throw ThrowingEx.asRuntime(e);
125+
}
126+
}
127+
128+
private void endServer(BaseNpmRestService restService, ServerProcessInfo restServer) throws Exception {
129+
FormattedPrinter.SYSOUT.print("Closing formatting function (ending server).");
130+
try {
131+
restService.shutdown();
132+
} catch (Throwable t) {
133+
logger.info("Failed to request shutdown of rest service via api. Trying via process.", t);
134+
}
135+
restServer.close();
136+
}
137+
138+
}
139+
140+
private static class EslintFilePathPassingFormatterFunc implements FormatterFunc.NeedsFile {
141+
private final File projectDir;
142+
private final File nodeModulesDir;
143+
private final EslintConfig eslintConfig;
144+
private final EslintRestService restService;
145+
146+
public EslintFilePathPassingFormatterFunc(File projectDir, File nodeModulesDir, EslintConfig eslintConfig, EslintRestService restService) {
147+
this.projectDir = requireNonNull(projectDir);
148+
this.nodeModulesDir = requireNonNull(nodeModulesDir);
149+
this.eslintConfig = requireNonNull(eslintConfig);
150+
this.restService = requireNonNull(restService);
151+
}
152+
153+
@Override
154+
public String applyWithFile(String unix, File file) throws Exception {
155+
FormattedPrinter.SYSOUT.print("formatting String '" + unix.substring(0, Math.min(50, unix.length())) + "[...]' in file '" + file + "'");
156+
157+
Map<FormatOption, Object> eslintCallOptions = new HashMap<>();
158+
setConfigToCallOptions(eslintCallOptions);
159+
setFilePathToCallOptions(eslintCallOptions, file);
160+
return restService.format(unix, eslintCallOptions);
161+
}
162+
163+
private void setFilePathToCallOptions(Map<FormatOption, Object> eslintCallOptions, File fileToBeFormatted) {
164+
eslintCallOptions.put(FormatOption.FILE_PATH, fileToBeFormatted.getAbsolutePath());
165+
}
166+
167+
private void setConfigToCallOptions(Map<FormatOption, Object> eslintCallOptions) {
168+
if (eslintConfig.getEslintConfigPath() != null) {
169+
eslintCallOptions.put(FormatOption.ESLINT_OVERRIDE_CONFIG_FILE, eslintConfig.getEslintConfigPath().getAbsolutePath());
170+
}
171+
if (eslintConfig.getEslintConfigJs() != null) {
172+
eslintCallOptions.put(FormatOption.ESLINT_OVERRIDE_CONFIG, eslintConfig.getEslintConfigJs());
173+
}
174+
if (eslintConfig instanceof EslintTypescriptConfig) {
175+
// if we are a ts config, see if we need to use specific paths or use default projectDir
176+
File tsConfigFilePath = ((EslintTypescriptConfig) eslintConfig).getTypescriptConfigPath();
177+
File tsConfigRootDir = tsConfigFilePath != null ? tsConfigFilePath.getParentFile() : projectDir;
178+
eslintCallOptions.put(FormatOption.TS_CONFIG_ROOT_DIR, nodeModulesDir.getAbsoluteFile().toPath().relativize(tsConfigRootDir.getAbsoluteFile().toPath()).toString());
179+
}
180+
}
181+
}
182+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2016-2023 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.npm;
17+
18+
import java.util.LinkedHashMap;
19+
import java.util.Map;
20+
import java.util.Map.Entry;
21+
22+
public class EslintRestService extends BaseNpmRestService {
23+
24+
EslintRestService(String baseUrl) {
25+
super(baseUrl);
26+
}
27+
28+
public String format(String fileContent, Map<FormatOption, Object> formatOptions) {
29+
Map<String, Object> jsonProperties = new LinkedHashMap<>();
30+
jsonProperties.put("file_content", fileContent);
31+
for (Entry<FormatOption, Object> option : formatOptions.entrySet()) {
32+
jsonProperties.put(option.getKey().backendName, option.getValue());
33+
}
34+
return restClient.postJson("/eslint/format", jsonProperties);
35+
}
36+
37+
enum FormatOption {
38+
ESLINT_OVERRIDE_CONFIG("eslint_override_config"), ESLINT_OVERRIDE_CONFIG_FILE("eslint_override_config_file"), FILE_PATH("file_path"), TS_CONFIG_ROOT_DIR("ts_config_root_dir");
39+
40+
private final String backendName;
41+
42+
FormatOption(String backendName) {
43+
this.backendName = backendName;
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)