Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scalafix rule to remove instance imports when upgrading to 2.2.0 #3566

Merged
merged 20 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
- stage: test
name: Scalafix tests
env: TEST="scalafix"
script: cd scalafix && sbt tests/test
script: cd scalafix && sbt test

- &bincompat
stage: test
Expand Down
30 changes: 21 additions & 9 deletions scalafix/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
# Scalafix rules for cats

## Try this!
## How to use

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

To run all rules that apply to version `1.0.0-RC1` run
2. Configure the SemanticDB compiler plugin to enable synthetics:
fthomas marked this conversation as resolved.
Show resolved Hide resolved

```
scalacOptions += "-P:semanticdb:synthetics:on"
```

3. Run the rules appropriate to your Cats version (see below)

## Migration to Cats v2.2.0

```sh
sbt scalafix github:typelevel/cats/v1.0.0?sha=v1.0.0-RC1
sbt scalafix github:typelevel/cats/Cats_v2_2_0
```

to run all rules that apply to the current `1.0.0-SNAPSHOT` run
### Available rules

- Type class instances are now available in implicit scope, so there is a rule to
remove imports that are no longer needed

## Migration to Cats v1.0.0

```sh
sbt scalafix github:typelevel/cats/v1.0.0
sbt scalafix github:typelevel/cats/Cats_v1_0_0
```

## Available rules
### Available rules

- [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.

Expand All @@ -40,7 +53,7 @@ sbt scalafix github:typelevel/cats/v1.0.0

- [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

# WIP
### WIP

- [ ] 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".

Expand All @@ -56,7 +69,6 @@ sbt scalafix github:typelevel/cats/v1.0.0
## To test scala fix

```bash
sbt coreJVM/publishLocal freeJVM/publishLocal
cd scalafix
sbt test
```
74 changes: 55 additions & 19 deletions scalafix/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,68 @@ lazy val rules = project.settings(
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion
)

lazy val input = project.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats" % "0.9.0"
),
scalacOptions += "-language:higherKinds"
)
lazy val v1_0_0_input = project.in(file("v1_0_0/input"))
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats" % "0.9.0"
),
scalacOptions += "-language:higherKinds"
)

lazy val output = project.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "1.0.0",
"org.typelevel" %% "cats-free" % "1.0.0"
),
scalacOptions += "-language:higherKinds"
)
lazy val v1_0_0_output = project.in(file("v1_0_0/output"))
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "1.0.0",
"org.typelevel" %% "cats-free" % "1.0.0"
),
scalacOptions ++= Seq(
"-language:higherKinds",
"-Ypartial-unification"
)
)

lazy val v1_0_0_tests = project.in(file("v1_0_0/tests"))
.settings(
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full,
compile.in(Compile) :=
compile.in(Compile).dependsOn(compile.in(v1_0_0_input, Compile)).value,
scalafixTestkitOutputSourceDirectories :=
sourceDirectories.in(v1_0_0_output, Compile).value,
scalafixTestkitInputSourceDirectories :=
sourceDirectories.in(v1_0_0_input, Compile).value,
scalafixTestkitInputClasspath :=
fullClasspath.in(v1_0_0_input, Compile).value
)
.dependsOn(v1_0_0_input, rules)
.enablePlugins(ScalafixTestkitPlugin)

lazy val v2_2_0_input = project.in(file("v2_2_0/input"))
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.1.0"
),
scalacOptions ++= Seq("-language:higherKinds", "-P:semanticdb:synthetics:on")
)

lazy val v2_2_0_output = project.in(file("v2_2_0/output"))
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.2.0-RC4"
),
scalacOptions += "-language:higherKinds"
)

