diff --git a/CHANGES.md b/CHANGES.md index af51141d31..e758505686 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ 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] +# Added +* Support for Scalafix ([#591](https://github.com/diffplug/spotless/pull/591)) ## [2.1.0] - 2020-07-04 ### Added diff --git a/README.md b/README.md index 5019198eef..30c6ef4c85 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} |', lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', +lib('scala.ScalaFixStep') +'{{yes}} | {{no}} | {{no}} |', lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{no}} |', lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{no}} | {{no}} |', extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', @@ -89,6 +90,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | | [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | +| [`scala.ScalaFixStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFixStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :white_large_square: | | [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`wtp.EclipseWtpFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStep.java) | :+1: | :+1: | :white_large_square: | diff --git a/lib/src/main/java/com/diffplug/spotless/FileSignature.java b/lib/src/main/java/com/diffplug/spotless/FileSignature.java index d0a8c2881c..f8408a2cb0 100644 --- a/lib/src/main/java/com/diffplug/spotless/FileSignature.java +++ b/lib/src/main/java/com/diffplug/spotless/FileSignature.java @@ -74,7 +74,7 @@ public static FileSignature signAsSet(Iterable files) throws IOException { builder.append(" " + file.getAbsolutePath() + "\n"); } builder.append("a caching signature is being generated, which will be based only on their\n"); - builder.append("names, not their full path (foo.txt, not C:\folder\foo.txt). Unexpectedly,\n"); + builder.append("names, not their full path (foo.txt, not C:\\folder\\foo.txt). Unexpectedly,\n"); builder.append("you have two files with different paths, but the same names. You must\n"); builder.append("rename one of them so that all files have unique names."); throw new IllegalArgumentException(builder.toString()); diff --git a/lib/src/main/java/com/diffplug/spotless/scala/ScalaCompiler.java b/lib/src/main/java/com/diffplug/spotless/scala/ScalaCompiler.java new file mode 100644 index 0000000000..9f74b9bdd6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/scala/ScalaCompiler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2020 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.scala; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class ScalaCompiler implements Serializable { + private static final long serialVersionUID = 1L; + + private final String version; + private final Set plugins; + private final Set compilerOptions; + private final String classpath; + private Boolean enabled = false; + + public ScalaCompiler(final String version, final String classpath) { + this.plugins = new HashSet<>(); + this.compilerOptions = new HashSet<>(); + this.version = version; + this.classpath = classpath; + } + + public String getVersion() { + return version; + } + + public String getClasspath() { + return classpath; + } + + public Set getPlugins() { + return plugins; + } + + public Set getCompilerOptions() { + return compilerOptions; + } + + public Boolean isEnabled() { + return enabled; + } + + public void addPlugin(final String dependency) { + plugins.add(dependency); + } + + public void addCompilerOptions(final Collection options) { + compilerOptions.addAll(options); + } + + /** + * Call this to compile the project before spotless is applied + */ + public void enable() { + enabled = true; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/scala/ScalaFixStep.java b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFixStep.java new file mode 100644 index 0000000000..ef78a615f6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFixStep.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016-2020 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.scala; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class ScalaFixStep { + // prevent direct instantiation + private ScalaFixStep() {} + + public static final String NAME = "scalafix"; + private static final String DEFAULT_VERSION = "0.9.16"; + private static final String DEFAULT_SCALA_VERSION = "2.12.11"; + private static final String MAVEN_COORDINATE = "ch.epfl.scala:scalafix-cli_%s:%s"; + private static final String SEMANTICDB_MAVEN_COORDINATE = "org.scalameta:semanticdb-scalac_%s:latest.release"; + + public static void init(final ScalaCompiler scalaCompiler, final File projectDir) { + final List compilerOptions = Arrays.asList( + "-Ywarn-unused", + "-Yrangepos", + "-P:semanticdb:synthetics:on", + "-P:semanticdb:sourceroot:" + projectDir); + + scalaCompiler.enable(); + scalaCompiler.addPlugin(String.format(SEMANTICDB_MAVEN_COORDINATE, scalaCompiler.getVersion())); + scalaCompiler.addCompilerOptions(compilerOptions); + } + + public static FormatterStep create(final String version, final String scalaVersion, final Provisioner provisioner, final ScalaCompiler scalaCompiler, @Nullable final File configFile) { + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(scalaVersion, "scalaVersion"); + Objects.requireNonNull(provisioner, "provisioner"); + Objects.requireNonNull(scalaCompiler, "scalaCompiler"); + return FormatterStep.createLazy(NAME, () -> new State(version, scalaVersion, provisioner, scalaCompiler, configFile), State::createFormat); + } + + public static String defaultVersion() { + return DEFAULT_VERSION; + } + + public static String defaultScalaVersion() { + return DEFAULT_SCALA_VERSION; + } + + static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + final JarState jarState; + final FileSignature configSignature; + final ScalaCompiler scalaCompiler; + + State(final String version, final String scalaVersion, final Provisioner provisioner, final ScalaCompiler scalaCompiler, @Nullable final File configFile) throws IOException { + this.jarState = JarState.from(String.format(MAVEN_COORDINATE, scalaVersion, version), provisioner); + this.configSignature = FileSignature.signAsList(configFile == null ? Collections.emptySet() : Collections.singleton(configFile)); + this.scalaCompiler = scalaCompiler; + } + + FormatterFunc.NeedsFile createFormat() throws Exception { + final ClassLoader classLoader = jarState.getClassLoader(); + + final Class javaConversionsCls = classLoader.loadClass("scala.collection.JavaConversions"); + final Class absolutePathCls = classLoader.loadClass("scala.meta.io.AbsolutePath"); + final Class classpathCls = classLoader.loadClass("scala.meta.io.Classpath"); + final Class someCls = classLoader.loadClass("scala.Some"); + final Class argsCls = classLoader.loadClass("scalafix.internal.v1.Args"); + final Class mainOpsCls = classLoader.loadClass("scalafix.internal.v1.MainOps"); + + final Method absolutePathApply = absolutePathCls.getMethod("apply", File.class, absolutePathCls); + final Method asScalaIterator = javaConversionsCls.getMethod("asScalaIterator", Iterator.class); + final Method argsDefault = argsCls.getMethod("default", absolutePathCls, PrintStream.class); + final Object workingDirectory = absolutePathCls.getMethod("workingDirectory").invoke(null); + final Object scalacOptionsIter = asScalaIterator.invoke(null, scalaCompiler.getCompilerOptions().iterator()); + final Object scalacOptions = scalacOptionsIter.getClass().getMethod("toList").invoke(scalacOptionsIter); + final Object classpath = classpathCls.getMethod("apply", String.class).invoke(null, scalaCompiler.getClasspath()); + + final Method someApply = someCls.getMethod("apply", Object.class); + + return (String input, File source) -> { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + final PrintStream out = new PrintStream(byteArrayOutputStream); + final Object args = argsDefault.invoke(null, workingDirectory, out); + setField(args, "stdout", true); + setField(args, "scalacOptions", scalacOptions); + setField(args, "classpath", classpath); + if (!configSignature.files().isEmpty()) { + final File file = configSignature.getOnlyFile(); + final Object config = absolutePathApply.invoke(null, file, workingDirectory); + setField(args, "config", someApply.invoke(null, config)); + } + + final Object maybeValidatedArgs = argsCls.getMethod("validate").invoke(args); + final Object validatedArgs = maybeValidatedArgs.getClass().getMethod("get").invoke(maybeValidatedArgs); + final Method handleFile = mainOpsCls.getMethod("handleFile", validatedArgs.getClass(), absolutePathCls); + final Object sourcePath = absolutePathApply.invoke(null, source, workingDirectory); + handleFile.invoke(null, validatedArgs, sourcePath); + final String output = byteArrayOutputStream.toString(); + // Remove the last new line added by println + return output.substring(0, output.length() - 1); + }; + } + } + + private static void setField(final Object obj, final String name, final Object value) throws Exception { + final Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 9875a4f8d4..74d08536c7 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* Support for Scalafix ([#591](https://github.com/diffplug/spotless/pull/591)) ## [5.0.0] - 2020-07-12 diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 8af45e3455..ebf45a5890 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -290,6 +290,7 @@ spotless { // by default, all `.scala` and `.sc` files in the java sourcesets will be formatted scalafmt() // has its own section below + scalafix() // has its own section below licenseHeader '/* (C) $YEAR */', 'package ' // or licenseHeaderFile // note the 'package ' argument - this is a regex which identifies the top @@ -310,6 +311,19 @@ spotless { scalafmt('2.6.1').configFile('scalafmt.conf') ``` +### scalafix + +[homepage](https://scalacenter.github.io/scalafix/). [changelog](https://github.com/scalacenter/scalafix/releases). [config docs](https://scalacenter.github.io/scalafix/docs/users/configuration.html). + +```gradle +spotless { + scala { + scalafix() + // optional: you can specify a specific version of scalafix-cli >= 0.9.1 or Scala version suffix depended by it, or config file + // default: 0.9.16 (scalafix-cli), 2.12.11 (Scala), .scalafix.conf (config file) + scalafix('0.9.1', '2.11.12').configFile('scalafix.conf') +``` + ## C/C++ diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java index 40d6782820..4b8ecbfd05 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java @@ -16,23 +16,51 @@ package com.diffplug.gradle.spotless; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.gradle.api.GradleException; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.PluginManager; +import org.gradle.api.tasks.ScalaRuntime; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.scala.ScalaCompile; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.scala.ScalaCompiler; +import com.diffplug.spotless.scala.ScalaFixStep; import com.diffplug.spotless.scala.ScalaFmtStep; public class ScalaExtension extends FormatExtension { static final String NAME = "scala"; + private static final String SCALA_COMPILER_PLUGINS_CONFIGURATION_NAME = "spotlessScalaCompilerPlugins"; - public ScalaExtension(SpotlessExtension spotless) { - super(spotless); + private final ScalaCompiler scalaCompiler; + + public ScalaExtension(SpotlessExtension rootExtension) { + super(rootExtension); + + final PluginManager pluginManager = getProject().getPluginManager(); + if (!pluginManager.hasPlugin("scala")) { + pluginManager.apply("scala"); + } + + List destinationDirs = new ArrayList<>(); + getProject().getTasks().withType(ScalaCompile.class).forEach(scalaCompile -> destinationDirs.add(scalaCompile.getDestinationDir())); + + ScalaRuntime scalaRuntime = getProject().getExtensions().findByType(ScalaRuntime.class); + ScalaCompile scalaCompileTask = getProject().getTasks().withType(ScalaCompile.class).findByName("compileScala"); + File scalaJar = scalaRuntime.findScalaJar(scalaCompileTask.getClasspath(), "library"); + if (scalaJar == null) { + throw new GradleException("You must add a dependency of scala compiler."); + } + scalaCompiler = new ScalaCompiler(scalaRuntime.getScalaVersion(scalaJar), getProject().files(destinationDirs).getAsPath()); } public ScalaFmtConfig scalafmt() { @@ -43,6 +71,18 @@ public ScalaFmtConfig scalafmt(String version) { return new ScalaFmtConfig(version); } + public ScalaFixConfig scalafix() { + return scalafix(ScalaFixStep.defaultVersion()); + } + + public ScalaFixConfig scalafix(String version) { + return scalafix(version, ScalaFixStep.defaultScalaVersion()); + } + + public ScalaFixConfig scalafix(String version, String scalaVersion) { + return new ScalaFixConfig(version, scalaVersion); + } + public class ScalaFmtConfig { final String version; @Nullable @@ -64,7 +104,30 @@ private FormatterStep createStep() { } } - /** If the user hasn't specified the files yet, we'll assume he/she means all of the scala files. */ + public class ScalaFixConfig { + final String version; + final String scalaVersion; + @Nullable + Object configFile; + + ScalaFixConfig(String version, String scalaVersion) { + this.version = Objects.requireNonNull(version); + this.scalaVersion = Objects.requireNonNull(scalaVersion); + ScalaFixStep.init(scalaCompiler, getProject().getProjectDir()); + addStep(createStep()); + } + + public void configFile(Object configFile) { + this.configFile = Objects.requireNonNull(configFile); + replaceStep(createStep()); + } + + private FormatterStep createStep() { + File resolvedConfigFile = configFile == null ? null : getProject().file(configFile); + return ScalaFixStep.create(version, scalaVersion, provisioner(), scalaCompiler, resolvedConfigFile); + } + } + @Override protected void setupTask(SpotlessTask task) { if (target == null) { @@ -81,6 +144,19 @@ protected void setupTask(SpotlessTask task) { } target = union; } + + if (scalaCompiler.isEnabled()) { + // scalaCompilerPlugins is not available on Gradle < 6.4 + final Configuration scalacPlugins = getProject().getConfigurations().create(SCALA_COMPILER_PLUGINS_CONFIGURATION_NAME); + scalacPlugins.setTransitive(false); + scalacPlugins.setCanBeConsumed(false); + scalaCompiler.getPlugins().forEach(plugin -> getProject().getDependencies().add(SCALA_COMPILER_PLUGINS_CONFIGURATION_NAME, plugin)); + scalaCompiler.addCompilerOptions(scalacPlugins.getFiles().stream().map(file -> "-Xplugin:" + file).collect(Collectors.toList())); + getProject().getTasks().withType(ScalaCompile.class).forEach(scalaCompile -> scalaCompile.getScalaCompileOptions().setAdditionalParameters(new ArrayList(scalaCompiler.getCompilerOptions()))); + + task.dependsOn("compileTestScala"); + } + super.setupTask(task); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ScalaExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ScalaExtensionTest.java index aa3bf7e926..dd6d03a058 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ScalaExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ScalaExtensionTest.java @@ -21,13 +21,16 @@ public class ScalaExtensionTest extends GradleIntegrationHarness { @Test - public void integration() throws IOException { + public void integrationScalafmt() throws IOException { setFile("build.gradle").toLines( - "buildscript { repositories { mavenCentral() } }", "plugins {", " id 'com.diffplug.spotless'", + " id 'scala'", + "}", + "repositories { mavenCentral() }", + "dependencies {", + " implementation 'org.scala-lang:scala-library:2.13.2'", "}", - "apply plugin: 'scala'", "spotless {", " scala {", " scalafmt().configFile('scalafmt.conf')", @@ -38,4 +41,82 @@ public void integration() throws IOException { gradleRunner().withArguments("spotlessApply").build(); assertFile("src/main/scala/basic.scala").sameAsResource("scala/scalafmt/basic.cleanWithCustomConf_2.0.1"); } + + @Test + public void integrationScalafix() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'scala'", + "}", + "repositories { mavenCentral() }", + "dependencies {", + " implementation 'org.scala-lang:scala-library:2.12.11'", + "}", + "spotless {", + " scala {", + " scalafix()", + " }", + "}"); + setFile(".scalafix.conf").toResource("scala/scalafix/integration/.scalafix.conf"); + setFile("src/main/scala/basic.scala").toResource("scala/scalafix/integration/basic.dirty"); + // Scalafix uses "user.dir" as the working directory + final String originalDir = System.getProperty("user.dir"); + System.setProperty("user.dir", rootFolder().toString()); + gradleRunner().withArguments("spotlessApply").build(); + System.setProperty("user.dir", originalDir); + assertFile("src/main/scala/basic.scala").sameAsResource("scala/scalafix/integration/basic.clean"); + } + + @Test + public void integrationScalafix_0_9_1() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'scala'", + "}", + "repositories { mavenCentral() }", + "dependencies {", + " implementation 'org.scala-lang:scala-library:2.12.11'", + "}", + "spotless {", + " scala {", + " scalafix('0.9.1', '2.11.12')", + " }", + "}"); + setFile(".scalafix.conf").toResource("scala/scalafix/integration/.scalafix.conf"); + setFile("src/main/scala/basic.scala").toResource("scala/scalafix/integration/basic.dirty"); + final String originalDir = System.getProperty("user.dir"); + // Scalafix uses "user.dir" as the working directory + System.setProperty("user.dir", rootFolder().toString()); + gradleRunner().withArguments("spotlessApply").build(); + System.setProperty("user.dir", originalDir); + assertFile("src/main/scala/basic.scala").sameAsResource("scala/scalafix/integration/basic.clean"); + } + + @Test + public void integrationScalafixTestdir() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'scala'", + "}", + "repositories { mavenCentral() }", + "dependencies {", + " implementation 'org.scala-lang:scala-library:2.12.11'", + "}", + "spotless {", + " scala {", + " scalafix()", + " }", + "}"); + setFile(".scalafix.conf").toResource("scala/scalafix/integration/.scalafix.conf"); + setFile("src/test/scala/basic.scala").toResource("scala/scalafix/integration/basic.dirty"); + // Scalafix uses "user.dir" as the working directory + final String originalDir = System.getProperty("user.dir"); + System.setProperty("user.dir", rootFolder().toString()); + gradleRunner().withArguments("spotlessApply").build(); + System.setProperty("user.dir", originalDir); + assertFile("src/test/scala/basic.scala").sameAsResource("scala/scalafix/integration/basic.clean"); + } } diff --git a/testlib/src/main/resources/scala/scalafix/.scalafix.conf b/testlib/src/main/resources/scala/scalafix/.scalafix.conf new file mode 100644 index 0000000000..b5faa94741 --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/.scalafix.conf @@ -0,0 +1,3 @@ +rules = [ + NoValInForComprehension +] diff --git a/testlib/src/main/resources/scala/scalafix/.scalafix2.conf b/testlib/src/main/resources/scala/scalafix/.scalafix2.conf new file mode 100644 index 0000000000..154f4407fd --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/.scalafix2.conf @@ -0,0 +1,4 @@ +rules = [ + NoValInForComprehension, + LeakingImplicitClassVal +] diff --git a/testlib/src/main/resources/scala/scalafix/basic.clean b/testlib/src/main/resources/scala/scalafix/basic.clean new file mode 100644 index 0000000000..b73010990c --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/basic.clean @@ -0,0 +1,8 @@ +object a { + implicit class b(var xs: List[Int]) extends AnyVal { + def c: List[Int] = for { + x <- xs + y = x + 1 + } yield y + } +} diff --git a/testlib/src/main/resources/scala/scalafix/basic.clean2 b/testlib/src/main/resources/scala/scalafix/basic.clean2 new file mode 100644 index 0000000000..a729e88d46 --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/basic.clean2 @@ -0,0 +1,8 @@ +object a { + implicit class b(private var xs: List[Int]) extends AnyVal { + def c: List[Int] = for { + x <- xs + y = x + 1 + } yield y + } +} diff --git a/testlib/src/main/resources/scala/scalafix/basic.dirty b/testlib/src/main/resources/scala/scalafix/basic.dirty new file mode 100644 index 0000000000..6f6ab49b23 --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/basic.dirty @@ -0,0 +1,8 @@ +object a { + implicit class b(var xs: List[Int]) extends AnyVal { + def c: List[Int] = for { + x <- xs + val y = x + 1 + } yield y + } +} diff --git a/testlib/src/main/resources/scala/scalafix/integration/.scalafix.conf b/testlib/src/main/resources/scala/scalafix/integration/.scalafix.conf new file mode 100644 index 0000000000..4f269273a0 --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/integration/.scalafix.conf @@ -0,0 +1,4 @@ +rules = [ + RemoveUnused, + ProcedureSyntax +] diff --git a/testlib/src/main/resources/scala/scalafix/integration/basic.clean b/testlib/src/main/resources/scala/scalafix/integration/basic.clean new file mode 100644 index 0000000000..d06ec9a34a --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/integration/basic.clean @@ -0,0 +1,12 @@ +import scala.util.Random + +object a { + def b: List[Int] = { + val c = List(1, 2, Random.nextInt()) + c.map(_ * 2) + } + + def d: Unit = { + Console.println(b) + } +} diff --git a/testlib/src/main/resources/scala/scalafix/integration/basic.dirty b/testlib/src/main/resources/scala/scalafix/integration/basic.dirty new file mode 100644 index 0000000000..b4eb089df0 --- /dev/null +++ b/testlib/src/main/resources/scala/scalafix/integration/basic.dirty @@ -0,0 +1,13 @@ +import scala.util.Random +import scala.util.Try + +object a { + def b: List[Int] = { + val c = List(1, 2, Random.nextInt()) + c.map(_ * 2) + } + + def d { + Console.println(b) + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFixStepTest.java b/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFixStepTest.java new file mode 100644 index 0000000000..2ccc0a776e --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFixStepTest.java @@ -0,0 +1,222 @@ +/* + * Copyright 2016-2020 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.scala; + +import java.io.File; +import java.io.IOException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.diffplug.spotless.*; + +/** + * The rules used in these tests can run directly on source code without compilation. + * Other tests that need compilation are written in ScalaExtensionTest. + */ +public class ScalaFixStepTest extends ResourceHarness { + private ScalaCompiler scalaCompiler; + private String originalDir; + private File testFile; + + @Before + public void before() throws IOException { + scalaCompiler = new ScalaCompiler("2.13.2", ""); + originalDir = System.getProperty("user.dir"); + final File configFile = createTestFile("scala/scalafix/.scalafix.conf"); + // Scalafix uses "user.dir" as the working directory + System.setProperty("user.dir", configFile.getParentFile().getAbsolutePath()); + testFile = newFile("basic.sc"); + testFile.createNewFile(); + } + + @After + public void after() { + System.setProperty("user.dir", originalDir); + } + + @Test + public void behaviorDefaultConfigFilename() throws Exception { + StepHarnessWithFile.forStep(ScalaFixStep.create("0.9.16", "2.12.11", TestProvisioner.mavenCentral(), scalaCompiler, null)) + .test(testFile, "scala/scalafix/basic.dirty", "scala/scalafix/basic.clean"); + } + + @Test + public void behaviorCustomConfigFilename() throws Exception { + StepHarnessWithFile.forStep(ScalaFixStep.create("0.9.16", "2.12.11", TestProvisioner.mavenCentral(), scalaCompiler, createTestFile("scala/scalafix/.scalafix2.conf"))) + .test(testFile, "scala/scalafix/basic.dirty", "scala/scalafix/basic.clean2"); + } + + /* + @Test + public void behaviorDefaultConfigFilename_0_9_1() throws Exception { + StepHarnessWithFile.forStep(ScalaFixStep.create("0.9.1", "2.11.12", TestProvisioner.mavenCentral(), scalaCompiler, null)) + .test(testFile, "scala/scalafix/basic.dirty", "scala/scalafix/basic.clean"); + } + + @Test + public void behaviorCustomConfigFilename_0_9_1() throws Exception { + StepHarnessWithFile.forStep(ScalaFixStep.create("0.9.1", "2.11.12", TestProvisioner.mavenCentral(), scalaCompiler, createTestFile("scala/scalafix/.scalafix2.conf"))) + .test(testFile, "scala/scalafix/basic.dirty", "scala/scalafix/basic.clean2"); + } + + The tests above are broken because their transitive dependencies include multiple + libraries with the same name, specifically: + + - com.lihaoyi:fastparse_2.11:1.0.0 + - org.scalameta:fastparse_2.11:1.0.0 + + Seems like dropping support for these old versions wouldn't be a big drawback, no? + + \--- ch.epfl.scala:scalafix-cli_2.11.12:0.9.1 + +--- org.scala-lang:scala-library:2.11.12 + +--- ch.epfl.scala:scalafix-reflect_2.11.12:0.9.1 + | +--- org.scala-lang:scala-compiler:2.11.12 + | | +--- org.scala-lang:scala-library:2.11.12 + | | +--- org.scala-lang:scala-reflect:2.11.12 + | | | \--- org.scala-lang:scala-library:2.11.12 + | | +--- org.scala-lang.modules:scala-xml_2.11:1.0.5 + | | | \--- org.scala-lang:scala-library:2.11.7 -> 2.11.12 + | | \--- org.scala-lang.modules:scala-parser-combinators_2.11:1.0.4 + | | \--- org.scala-lang:scala-library:2.11.6 -> 2.11.12 + | +--- org.scala-lang:scala-library:2.11.12 + | +--- ch.epfl.scala:scalafix-core_2.11:0.9.1 + | | +--- org.scala-lang:scala-library:2.11.12 + | | +--- org.scalameta:contrib_2.11:4.1.0 + | | | +--- org.scala-lang:scala-library:2.11.12 + | | | \--- org.scalameta:scalameta_2.11:4.1.0 + | | | +--- org.scala-lang:scala-library:2.11.12 + | | | +--- org.scalameta:common_2.11:4.1.0 + | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | +--- org.scalameta:semanticdb_2.11:4.1.0 + | | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | | \--- com.thesamet.scalapb:scalapb-runtime_2.11:0.8.0-RC1 + | | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | | +--- com.thesamet.scalapb:lenses_2.11:0.8.0-RC1 + | | | | | | \--- org.scala-lang:scala-library:2.11.12 + | | | | | +--- com.lihaoyi:fastparse_2.11:1.0.0 + | | | | | | +--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | | | | | +--- com.lihaoyi:fastparse-utils_2.11:1.0.0 + | | | | | | | +--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | | | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 + | | | | | | | \--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 (*) + | | | | | \--- com.google.protobuf:protobuf-java:3.5.1 + | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 (*) + | | | +--- org.scalameta:dialects_2.11:4.1.0 + | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | \--- org.scalameta:common_2.11:4.1.0 (*) + | | | +--- org.scalameta:parsers_2.11:4.1.0 + | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | +--- org.scalameta:dialects_2.11:4.1.0 (*) + | | | | +--- org.scalameta:inputs_2.11:4.1.0 + | | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | | \--- org.scalameta:io_2.11:4.1.0 + | | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | | \--- org.scalameta:common_2.11:4.1.0 (*) + | | | | +--- org.scalameta:tokens_2.11:4.1.0 + | | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | | +--- org.scalameta:dialects_2.11:4.1.0 (*) + | | | | | \--- org.scalameta:inputs_2.11:4.1.0 (*) + | | | | +--- org.scalameta:tokenizers_2.11:4.1.0 + | | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | | +--- org.scalameta:dialects_2.11:4.1.0 (*) + | | | | | +--- org.scalameta:inputs_2.11:4.1.0 (*) + | | | | | +--- org.scalameta:tokens_2.11:4.1.0 (*) + | | | | | \--- org.scalameta:fastparse_2.11:1.0.0 + | | | | | +--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | | | | +--- org.scalameta:fastparse-utils_2.11:1.0.0 + | | | | | | +--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 (*) + | | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 (*) + | | | | \--- org.scalameta:trees_2.11:4.1.0 + | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | +--- org.scalameta:dialects_2.11:4.1.0 (*) + | | | | +--- org.scalameta:inputs_2.11:4.1.0 (*) + | | | | +--- org.scalameta:tokens_2.11:4.1.0 (*) + | | | | \--- org.scalameta:tokenizers_2.11:4.1.0 (*) + | | | +--- org.scalameta:quasiquotes_2.11:4.1.0 + | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | +--- org.scalameta:dialects_2.11:4.1.0 (*) + | | | | +--- org.scalameta:inputs_2.11:4.1.0 (*) + | | | | +--- org.scalameta:trees_2.11:4.1.0 (*) + | | | | \--- org.scalameta:parsers_2.11:4.1.0 (*) + | | | +--- org.scalameta:tokenizers_2.11:4.1.0 (*) + | | | +--- org.scalameta:transversers_2.11:4.1.0 + | | | | +--- org.scala-lang:scala-library:2.11.12 + | | | | +--- org.scalameta:common_2.11:4.1.0 (*) + | | | | \--- org.scalameta:trees_2.11:4.1.0 (*) + | | | +--- org.scalameta:trees_2.11:4.1.0 (*) + | | | +--- org.scalameta:inputs_2.11:4.1.0 (*) + | | | \--- org.scalameta:io_2.11:4.1.0 (*) + | | +--- org.scalameta:symtab_2.11:4.1.0 + | | | +--- org.scala-lang:scala-library:2.11.12 + | | | \--- org.scalameta:metacp_2.11:4.1.0 + | | | +--- org.scala-lang:scala-library:2.11.12 + | | | +--- org.scalameta:semanticdb_2.11:4.1.0 (*) + | | | +--- org.scalameta:cli_2.11:4.1.0 + | | | | \--- org.scala-lang:scala-library:2.11.12 + | | | +--- org.scalameta:io_2.11:4.1.0 (*) + | | | \--- org.scala-lang:scalap:2.11.12 + | | | \--- org.scala-lang:scala-compiler:2.11.12 (*) + | | +--- org.scalameta:metap_2.11:4.1.0 + | | | +--- org.scala-lang:scala-library:2.11.12 + | | | +--- org.scalameta:semanticdb_2.11:4.1.0 (*) + | | | +--- org.scalameta:cli_2.11:4.1.0 (*) + | | | \--- org.scalameta:inputs_2.11:4.1.0 (*) + | | +--- com.googlecode.java-diff-utils:diffutils:1.3.0 + | | \--- com.geirsson:metaconfig-typesafe-config_2.11:0.9.1 + | | +--- org.scala-lang:scala-library:2.11.12 + | | +--- com.geirsson:metaconfig-core_2.11:0.9.1 + | | | +--- org.scala-lang:scala-library:2.11.12 + | | | +--- com.lihaoyi:pprint_2.11:0.5.3 + | | | | +--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | | | +--- com.lihaoyi:fansi_2.11:0.2.5 + | | | | | +--- org.scala-lang:scala-library:2.11.8 -> 2.11.12 + | | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 (*) + | | | | \--- com.lihaoyi:sourcecode_2.11:0.1.4 (*) + | | | \--- org.typelevel:paiges-core_2.11:0.2.0 + | | | \--- org.scala-lang:scala-library:2.11.11 -> 2.11.12 + | | \--- com.typesafe:config:1.2.1 + | +--- ch.epfl.scala:scalafix-rules_2.11:0.9.1 + | | +--- org.scala-lang:scala-library:2.11.12 + | | \--- ch.epfl.scala:scalafix-core_2.11:0.9.1 (*) + | +--- org.scalameta:metacp_2.11:4.1.0 (*) + | \--- org.scala-lang:scala-reflect:2.11.12 (*) + +--- ch.epfl.scala:scalafix-interfaces:0.9.1 + +--- com.martiansoftware:nailgun-server:0.9.1 + +--- org.eclipse.jgit:org.eclipse.jgit:4.5.4.201711221230-r + | +--- com.jcraft:jsch:0.1.53 + | +--- com.googlecode.javaewah:JavaEWAH:0.7.9 + | +--- org.apache.httpcomponents:httpclient:4.3.6 + | | +--- org.apache.httpcomponents:httpcore:4.3.3 + | | +--- commons-logging:commons-logging:1.1.3 + | | \--- commons-codec:commons-codec:1.6 + | \--- org.slf4j:slf4j-api:1.7.2 -> 1.7.25 + +--- ch.qos.logback:logback-classic:1.2.3 + | +--- ch.qos.logback:logback-core:1.2.3 + | \--- org.slf4j:slf4j-api:1.7.25 + \--- org.apache.commons:commons-text:1.2 + \--- org.apache.commons:commons-lang3:3.7 + */ +}