From e13f43fd5ff76d0d3a564f59ea8fe0ce7fb05316 Mon Sep 17 00:00:00 2001
From: Sergey Kuznetsov <32077868+skuznets0v@users.noreply.github.com>
Date: Fri, 13 Dec 2024 20:55:21 +0300
Subject: [PATCH] Support editorconfig file for prettier

---
 .../diffplug/spotless/npm/PrettierConfig.java | 10 ++++++-
 .../spotless/npm/PrettierFormatterStep.java   |  2 +-
 .../spotless/npm/PrettierRestService.java     |  5 +++-
 .../diffplug/spotless/npm/prettier-serve.js   | 21 +++++--------
 .../gradle/spotless/FormatExtension.java      | 11 ++++++-
 .../spotless/PrettierIntegrationTest.java     | 29 ++++++++++++++++++
 .../spotless/maven/generic/Prettier.java      |  5 +++-
 .../prettier/PrettierFormatStepTest.java      | 19 ++++++++++++
 .../npm/prettier/config/.editorconfig_20      |  2 ++
 .../npm/prettier/config/.editorconfig_300     |  2 ++
 .../npm/prettier/config/.prettierrc_noop.yml  |  1 +
 .../npm/PrettierFormatterStepTest.java        | 30 ++++++++++++-------
 12 files changed, 108 insertions(+), 29 deletions(-)
 create mode 100644 testlib/src/main/resources/npm/prettier/config/.editorconfig_20
 create mode 100644 testlib/src/main/resources/npm/prettier/config/.editorconfig_300
 create mode 100644 testlib/src/main/resources/npm/prettier/config/.prettierrc_noop.yml

diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java
index 8a51910205..5bce2726e9 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java
@@ -32,9 +32,12 @@ public class PrettierConfig implements Serializable {
 
 	private final TreeMap<String, Object> options;
 
-	public PrettierConfig(@Nullable File prettierConfigPath, @Nullable Map<String, Object> options) {
+	private final Boolean editorconfig;
+
+	public PrettierConfig(@Nullable File prettierConfigPath, @Nullable Map<String, Object> options, @Nullable Boolean editorconfig) {
 		this.prettierConfigPathSignature = prettierConfigPath == null ? null : FileSignature.promise(prettierConfigPath);
 		this.options = options == null ? new TreeMap<>() : new TreeMap<>(options);
+		this.editorconfig = editorconfig;
 	}
 
 	@Nullable
@@ -45,4 +48,9 @@ public File getPrettierConfigPath() {
 	public Map<String, Object> getOptions() {
 		return new TreeMap<>(this.options);
 	}
+
+	@Nullable
+	public Boolean getEditorconfig() {
+		return editorconfig;
+	}
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
index 27a1002df5..472a2c11a3 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
@@ -90,7 +90,7 @@ public FormatterFunc createFormatterFunc() {
 				logger.info("creating formatter function (starting server)");
 				ServerProcessInfo prettierRestServer = toRuntime().npmRunServer();
 				PrettierRestService restService = new PrettierRestService(prettierRestServer.getBaseUrl());
-				String prettierConfigOptions = restService.resolveConfig(this.prettierConfig.getPrettierConfigPath(), this.prettierConfig.getOptions());
+				String prettierConfigOptions = restService.resolveConfig(this.prettierConfig.getPrettierConfigPath(), this.prettierConfig.getOptions(), prettierConfig.getEditorconfig());
 				return Closeable.ofDangerous(() -> endServer(restService, prettierRestServer), new PrettierFilePathPassingFormatterFunc(prettierConfigOptions, restService));
 			} catch (IOException e) {
 				throw ThrowingEx.asRuntime(e);
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java
index 11fd29c68a..c6e4105178 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java
@@ -25,7 +25,7 @@ public class PrettierRestService extends BaseNpmRestService {
 		super(baseUrl);
 	}
 
-	public String resolveConfig(File prettierConfigPath, Map<String, Object> prettierConfigOptions) {
+	public String resolveConfig(File prettierConfigPath, Map<String, Object> prettierConfigOptions, Boolean editorconfig) {
 		Map<String, Object> jsonProperties = new LinkedHashMap<>();
 		if (prettierConfigPath != null) {
 			jsonProperties.put("prettier_config_path", prettierConfigPath.getAbsolutePath());
@@ -33,6 +33,9 @@ public String resolveConfig(File prettierConfigPath, Map<String, Object> prettie
 		if (prettierConfigOptions != null) {
 			jsonProperties.put("prettier_config_options", JsonWriter.of(prettierConfigOptions).toJsonRawValue());
 		}
+		if (editorconfig != null) {
+			jsonProperties.put("editorconfig", editorconfig);
+		}
 		return restClient.postJson("/prettier/config-options", jsonProperties);
 	}
 
diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js
index ac1f26790d..b885635211 100644
--- a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js
+++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js
@@ -5,19 +5,14 @@ app.post("/prettier/config-options", (req, res) => {
 	const prettier_config_path = config_data.prettier_config_path;
 	const prettier_config_options = config_data.prettier_config_options || {};
 
-	if (prettier_config_path) {
-		prettier
-			.resolveConfig(undefined, { config: prettier_config_path })
-			.then(options => {
-				const mergedConfigOptions = mergeConfigOptions(options, prettier_config_options);
-				res.set("Content-Type", "application/json")
-				res.json(mergedConfigOptions);
-			})
-			.catch(reason => res.status(501).send("Exception while resolving config_file_path: " + reason));
-		return;
-	}
-	res.set("Content-Type", "application/json")
-	res.json(prettier_config_options);
+	prettier
+		.resolveConfig(prettier_config_path, { editorconfig: config_data.editorconfig })
+		.then(options => {
+			const mergedConfigOptions = mergeConfigOptions(options, prettier_config_options);
+			res.set("Content-Type", "application/json")
+			res.json(mergedConfigOptions);
+		})
+		.catch(reason => res.status(501).send("Exception while resolving config_file_path: " + reason));
 });
 
 app.post("/prettier/format", async (req, res) => {
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
index 59898cbf56..988b35c9ba 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
@@ -769,6 +769,9 @@ public class PrettierConfig extends NpmStepConfig<PrettierConfig> {
 		@Nullable
 		Map<String, Object> prettierConfig;
 
+		@Nullable
+		Boolean editorconfig;
+
 		final Map<String, String> devDependencies;
 
 		PrettierConfig(Map<String, String> devDependencies) {
@@ -788,6 +791,12 @@ public PrettierConfig config(final Map<String, Object> prettierConfig) {
 			return this;
 		}
 
+		public PrettierConfig editorconfig(Boolean editorconfig) {
+			this.editorconfig = editorconfig;
+			replaceStep();
+			return this;
+		}
+
 		@Override
 		protected FormatterStep createStep() {
 			final Project project = getProject();
@@ -797,7 +806,7 @@ protected FormatterStep createStep() {
 							Arrays.asList(project.getProjectDir(), project.getRootDir())),
 					new com.diffplug.spotless.npm.PrettierConfig(
 							this.prettierConfigFile != null ? project.file(this.prettierConfigFile) : null,
-							this.prettierConfig));
+							this.prettierConfig, this.editorconfig));
 		}
 	}
 
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
index b6547204b8..6ee13c54cd 100644
--- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
@@ -115,6 +115,35 @@ void useFileConfig(String prettierVersion) throws IOException {
 		}
 	}
 
+	@ParameterizedTest(name = "{index}: useEditorconfig with prettier {0}")
+	@ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3})
+	void useEditorconfig(String prettierVersion) throws IOException {
+		setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc_noop.yml");
+		setFile(".editorconfig").toResource("npm/prettier/config/.editorconfig_20");
+		setFile("build.gradle").toLines(
+			"plugins {",
+			"    id 'com.diffplug.spotless'",
+			"}",
+			"repositories { mavenCentral() }",
+			"spotless {",
+			"    format 'mytypescript', {",
+			"        target 'test.ts'",
+			"        prettier('" + prettierVersion + "').configFile('.prettierrc.yml').editorconfig(true)",
+			"    }",
+			"}");
+		setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+		final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+		switch (prettierVersion) {
+			case PRETTIER_VERSION_2:
+				assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean");
+				break;
+			case PRETTIER_VERSION_3:
+				assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_3.clean");
+				break;
+		}
+	}
+
 	@ParameterizedTest(name = "{index}: chooseParserBasedOnFilename with prettier {0}")
 	@ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3})
 	void chooseParserBasedOnFilename(String prettierVersion) throws IOException {
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java
index c6b3e46e3c..0af813bc9a 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java
@@ -47,6 +47,9 @@ public class Prettier extends AbstractNpmFormatterStepFactory {
 	@Parameter
 	private String configFile;
 
+	@Parameter
+	private Boolean editorconfig;
+
 	@Override
 	public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
 
@@ -101,7 +104,7 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
 		File baseDir = baseDir(stepConfig);
 		File buildDir = buildDir(stepConfig);
 		File cacheDir = cacheDir(stepConfig);
-		PrettierConfig prettierConfig = new PrettierConfig(configFileHandler, configInline);
+		PrettierConfig prettierConfig = new PrettierConfig(configFileHandler, configInline, editorconfig);
 		NpmPathResolver npmPathResolver = npmPathResolver(stepConfig);
 		return PrettierFormatterStep.create(devDependencies, stepConfig.getProvisioner(), baseDir, buildDir, cacheDir, npmPathResolver, prettierConfig);
 	}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java
index abba35e72c..ceaff92674 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java
@@ -158,6 +158,25 @@ void multiple_prettier_configs() throws Exception {
 
 	}
 
+	@Test
+	void prettier_editorconfig() throws Exception {
+		String suffix = "ts";
+		writePomWithPrettierSteps("**/*." + suffix,
+			"<prettier>",
+			"  <prettierVersion>1.16.4</prettierVersion>",
+			"  <configFile>.prettierrc.yml</configFile>",
+			"  <editorconfig>true</editorconfig>",
+			"</prettier>");
+
+		String kind = "typescript";
+		setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc_noop.yml");
+		setFile(".editorconfig").toResource("npm/prettier/config/.editorconfig_300");
+		String path = "src/main/" + kind + "/test." + suffix;
+		setFile(path).toResource("npm/prettier/filetypes/" + kind + "/" + kind + ".dirty");
+		mavenRunner().withArguments("spotless:apply").runNoError();
+		assertFile(path).sameAsResource("npm/prettier/filetypes/" + kind + "/" + kind + ".clean");
+	}
+
 	@Test
 	void custom_plugin() throws Exception {
 		writePomWithFormatSteps(
diff --git a/testlib/src/main/resources/npm/prettier/config/.editorconfig_20 b/testlib/src/main/resources/npm/prettier/config/.editorconfig_20
new file mode 100644
index 0000000000..8984b96358
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/config/.editorconfig_20
@@ -0,0 +1,2 @@
+[*]
+max_line_length = 20
diff --git a/testlib/src/main/resources/npm/prettier/config/.editorconfig_300 b/testlib/src/main/resources/npm/prettier/config/.editorconfig_300
new file mode 100644
index 0000000000..2d70e3af58
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/config/.editorconfig_300
@@ -0,0 +1,2 @@
+[*]
+max_line_length = 300
diff --git a/testlib/src/main/resources/npm/prettier/config/.prettierrc_noop.yml b/testlib/src/main/resources/npm/prettier/config/.prettierrc_noop.yml
new file mode 100644
index 0000000000..dc710caa92
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/config/.prettierrc_noop.yml
@@ -0,0 +1 @@
+parser: typescript
diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java
index 54ce09e7e8..6261df1166 100644
--- a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java
+++ b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java
@@ -69,7 +69,7 @@ private void runTestUsingPrettier(String fileType, Map<String, String> dependenc
 					buildDir(),
 					null,
 					npmPathResolver(),
-					new PrettierConfig(prettierRc, null));
+					new PrettierConfig(prettierRc, null, null));
 
 			try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) {
 				stepHarness.testResource(dirtyFile, cleanFile);
@@ -96,7 +96,7 @@ void parserInferenceBasedOnExplicitFilepathIsWorking(String prettierVersion) thr
 					buildDir(),
 					null,
 					npmPathResolver(),
-					new PrettierConfig(null, ImmutableMap.of("filepath", "anyname.json"))); // should select parser based on this name
+					new PrettierConfig(null, ImmutableMap.of("filepath", "anyname.json"), null)); // should select parser based on this name
 
 			try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) {
 				stepHarness.testResource(dirtyFile, cleanFile);
@@ -118,7 +118,7 @@ void parserInferenceBasedOnFilenameIsWorking(String prettierVersion) throws Exce
 					buildDir(),
 					null,
 					npmPathResolver(),
-					new PrettierConfig(null, Collections.emptyMap()));
+					new PrettierConfig(null, Collections.emptyMap(), null));
 
 			try (StepHarnessWithFile stepHarness = StepHarnessWithFile.forStep(this, formatterStep)) {
 				stepHarness.testResource("test.json", dirtyFile, cleanFile);
@@ -134,7 +134,7 @@ void verifyPrettierErrorMessageIsRelayed() throws Exception {
 					buildDir(),
 					null,
 					npmPathResolver(),
-					new PrettierConfig(null, ImmutableMap.of("parser", "postcss")));
+					new PrettierConfig(null, ImmutableMap.of("parser", "postcss"), null));
 			try (StepHarnessWithFile stepHarness = StepHarnessWithFile.forStep(this, formatterStep)) {
 				stepHarness.expectLintsOfResource("npm/prettier/filetypes/scss/scss.dirty")
 						.toBe("LINE_UNDEFINED prettier-format(com.diffplug.spotless.npm.SimpleRestClient$SimpleRestResponseException) Unexpected response status code at /prettier/format [HTTP 500] -- (Error while formatting: Error: Couldn't resolve parser \"postcss\") (...)");
@@ -170,19 +170,27 @@ void runFormatTest(String prettierVersion, PrettierConfig config, String cleanFi
 		@ParameterizedTest(name = "{index}: defaults are applied with prettier {0}")
 		@ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3})
 		void defaultsAreApplied(String prettierVersion) throws Exception {
-			runFormatTest(prettierVersion, new PrettierConfig(null, ImmutableMap.of("parser", "typescript")), "defaults_prettier_" + major(prettierVersion));
+			runFormatTest(prettierVersion, new PrettierConfig(null, ImmutableMap.of("parser", "typescript"), null), "defaults_prettier_" + major(prettierVersion));
 		}
 
 		@ParameterizedTest(name = "{index}: config file options are applied with prettier {0}")
 		@ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3})
 		void configFileOptionsAreApplied(String prettierVersion) throws Exception {
-			runFormatTest(prettierVersion, new PrettierConfig(createTestFile(FILEDIR + ".prettierrc.yml"), null), "configfile_prettier_" + major(prettierVersion));
+			runFormatTest(prettierVersion, new PrettierConfig(createTestFile(FILEDIR + ".prettierrc.yml"), null, null), "configfile_prettier_" + major(prettierVersion));
 		}
 
