Skip to content

Commit 1719f64

Browse files
authored
Scalafix rule to remove instance imports when upgrading to 2.2.0 (#3566)
* Bump scalafix * Move the v1.0.0 tests to a subdirectory * WIP scalafix rule to remove instance imports * Catch the low-hanging fruit * Support for removing "import cats.instances.all._" The import is only removed if there is no possibility it is being used to import Future instances. * Support for removing "import cats.implicits._" * Update the scalafix readme * Update test command in .travis.yml * Put sbt-scalafix back how it was Upgrading it broke one of the v1.0.0 rules and I can't be bothered investigating * No need to list all the positives, just check the negatives * Add -Ypartial-unification The v1.0.0 expected output files won't compile without this. * Remove special handling of Future instances * Rewrite "import cats.implicits._" to "import cats.syntax.all._" * Bump cats-core dependency in scalafix build.sbt This is so we get Future instances in implicit scope * Bump cats dependency to 2.2.0-RC4 * Add a test for rewriting of global imports i.e. imports at the top of the file * Work around a known Scalafix issue * Improve the check for use of extension methods * Handle implicit conversions Do not remove an import of cats.implicits._ if it is being used to import an implicit conversion * Reorganise readme
1 parent c2719aa commit 1719f64

40 files changed

+430
-30
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- stage: test
6464
name: Scalafix tests
6565
env: TEST="scalafix"
66-
script: cd scalafix && sbt tests/test
66+
script: cd scalafix && sbt test
6767

6868
- &bincompat
6969
stage: test

scalafix/README.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,37 @@
11
# Scalafix rules for cats
22

3-
## Try this!
3+
## How to use
44

5-
[Install the Scalafix sbt plugin](https://scalacenter.github.io/scalafix/docs/users/installation)
5+
1. [Install the Scalafix sbt plugin](https://scalacenter.github.io/scalafix/docs/users/installation)
66

7-
To run all rules that apply to version `1.0.0-RC1` run
7+
2. Run the rules appropriate to your Cats version (see below)
8+
9+
## Migration to Cats v2.2.0
10+
11+
First configure the SemanticDB compiler plugin to enable synthetics:
12+
13+
```
14+
ThisBuild / scalacOptions += "-P:semanticdb:synthetics:on"
15+
```
16+
17+
Then run Scalafix:
818

919
```sh
10-
sbt scalafix github:typelevel/cats/v1.0.0?sha=v1.0.0-RC1
20+
sbt scalafix github:typelevel/cats/Cats_v2_2_0
1121
```
1222

13-
to run all rules that apply to the current `1.0.0-SNAPSHOT` run
23+
### Available rules
24+
25+
- Type class instances are now available in implicit scope, so there is a rule to
26+
remove imports that are no longer needed
27+
28+
## Migration to Cats v1.0.0
1429

1530
```sh
16-
sbt scalafix github:typelevel/cats/v1.0.0
31+
sbt scalafix github:typelevel/cats/Cats_v1_0_0
1732
```
1833

19-
## Available rules
34+
### Available rules
2035

2136
- [x] All Unapply enabled methods, e.g. sequenceU, traverseU, etc. are removed. Unapply enabled syntax ops are also removed. Please use the partial unification SI-2712 fix instead. The easiest way might be this sbt-plugin.
2237

@@ -40,7 +55,7 @@ sbt scalafix github:typelevel/cats/v1.0.0
4055

4156
- [x] Split is removed, and the method split is moved to Arrow. Note that only under CommutativeArrow does it guarantee the non-interference between the effects. see #1567
4257

43-
# WIP
58+
### WIP
4459

4560
- [ ] cats no longer publishes the all-inclusive bundle package "org.typelevel" % "cats", use cats-core, cats-free, or cats-law accordingly instead. If you need cats.free, use "org.typelevel" % "cats-free", if you need cats-laws use "org.typelevel" % "cats-laws", if neither, use "org.typelevel" % "cats-core".
4661

@@ -53,10 +68,9 @@ sbt scalafix github:typelevel/cats/v1.0.0
5368
- [ ] iteratorFoldM was removed from Foldable due to #1716
5469

5570

56-
## To test scala fix
71+
## To test the Scalafix rules
5772

5873
```bash
59-
sbt coreJVM/publishLocal freeJVM/publishLocal
6074
cd scalafix
6175
sbt test
6276
```

scalafix/build.sbt

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,68 @@ lazy val rules = project.settings(
1111
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion
1212
)
1313

14-
lazy val input = project.settings(
15-
libraryDependencies ++= Seq(
16-
"org.typelevel" %% "cats" % "0.9.0"
17-
),
18-
scalacOptions += "-language:higherKinds"
19-
)
14+
lazy val v1_0_0_input = project.in(file("v1_0_0/input"))
15+
.settings(
16+
libraryDependencies ++= Seq(
17+
"org.typelevel" %% "cats" % "0.9.0"
18+
),
19+
scalacOptions += "-language:higherKinds"
20+
)
2021

21-
lazy val output = project.settings(
22-
libraryDependencies ++= Seq(
23-
"org.typelevel" %% "cats-core" % "1.0.0",
24-
"org.typelevel" %% "cats-free" % "1.0.0"
25-
),
26-
scalacOptions += "-language:higherKinds"
27-
)
22+
lazy val v1_0_0_output = project.in(file("v1_0_0/output"))
23+
.settings(
24+
libraryDependencies ++= Seq(
25+
"org.typelevel" %% "cats-core" % "1.0.0",
26+
"org.typelevel" %% "cats-free" % "1.0.0"
27+
),
28+
scalacOptions ++= Seq(
29+
"-language:higherKinds",
30+
"-Ypartial-unification"
31+
)
32+
)
33+
34+
lazy val v1_0_0_tests = project.in(file("v1_0_0/tests"))
35+
.settings(
36+
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full,
37+
compile.in(Compile) :=
38+
compile.in(Compile).dependsOn(compile.in(v1_0_0_input, Compile)).value,
39+
scalafixTestkitOutputSourceDirectories :=
40+
sourceDirectories.in(v1_0_0_output, Compile).value,
41+
scalafixTestkitInputSourceDirectories :=
42+
sourceDirectories.in(v1_0_0_input, Compile).value,
43+
scalafixTestkitInputClasspath :=
44+
fullClasspath.in(v1_0_0_input, Compile).value
45+
)
46+
.dependsOn(v1_0_0_input, rules)
47+
.enablePlugins(ScalafixTestkitPlugin)
48+
49+
lazy val v2_2_0_input = project.in(file("v2_2_0/input"))
50+
.settings(
51+
libraryDependencies ++= Seq(
52+
"org.typelevel" %% "cats-core" % "2.1.0"
53+
),
54+
scalacOptions ++= Seq("-language:higherKinds", "-P:semanticdb:synthetics:on")
55+
)
56+
57+
lazy val v2_2_0_output = project.in(file("v2_2_0/output"))
58+
.settings(
59+
libraryDependencies ++= Seq(
60+
"org.typelevel" %% "cats-core" % "2.2.0-RC4"
61+
),
62+
scalacOptions += "-language:higherKinds"
63+
)
2864

29-
lazy val tests = project
65+
lazy val v2_2_0_tests = project.in(file("v2_2_0/tests"))
3066
.settings(
3167
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full,
3268
compile.in(Compile) :=
33-
compile.in(Compile).dependsOn(compile.in(input, Compile)).value,
69+
compile.in(Compile).dependsOn(compile.in(v2_2_0_input, Compile)).value,
3470
scalafixTestkitOutputSourceDirectories :=
35-
sourceDirectories.in(output, Compile).value,
71+
sourceDirectories.in(v2_2_0_output, Compile).value,
3672
scalafixTestkitInputSourceDirectories :=
37-
sourceDirectories.in(input, Compile).value,
73+
sourceDirectories.in(v2_2_0_input, Compile).value,
3874
scalafixTestkitInputClasspath :=
39-
fullClasspath.in(input, Compile).value
75+
fullClasspath.in(v2_2_0_input, Compile).value
4076
)
41-
.dependsOn(input, rules)
77+
.dependsOn(v2_2_0_input, rules)
4278
.enablePlugins(ScalafixTestkitPlugin)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package fix
2+
package v2_2_0
3+
4+
import scalafix.v0._
5+
import scalafix.syntax._
6+
import scala.meta._
7+
import scala.meta.contrib._
8+
import scala.meta.Term.Apply
9+
10+
// ref: https://github.com/typelevel/cats/issues/3563
11+
case class RemoveInstanceImports(index: SemanticdbIndex)
12+
extends SemanticRule(index, "RemoveInstanceImports") {
13+
14+
override def fix(ctx: RuleCtx): Patch = ctx.tree.collect {
15+
// e.g. "import cats.instances.int._" or "import cats.instances.all._"
16+
case i @ Import(Importer(Select(Select(Name("cats"), Name("instances")), x), _) :: _) =>
17+
removeImportLine(ctx)(i)
18+
19+
// "import cats.implicits._"
20+
case i @ Import(Importer(Select(Name("cats"), Name("implicits")), _) :: _) =>
21+
val boundary = findLexicalBoundary(i)
22+
23+
// Find all synthetics between the import statement and the end of the lexical boundary
24+
val lexicalStart = i.pos.end
25+
val lexicalEnd = boundary.pos.end
26+
try {
27+
val relevantSynthetics =
28+
ctx.index.synthetics.filter(x => x.position.start >= lexicalStart && x.position.end <= lexicalEnd)
29+
30+
val usesImplicitConversion = relevantSynthetics.exists(containsImplicitConversion)
31+
val usesSyntax = relevantSynthetics.exists(containsCatsSyntax)
32+
33+
if (usesImplicitConversion) {
34+
// the import is used to enable an implicit conversion,
35+
// so we have to keep it
36+
Patch.empty
37+
} else if (usesSyntax) {
38+
// the import is used to enable an extension method,
39+
// so replace it with "import cats.syntax.all._"
40+
ctx.replaceTree(i, "import cats.syntax.all._")
41+
} else {
42+
// the import is only used to import instances,
43+
// so it's safe to remove
44+
removeImportLine(ctx)(i)
45+
}
46+
} catch {
47+
case e: scalafix.v1.MissingSymbolException =>
48+
// see https://github.com/typelevel/cats/pull/3566#issuecomment-684007028
49+
// and https://github.com/scalacenter/scalafix/issues/1123
50+
println(s"Skipping rewrite of 'import cats.implicits._' in file ${ctx.input.label} because we ran into a Scalafix bug. $e")
51+
e.printStackTrace()
52+
Patch.empty
53+
}
54+
}.asPatch
55+
56+
private def removeImportLine(ctx: RuleCtx)(i: Import): Patch =
57+
ctx.removeTokens(i.tokens) + removeWhitespaceAndNewlineBefore(ctx)(i.tokens.start)
58+
59+
private def containsImplicitConversion(synthetic: Synthetic) =
60+
synthetic.names.exists(x => isCatsKernelConversion(x.symbol))
61+
62+
private def isCatsKernelConversion(symbol: Symbol) =
63+
symbol.syntax.contains("cats/kernel") && symbol.syntax.contains("Conversion")
64+
65+
private def containsCatsSyntax(synthetic: Synthetic) =
66+
synthetic.names.exists(x => isCatsSyntax(x.symbol))
67+
68+
private def isCatsSyntax(symbol: Symbol) =
69+
symbol.syntax.contains("cats") && (symbol.syntax.contains("syntax") || symbol.syntax.contains("Ops"))
70+
71+
private def findLexicalBoundary(t: Tree): Tree = {
72+
t.parent match {
73+
case Some(b: Term.Block) => b
74+
case Some(t: Template) => t
75+
case Some(parent) => findLexicalBoundary(parent)
76+
case None => t
77+
}
78+
}
79+
80+
private def removeWhitespaceAndNewlineBefore(ctx: RuleCtx)(index: Int): Patch = {
81+
val whitespaceAndNewlines = ctx.tokens.take(index).takeRightWhile(t =>
82+
t.is[Token.Space] ||
83+
t.is[Token.Tab] ||
84+
t.is[Token.LF]
85+
)
86+
ctx.removeTokens(whitespaceAndNewlines)
87+
}
88+
89+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)