Skip to content

Commit

Permalink
Adding ESLint as formatter step (#1453)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Jan 10, 2023
2 parents 6865449 + 0227e89 commit 1f41cb2
Show file tree
Hide file tree
Showing 87 changed files with 2,900 additions and 259 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
* 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)).
### Fixed
* 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))
* Added support for npm-based [ESLint](https://eslint.org/)-formatter for javascript and typescript ([#1453](https://github.com/diffplug/spotless/pull/1453))

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


## [2.31.1] - 2023-01-02
### Fixed
* Improve memory usage when using git ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426))
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}}
lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('markdown.FlexmarkStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
lib('npm.EslintFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('pom.SortPomStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
Expand Down Expand Up @@ -113,6 +114,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`markdown.FlexmarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
| [`npm.EslintFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 DiffPlug
* Copyright 2020-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2016-2023 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.npm;

abstract class BaseNpmRestService {

protected final SimpleRestClient restClient;

BaseNpmRestService(String baseUrl) {
this.restClient = SimpleRestClient.forBaseUrl(baseUrl);
}

public String shutdown() {
return restClient.post("/shutdown");
}

}
72 changes: 72 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/npm/EslintConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2016-2023 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.npm;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;

import javax.annotation.Nullable;

import com.diffplug.spotless.FileSignature;
import com.diffplug.spotless.ThrowingEx;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public class EslintConfig implements Serializable {

private static final long serialVersionUID = -6196834313082791248L;

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
@Nullable
private final transient File eslintConfigPath;

@SuppressWarnings("unused")
private final FileSignature eslintConfigPathSignature;

private final String eslintConfigJs;

public EslintConfig(@Nullable File eslintConfigPath, @Nullable String eslintConfigJs) {
try {
this.eslintConfigPath = eslintConfigPath;
this.eslintConfigPathSignature = eslintConfigPath != null ? FileSignature.signAsList(this.eslintConfigPath) : FileSignature.signAsList();
this.eslintConfigJs = eslintConfigJs;
} catch (IOException e) {
throw ThrowingEx.asRuntime(e);
}
}

public EslintConfig withEslintConfigPath(@Nullable File eslintConfigPath) {
return new EslintConfig(eslintConfigPath, this.eslintConfigJs);
}

@Nullable
public File getEslintConfigPath() {
return eslintConfigPath;
}

@Nullable
public String getEslintConfigJs() {
return eslintConfigJs;
}

public EslintConfig verify() {
if (eslintConfigPath == null && eslintConfigJs == null) {
throw new IllegalArgumentException("ESLint must be configured using either a configFile or a configJs - but both are null.");
}
return this;
}
}
182 changes: 182 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2016-2023 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.npm;

import static java.util.Objects.requireNonNull;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

import javax.annotation.Nonnull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.FormatterFunc.Closeable;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.Provisioner;
import com.diffplug.spotless.ThrowingEx;
import com.diffplug.spotless.npm.EslintRestService.FormatOption;

public class EslintFormatterStep {

private static final Logger logger = LoggerFactory.getLogger(EslintFormatterStep.class);

public static final String NAME = "eslint-format";

public static final String DEFAULT_ESLINT_VERSION = "^8.31.0";

public static Map<String, String> defaultDevDependenciesForTypescript() {
return defaultDevDependenciesTypescriptWithEslint(DEFAULT_ESLINT_VERSION);
}

public static Map<String, String> defaultDevDependenciesTypescriptWithEslint(String eslintVersion) {
Map<String, String> dependencies = new LinkedHashMap<>();
dependencies.put("@typescript-eslint/eslint-plugin", "^5.47.0");
dependencies.put("@typescript-eslint/parser", "^5.47.0");
dependencies.put("typescript", "^4.9.4");
dependencies.put("eslint", Objects.requireNonNull(eslintVersion));
return dependencies;
}

public static Map<String, String> defaultDevDependencies() {
return defaultDevDependenciesWithEslint(DEFAULT_ESLINT_VERSION);
}

public static Map<String, String> defaultDevDependenciesWithEslint(String version) {
return Collections.singletonMap("eslint", version);
}

public static FormatterStep create(Map<String, String> devDependencies, Provisioner provisioner, File projectDir, File buildDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) {
requireNonNull(devDependencies);
requireNonNull(provisioner);
requireNonNull(projectDir);
requireNonNull(buildDir);
return FormatterStep.createLazy(NAME,
() -> new State(NAME, devDependencies, projectDir, buildDir, npmPathResolver, eslintConfig),
State::createFormatterFunc);
}

private static class State extends NpmFormatterStepStateBase implements Serializable {

private static final long serialVersionUID = -539537027004745812L;
private final EslintConfig eslintConfig;

State(String stepName, Map<String, String> devDependencies, File projectDir, File buildDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) throws IOException {
super(stepName,
new NpmConfig(
replaceDevDependencies(
NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, "/com/diffplug/spotless/npm/eslint-package.json"),
new TreeMap<>(devDependencies)),
"eslint",
NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class,
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/eslint-serve.js"),
npmPathResolver.resolveNpmrcContent()),
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable());
this.eslintConfig = localCopyFiles(requireNonNull(eslintConfig));
}

private EslintConfig localCopyFiles(EslintConfig orig) {
if (orig.getEslintConfigPath() == null) {
return orig.verify();
}
// If any config files are provided, we need to make sure they are at the same location as the node modules
// as eslint will try to resolve plugin/config names relatively to the config file location and some
// eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.)
FormattedPrinter.SYSOUT.print("Copying config file <%s> to <%s> and using the copy", orig.getEslintConfigPath(), nodeModulesDir);
File configFileCopy = NpmResourceHelper.copyFileToDir(orig.getEslintConfigPath(), nodeModulesDir);
return orig.withEslintConfigPath(configFileCopy).verify();
}

@Override
@Nonnull
public FormatterFunc createFormatterFunc() {
try {
FormattedPrinter.SYSOUT.print("creating formatter function (starting server)");
ServerProcessInfo eslintRestServer = npmRunServer();
EslintRestService restService = new EslintRestService(eslintRestServer.getBaseUrl());
return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(projectDir, nodeModulesDir, eslintConfig, restService));
} catch (IOException e) {
throw ThrowingEx.asRuntime(e);
}
}

private void endServer(BaseNpmRestService restService, ServerProcessInfo restServer) throws Exception {
FormattedPrinter.SYSOUT.print("Closing formatting function (ending server).");
try {
restService.shutdown();
} catch (Throwable t) {
logger.info("Failed to request shutdown of rest service via api. Trying via process.", t);
}
restServer.close();
}

}

private static class EslintFilePathPassingFormatterFunc implements FormatterFunc.NeedsFile {
private final File projectDir;
private final File nodeModulesDir;
private final EslintConfig eslintConfig;
private final EslintRestService restService;

public EslintFilePathPassingFormatterFunc(File projectDir, File nodeModulesDir, EslintConfig eslintConfig, EslintRestService restService) {
this.projectDir = requireNonNull(projectDir);
this.nodeModulesDir = requireNonNull(nodeModulesDir);
this.eslintConfig = requireNonNull(eslintConfig);
this.restService = requireNonNull(restService);
}

@Override
public String applyWithFile(String unix, File file) throws Exception {
FormattedPrinter.SYSOUT.print("formatting String '" + unix.substring(0, Math.min(50, unix.length())) + "[...]' in file '" + file + "'");

Map<FormatOption, Object> eslintCallOptions = new HashMap<>();
setConfigToCallOptions(eslintCallOptions);
setFilePathToCallOptions(eslintCallOptions, file);
return restService.format(unix, eslintCallOptions);
}

private void setFilePathToCallOptions(Map<FormatOption, Object> eslintCallOptions, File fileToBeFormatted) {
eslintCallOptions.put(FormatOption.FILE_PATH, fileToBeFormatted.getAbsolutePath());
}

private void setConfigToCallOptions(Map<FormatOption, Object> eslintCallOptions) {
if (eslintConfig.getEslintConfigPath() != null) {
eslintCallOptions.put(FormatOption.ESLINT_OVERRIDE_CONFIG_FILE, eslintConfig.getEslintConfigPath().getAbsolutePath());
}
if (eslintConfig.getEslintConfigJs() != null) {
eslintCallOptions.put(FormatOption.ESLINT_OVERRIDE_CONFIG, eslintConfig.getEslintConfigJs());
}
if (eslintConfig instanceof EslintTypescriptConfig) {
// if we are a ts config, see if we need to use specific paths or use default projectDir
File tsConfigFilePath = ((EslintTypescriptConfig) eslintConfig).getTypescriptConfigPath();
File tsConfigRootDir = tsConfigFilePath != null ? tsConfigFilePath.getParentFile() : projectDir;
eslintCallOptions.put(FormatOption.TS_CONFIG_ROOT_DIR, nodeModulesDir.getAbsoluteFile().toPath().relativize(tsConfigRootDir.getAbsoluteFile().toPath()).toString());
}
}
}
}
46 changes: 46 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/npm/EslintRestService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2016-2023 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.npm;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

public class EslintRestService extends BaseNpmRestService {

EslintRestService(String baseUrl) {
super(baseUrl);
}

public String format(String fileContent, Map<FormatOption, Object> formatOptions) {
Map<String, Object> jsonProperties = new LinkedHashMap<>();
jsonProperties.put("file_content", fileContent);
for (Entry<FormatOption, Object> option : formatOptions.entrySet()) {
jsonProperties.put(option.getKey().backendName, option.getValue());
}
return restClient.postJson("/eslint/format", jsonProperties);
}

enum FormatOption {
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");

private final String backendName;

FormatOption(String backendName) {
this.backendName = backendName;
}
}
}
Loading

0 comments on commit 1f41cb2

Please sign in to comment.