lazy val tests = project
lazy val v2_2_0_tests = project.in(file("v2_2_0/tests"))
.settings(
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full,
compile.in(Compile) :=
compile.in(Compile).dependsOn(compile.in(input, Compile)).value,
compile.in(Compile).dependsOn(compile.in(v2_2_0_input, Compile)).value,
scalafixTestkitOutputSourceDirectories :=
sourceDirectories.in(output, Compile).value,
sourceDirectories.in(v2_2_0_output, Compile).value,
scalafixTestkitInputSourceDirectories :=
sourceDirectories.in(input, Compile).value,
sourceDirectories.in(v2_2_0_input, Compile).value,
scalafixTestkitInputClasspath :=
fullClasspath.in(input, Compile).value
fullClasspath.in(v2_2_0_input, Compile).value
)
.dependsOn(input, rules)
.dependsOn(v2_2_0_input, rules)
.enablePlugins(ScalafixTestkitPlugin)
68 changes: 68 additions & 0 deletions scalafix/rules/src/main/scala/fix/Cats_v2_2_0.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package fix
package v2_2_0

import scalafix.v0._
import scalafix.syntax._
import scala.meta._
import scala.meta.contrib._
import scala.meta.Term.Apply

// ref: https://github.com/typelevel/cats/issues/3563
case class RemoveInstanceImports(index: SemanticdbIndex)
extends SemanticRule(index, "RemoveInstanceImports") {

override def fix(ctx: RuleCtx): Patch = ctx.tree.collect {
// e.g. "import cats.instances.int._" or "import cats.instances.all._"
case i @ Import(Importer(Select(Select(Name("cats"), Name("instances")), x), _) :: _) =>
removeImportLine(ctx)(i)

// "import cats.implicits._"
case i @ Import(Importer(Select(Name("cats"), Name("implicits")), _) :: _) =>
val boundary = findLexicalBoundary(i)

// Find all synthetics between the import statement and the end of the lexical boundary
val lexicalStart = i.pos.end
val lexicalEnd = boundary.pos.end
val relevantSynthetics =
ctx.index.synthetics.filter(x => x.position.start >= lexicalStart && x.position.end <= lexicalEnd)

val usesSyntax = relevantSynthetics.exists(containsCatsSyntax)
if (usesSyntax) {
// the import is being used to enable an extension method,
// so replace it with "import cats.syntax.all._"
ctx.replaceTree(i, "import cats.syntax.all._")
} else {
// the import is only used to import instances,
// so it's safe to remove
removeImportLine(ctx)(i)
}
}.asPatch

private def removeImportLine(ctx: RuleCtx)(i: Import): Patch =
ctx.removeTokens(i.tokens) + removeWhitespaceAndNewlineBefore(ctx)(i.tokens.start)

private def containsCatsSyntax(synthetic: Synthetic) =
synthetic.names.exists(x => isCatsSyntax(x.symbol))

private def isCatsSyntax(symbol: Symbol) =
symbol.syntax.contains("cats") && symbol.syntax.contains("syntax")

private def findLexicalBoundary(t: Tree): Tree = {
t.parent match {
case Some(b: Term.Block) => b
case Some(t: Template) => t
case Some(parent) => findLexicalBoundary(parent)
case None => t
}
}

private def removeWhitespaceAndNewlineBefore(ctx: RuleCtx)(index: Int): Patch = {
val whitespaceAndNewlines = ctx.tokens.take(index).takeRightWhile(t =>
t.is[Token.Space] ||
t.is[Token.Tab] ||
t.is[Token.LF]
)
ctx.removeTokens(whitespaceAndNewlines)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
rule = "scala:fix.v2_2_0.RemoveInstanceImports"
*/
package fix
package to2_2_0

import cats.Semigroup
import scala.concurrent.Future

object RemoveInstanceImportsTests {
{
import cats.instances.option._
import cats.instances.int._
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.instances.all._
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.implicits._
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.instances.option._
import cats.instances.int._
import cats.syntax.semigroup._
Option(1) |+| Option(2)
}

{
import cats.implicits._
1.some |+| 2.some
}

{
import cats.instances.future._
import cats.instances.int._
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import cats.instances.all._
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package fix
package to2_2_0

import cats.Semigroup
import scala.concurrent.Future

object RemoveInstanceImportsTests {
{
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.syntax.semigroup._
Option(1) |+| Option(2)
}

{
import cats.syntax.all._
1.some |+| 2.some
}

{
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}
}
7 changes: 7 additions & 0 deletions scalafix/v2_2_0/tests/src/test/scala/fix/Cats_Tests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package fix

import scalafix.testkit._

class Cats_Tests extends SemanticRuleSuite() {
runAllTests()
}