From ea185c5e04834b1a809e7f67e3f0c7710bc2981b Mon Sep 17 00:00:00 2001 From: Matthew de Detrich Date: Mon, 22 Aug 2022 13:18:50 +0200 Subject: [PATCH] Convert scalafmt integration to use a compile-only sourceset --- CHANGES.md | 1 + lib/build.gradle | 6 +- .../diffplug/spotless/scala/ScalaFmtStep.java | 92 ++----------------- .../glue/scalafmt/ScalafmtFormatterFunc.java | 41 +++++++++ .../scalafmt/basic.cleanWithCustomConf_1.1.0 | 24 ----- .../scalafmt/basic.cleanWithCustomConf_2.0.1 | 25 ----- .../scala/scalafmt/basic.clean_1.1.0 | 16 ---- .../scala/scalafmt/basic.clean_2.0.1 | 18 ---- .../scala/scalafmt/basicPost2.0.0.clean | 18 ---- .../basicPost2.0.0.cleanWithCustomConf | 25 ----- .../spotless/scala/ScalaFmtStepTest.java | 41 ++------- 11 files changed, 62 insertions(+), 245 deletions(-) create mode 100644 lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java delete mode 100644 testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 delete mode 100644 testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 delete mode 100644 testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 delete mode 100644 testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 delete mode 100644 testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean delete mode 100644 testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf diff --git a/CHANGES.md b/CHANGES.md index 751749585f..446698d7ed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +* Converted `scalafmt` integration to use a compile-only source set. (fixes [#524](https://github.com/diffplug/spotless/issues/524)) ## [2.28.1] - 2022-08-10 ### Fixed diff --git a/lib/build.gradle b/lib/build.gradle index e14e5c6438..0b3f4555b2 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -12,7 +12,8 @@ def NEEDS_GLUE = [ 'ktfmt', 'ktlint', 'flexmark', - 'diktat' + 'diktat', + 'scalafmt' ] for (glue in NEEDS_GLUE) { sourceSets.register(glue) { @@ -51,6 +52,9 @@ dependencies { ktlintCompileOnly "com.pinterest.ktlint:ktlint-ruleset-experimental:$VER_KTLINT" ktlintCompileOnly "com.pinterest.ktlint:ktlint-ruleset-standard:$VER_KTLINT" + String VER_SCALAFMT="3.5.9" + scalafmtCompileOnly "org.scalameta:scalafmt-core_2.13:$VER_SCALAFMT" + String VER_DIKTAT = "1.2.3" diktatCompileOnly "org.cqfn.diktat:diktat-rules:$VER_DIKTAT" diff --git a/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java index 8c8e4ccb2d..8f96e1bfae 100644 --- a/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java @@ -18,13 +18,8 @@ import java.io.File; import java.io.IOException; import java.io.Serializable; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.lang.reflect.Constructor; import java.util.Collections; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -38,13 +33,8 @@ public class ScalaFmtStep { // prevent direct instantiation private ScalaFmtStep() {} - - private static final Pattern VERSION_PRE_2_0 = Pattern.compile("[10]\\.(\\d+)\\.\\d+"); - private static final Pattern VERSION_PRE_3_0 = Pattern.compile("2\\.(\\d+)\\.\\d+"); - private static final String DEFAULT_VERSION = "3.0.8"; + private static final String DEFAULT_VERSION = "3.5.9"; static final String NAME = "scalafmt"; - static final String MAVEN_COORDINATE_PRE_2_0 = "com.geirsson:scalafmt-core_2.11:"; - static final String MAVEN_COORDINATE_PRE_3_0 = "org.scalameta:scalafmt-core_2.11:"; static final String MAVEN_COORDINATE = "org.scalameta:scalafmt-core_2.13:"; public static FormatterStep create(Provisioner provisioner) { @@ -52,10 +42,8 @@ public static FormatterStep create(Provisioner provisioner) { } public static FormatterStep create(String version, Provisioner provisioner, @Nullable File configFile) { - Objects.requireNonNull(version, "version"); - Objects.requireNonNull(provisioner, "provisioner"); return FormatterStep.createLazy(NAME, - () -> new State(version, provisioner, configFile), + () -> new State(JarState.from(MAVEN_COORDINATE + version, provisioner), configFile), State::createFormat); } @@ -69,78 +57,16 @@ static final class State implements Serializable { final JarState jarState; final FileSignature configSignature; - State(String version, Provisioner provisioner, @Nullable File configFile) throws IOException { - String mavenCoordinate; - Matcher versionMatcher; - if ((versionMatcher = VERSION_PRE_2_0.matcher(version)).matches()) { - mavenCoordinate = MAVEN_COORDINATE_PRE_2_0; - } else if ((versionMatcher = VERSION_PRE_3_0.matcher(version)).matches()) { - mavenCoordinate = MAVEN_COORDINATE_PRE_3_0; - } else { - mavenCoordinate = MAVEN_COORDINATE; - } - - this.jarState = JarState.from(mavenCoordinate + version, provisioner); + State(JarState jarState, @Nullable File configFile) throws IOException { + this.jarState = jarState; this.configSignature = FileSignature.signAsList(configFile == null ? Collections.emptySet() : Collections.singleton(configFile)); } FormatterFunc createFormat() throws Exception { - ClassLoader classLoader = jarState.getClassLoader(); - - // scalafmt returns instances of formatted, we get result by calling get() - Class formatted = classLoader.loadClass("org.scalafmt.Formatted"); - Method formattedGet = formatted.getMethod("get"); - - // this is how we actually do a format - Class scalafmt = classLoader.loadClass("org.scalafmt.Scalafmt"); - Class scalaSet = classLoader.loadClass("scala.collection.immutable.Set"); - - Object defaultScalaFmtConfig = scalafmt.getMethod("format$default$2").invoke(null); - Object emptyRange = scalafmt.getMethod("format$default$3").invoke(null); - Method formatMethod = scalafmt.getMethod("format", String.class, defaultScalaFmtConfig.getClass(), scalaSet); - - // now we just need to parse the config, if any - Object config; - if (configSignature.files().isEmpty()) { - config = defaultScalaFmtConfig; - } else { - File file = configSignature.getOnlyFile(); - - Class optionCls = classLoader.loadClass("scala.Option"); - Class configCls = classLoader.loadClass("org.scalafmt.config.Config"); - Class scalafmtCls = classLoader.loadClass("org.scalafmt.Scalafmt"); - - Object configured; - - try { - // scalafmt >= 1.6.0 - Method parseHoconConfig = scalafmtCls.getMethod("parseHoconConfig", String.class); - - String configStr = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); - - configured = parseHoconConfig.invoke(null, configStr); - } catch (NoSuchMethodException e) { - // scalafmt >= v0.7.0-RC1 && scalafmt < 1.6.0 - Method fromHocon = configCls.getMethod("fromHoconString", String.class, optionCls); - Object fromHoconEmptyPath = configCls.getMethod("fromHoconString$default$2").invoke(null); - - String configStr = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); - - configured = fromHocon.invoke(null, configStr, fromHoconEmptyPath); - } - - config = invokeNoArg(configured, "get"); - } - return input -> { - Object resultInsideFormatted = formatMethod.invoke(null, input, config, emptyRange); - return (String) formattedGet.invoke(resultInsideFormatted); - }; + final ClassLoader classLoader = jarState.getClassLoader(); + final Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.scalafmt.ScalafmtFormatterFunc"); + final Constructor constructor = formatterFunc.getConstructor(FileSignature.class); + return (FormatterFunc) constructor.newInstance(this.configSignature); } } - - private static Object invokeNoArg(Object obj, String toInvoke) throws Exception { - Class clazz = obj.getClass(); - Method method = clazz.getMethod(toInvoke); - return method.invoke(obj); - } } diff --git a/lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java b/lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java new file mode 100644 index 0000000000..17040c790b --- /dev/null +++ b/lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java @@ -0,0 +1,41 @@ +package com.diffplug.spotless.glue.scalafmt; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; + +import org.scalafmt.Scalafmt; +import org.scalafmt.config.ScalafmtConfig; +import org.scalafmt.config.ScalafmtConfig$; + +import scala.collection.immutable.Set$; + +import java.io.File; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class ScalafmtFormatterFunc implements FormatterFunc { + private final FileSignature configSignature; + + public ScalafmtFormatterFunc(FileSignature configSignature) { + this.configSignature = configSignature; + } + + @Override + public String apply(String input) throws Exception { + ScalafmtConfig config; + if (configSignature.files().isEmpty()) { + // Note that reflection is used here only because Scalafmt has a method called + // default which happens to be a reserved Java keyword. The only way to call + // such methods is by reflection, see + // https://vlkan.com/blog/post/2015/11/20/scala-method-with-java-reserved-keyword/ + Method method = ScalafmtConfig$.MODULE$.getClass().getDeclaredMethod("default"); + config = (ScalafmtConfig) method.invoke(ScalafmtConfig$.MODULE$); + } else { + File file = configSignature.getOnlyFile(); + String configStr = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + config = Scalafmt.parseHoconConfig(configStr).get(); + } + return Scalafmt.format(input, config, Set$.MODULE$.empty()).get(); + } +} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 b/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 deleted file mode 100644 index 98bf69b7af..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 +++ /dev/null @@ -1,24 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a - extends b - with c { - def foo[ - T: Int#Double#Triple, - R <% String]( - @annot1 - x: Int @annot2 = - 2, - y: Int = 3) - : Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => - 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 b/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 deleted file mode 100644 index fc8267fb75..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 +++ /dev/null @@ -1,25 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a - extends b - with c { - def foo[ - T: Int#Double#Triple, - R <% String - ]( - @annot1 - x: Int @annot2 = - 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => - 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 b/testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 deleted file mode 100644 index 922a2ccbb9..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 +++ /dev/null @@ -1,16 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a extends b with c { - def foo[T: Int#Double#Triple, R <% String](@annot1 - x: Int @annot2 = 2, - y: Int = 3): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 b/testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 deleted file mode 100644 index b838dfea65..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 +++ /dev/null @@ -1,18 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a extends b with c { - def foo[T: Int#Double#Triple, R <% String]( - @annot1 - x: Int @annot2 = 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean b/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean deleted file mode 100644 index b838dfea65..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean +++ /dev/null @@ -1,18 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a extends b with c { - def foo[T: Int#Double#Triple, R <% String]( - @annot1 - x: Int @annot2 = 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf b/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf deleted file mode 100644 index fc8267fb75..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf +++ /dev/null @@ -1,25 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a - extends b - with c { - def foo[ - T: Int#Double#Triple, - R <% String - ]( - @annot1 - x: Int @annot2 = - 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => - 2 - } - } -} diff --git a/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java index 0f42252d36..ce850bade9 100644 --- a/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; @@ -34,38 +35,16 @@ class ScalaFmtStepTest extends ResourceHarness { @Test void behaviorDefaultConfig() throws Exception { - StepHarness.forStep(ScalaFmtStep.create("1.1.0", TestProvisioner.mavenCentral(), null)) - .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.clean_1.1.0"); - StepHarness.forStep(ScalaFmtStep.create("2.0.1", TestProvisioner.mavenCentral(), null)) - .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.clean_2.0.1"); StepHarness.forStep(ScalaFmtStep.create("3.0.0", TestProvisioner.mavenCentral(), null)) .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.clean_3.0.0"); } @Test void behaviorCustomConfig() throws Exception { - StepHarness.forStep(ScalaFmtStep.create("1.1.0", TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.conf"))) - .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.cleanWithCustomConf_1.1.0"); - StepHarness.forStep(ScalaFmtStep.create("2.0.1", TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.conf"))) - .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.cleanWithCustomConf_2.0.1"); StepHarness.forStep(ScalaFmtStep.create("3.0.0", TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.conf"))) .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.cleanWithCustomConf_3.0.0"); } - @Test - void behaviorDefaultConfigVersion_2_0_0() throws Exception { - FormatterStep step = ScalaFmtStep.create("2.0.0", TestProvisioner.mavenCentral(), null); - StepHarness.forStep(step) - .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basicPost2.0.0.clean"); - } - - @Test - void behaviorCustomConfigVersion_2_0_0() throws Exception { - FormatterStep step = ScalaFmtStep.create("2.0.0", TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.conf")); - StepHarness.forStep(step) - .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basicPost2.0.0.cleanWithCustomConf"); - } - @Test void behaviorDefaultConfigVersion_3_0_0() throws Exception { FormatterStep step = ScalaFmtStep.create("3.0.0", TestProvisioner.mavenCentral(), null); @@ -83,7 +62,7 @@ void behaviorCustomConfigVersion_3_0_0() throws Exception { @Test void equality() throws Exception { new SerializableEqualityTester() { - String version = "0.5.1"; + String version = "3.5.9"; File configFile = null; @Override @@ -91,7 +70,7 @@ protected void setupTest(API api) throws IOException { // same version == same api.areDifferentThan(); // change the version, and it's different - version = "0.5.0"; + version = "3.5.8"; api.areDifferentThan(); // add a config file, and its different configFile = createTestFile("scala/scalafmt/scalafmt.conf"); @@ -113,18 +92,10 @@ void invalidConfiguration() throws Exception { File invalidConfFile = createTestFile("scala/scalafmt/scalafmt.invalid.conf"); Provisioner provisioner = TestProvisioner.mavenCentral(); - InvocationTargetException exception; - - exception = assertThrows(InvocationTargetException.class, - () -> StepHarness.forStep(ScalaFmtStep.create("1.1.0", provisioner, invalidConfFile)).test("", "")); - assertThat(exception.getTargetException().getMessage()).contains("Invalid fields: invalidScalaFmtConfigField"); - - exception = assertThrows(InvocationTargetException.class, - () -> StepHarness.forStep(ScalaFmtStep.create("2.0.1", provisioner, invalidConfFile)).test("", "")); - assertThat(exception.getTargetException().getMessage()).contains("Invalid field: invalidScalaFmtConfigField"); + NoSuchElementException exception; - exception = assertThrows(InvocationTargetException.class, + exception = assertThrows(NoSuchElementException.class, () -> StepHarness.forStep(ScalaFmtStep.create("3.0.0", provisioner, invalidConfFile)).test("", "")); - assertThat(exception.getTargetException().getMessage()).contains("found option 'invalidScalaFmtConfigField' which wasn't expected"); + assertThat(exception.getMessage()).contains("found option 'invalidScalaFmtConfigField' which wasn't expected"); } }