-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
40 changed files
with
430 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
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 | ||
try { | ||
val relevantSynthetics = | ||
ctx.index.synthetics.filter(x => x.position.start >= lexicalStart && x.position.end <= lexicalEnd) | ||
|
||
val usesImplicitConversion = relevantSynthetics.exists(containsImplicitConversion) | ||
val usesSyntax = relevantSynthetics.exists(containsCatsSyntax) | ||
|
||
if (usesImplicitConversion) { | ||
// the import is used to enable an implicit conversion, | ||
// so we have to keep it | ||
Patch.empty | ||
} else if (usesSyntax) { | ||
// the import is 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) | ||
} | ||
} catch { | ||
case e: scalafix.v1.MissingSymbolException => | ||
// see https://github.com/typelevel/cats/pull/3566#issuecomment-684007028 | ||
// and https://github.com/scalacenter/scalafix/issues/1123 | ||
println(s"Skipping rewrite of 'import cats.implicits._' in file ${ctx.input.label} because we ran into a Scalafix bug. $e") | ||
e.printStackTrace() | ||
Patch.empty | ||
} | ||
}.asPatch | ||
|
||
private def removeImportLine(ctx: RuleCtx)(i: Import): Patch = | ||
ctx.removeTokens(i.tokens) + removeWhitespaceAndNewlineBefore(ctx)(i.tokens.start) | ||
|
||
private def containsImplicitConversion(synthetic: Synthetic) = | ||
synthetic.names.exists(x => isCatsKernelConversion(x.symbol)) | ||
|
||
private def isCatsKernelConversion(symbol: Symbol) = | ||
symbol.syntax.contains("cats/kernel") && symbol.syntax.contains("Conversion") | ||
|
||
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") || symbol.syntax.contains("Ops")) | ||
|
||
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) | ||
} | ||
|
||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
57 changes: 57 additions & 0 deletions
57
scalafix/v2_2_0/input/src/main/scala/fix/v2_2_0/RemoveInstanceImports.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]] | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
scalafix/v2_2_0/input/src/main/scala/fix/v2_2_0/RemoveInstanceImports2.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/* | ||
rule = "scala:fix.v2_2_0.RemoveInstanceImports" | ||
*/ | ||
package fix | ||
package to2_2_0 | ||
|
||
import cats.implicits._ | ||
|
||
object RemoveInstanceImportsTests2 { | ||
val x = "hello".some | ||
} |
19 changes: 19 additions & 0 deletions
19
scalafix/v2_2_0/input/src/main/scala/fix/v2_2_0/RemoveInstanceImports3.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
rule = "scala:fix.v2_2_0.RemoveInstanceImports" | ||
*/ | ||
package fix | ||
package to2_2_0 | ||
|
||
import cats._ | ||
import cats.implicits._ | ||
|
||
object RemoveInstanceImportsTests3 { | ||
|
||
def doSomethingMonadic[F[_]: Monad](x: F[Int]): F[String] = | ||
for { | ||
a <- x | ||
b <- Monad[F].pure("hi") | ||
c <- Monad[F].pure("hey") | ||
} yield a.toString + b + c | ||
|
||
} |
51 changes: 51 additions & 0 deletions
51
scalafix/v2_2_0/input/src/main/scala/fix/v2_2_0/RemoveInstanceImports4.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
rule = "scala:fix.v2_2_0.RemoveInstanceImports" | ||
*/ | ||
package fix | ||
package to2_2_0 | ||
|
||
import cats.implicits._ | ||
import cats.Order | ||
|
||
sealed trait Resolver extends Product with Serializable { | ||
import Resolver._ | ||
|
||
val path: String = { | ||
val url = this match { | ||
case MavenRepository(_, location, _) => location | ||
case IvyRepository(_, pattern, _) => pattern.takeWhile(!Set('[', '(')(_)) | ||
} | ||
url.replace(":", "") | ||
} | ||
} | ||
|
||
object Resolver { | ||
final case class Credentials(user: String, pass: String) | ||
|
||
final case class MavenRepository(name: String, location: String, credentials: Option[Credentials]) | ||
extends Resolver | ||
final case class IvyRepository(name: String, pattern: String, credentials: Option[Credentials]) | ||
extends Resolver | ||
|
||
val mavenCentral: MavenRepository = | ||
MavenRepository("public", "https://repo1.maven.org/maven2/", None) | ||
|
||
implicit val resolverOrder: Order[Resolver] = | ||
Order.by { | ||
case MavenRepository(name, location, _) => (1, name, location) | ||
case IvyRepository(name, pattern, _) => (2, name, pattern) | ||
} | ||
} | ||
|
||
final case class Scope[A](value: A, resolvers: List[Resolver]) | ||
|
||
object Scope { | ||
|
||
def combineByResolvers[A: Order](scopes: List[Scope[List[A]]]): List[Scope[List[A]]] = | ||
scopes.groupByNel(_.resolvers).toList.map { | ||
case (resolvers, group) => Scope(group.reduceMap(_.value).distinct.sorted, resolvers) | ||
} | ||
|
||
implicit def scopeOrder[A: Order]: Order[Scope[A]] = | ||
Order.by((scope: Scope[A]) => (scope.value, scope.resolvers)) | ||
} |
Oops, something went wrong.