-		@ParameterizedTest(name = "{index}: config file options can be overriden with prettier {0}")
+		@ParameterizedTest(name = "{index}: config file options can be overridden with prettier {0}")
 		@ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3})
-		void configFileOptionsCanBeOverriden(String prettierVersion) throws Exception {
-			runFormatTest(prettierVersion, new PrettierConfig(createTestFile(FILEDIR + ".prettierrc.yml"), ImmutableMap.of("printWidth", 300)), "override_prettier_" + major(prettierVersion));
+		void configFileOptionsCanBeOverridden(String prettierVersion) throws Exception {
+			runFormatTest(prettierVersion, new PrettierConfig(createTestFile(FILEDIR + ".prettierrc.yml"), ImmutableMap.of("printWidth", 300), null), "override_prettier_" + major(prettierVersion));
+		}
+
+		@ParameterizedTest(name = "{index}: config file options can be extended with editorconfig with prettier {0}")
+		@ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3})
+		void configFileOptionsCanBeExtendedWithEditorconfig(String prettierVersion) throws Exception {
+			setFile(".editorconfig").toResource(FILEDIR + ".editorconfig_300");
+			File prettierConfigFile = setFile(".prettierrc.yml").toResource(FILEDIR + ".prettierrc_noop.yml");
+			runFormatTest(prettierVersion, new PrettierConfig(prettierConfigFile, null, true), "override_prettier_" + major(prettierVersion));
 		}
 
 		private String major(String semVer) {
@@ -194,7 +202,7 @@ private String major(String semVer) {
 	void equality() {
 		new SerializableEqualityTester() {
 			String prettierVersion = "3.0.0";
-			PrettierConfig config = new PrettierConfig(null, Map.of("parser", "typescript"));
+			PrettierConfig config = new PrettierConfig(null, Map.of("parser", "typescript"), null);
 
 			@Override
 			protected void setupTest(API api) {
@@ -203,7 +211,7 @@ protected void setupTest(API api) {
 				// change the groupArtifact, and it's different
 				prettierVersion = "2.8.8";
 				api.areDifferentThan();
-				config = new PrettierConfig(null, Map.of("parser", "css"));
+				config = new PrettierConfig(null, Map.of("parser", "css"), null);
 				api.areDifferentThan();
 			}