Skip to content

Commit

Permalink
Add RemoveUnapply and RemoveCartesianBuilder Scalafix rewrites
Browse files Browse the repository at this point in the history
  • Loading branch information
gabro committed Aug 7, 2017
1 parent 0519906 commit d537b03
Show file tree
Hide file tree
Showing 13 changed files with 378 additions and 0 deletions.
1 change: 1 addition & 0 deletions scalafix/.scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
align = none
36 changes: 36 additions & 0 deletions scalafix/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Use a scala version supported by scalafix.
scalaVersion in ThisBuild := org.scalameta.BuildInfo.supportedScalaVersions.last

lazy val rewrites = project.settings(
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.5.0-M1"
)

lazy val input = project.settings(
scalametaSourceroot := sourceDirectory.in(Compile).value,
libraryDependencies ++= Seq(
"org.typelevel" %% "cats" % "0.9.0"
)
)

lazy val output = project.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "1.0.0-MF",
"org.typelevel" %% "cats-free" % "1.0.0-MF"
)
)

lazy val tests = project
.settings(
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % "0.5.0-M1" % Test cross CrossVersion.full,
buildInfoPackage := "fix",
buildInfoKeys := Seq[BuildInfoKey](
"inputSourceroot" ->
sourceDirectory.in(input, Compile).value,
"outputSourceroot" ->
sourceDirectory.in(output, Compile).value,
"inputClassdirectory" ->
classDirectory.in(input, Compile).value
)
)
.dependsOn(input, rewrites)
.enablePlugins(BuildInfoPlugin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
rewrite = "scala:fix.to1_0_0.RemoveCartesianBuilder"
*/
package fix
package to1_0_0

object RemoveCartesianBuilderTests {
{
import cats.instances.option._
import cats.syntax.cartesian._
val o1: Option[Int] = Some(42)
val o2: Option[String] = Some("hello")
val o3: Option[Int] = Some(2)
(o1 |@| o2).map((i: Int, s: String) => i.toString ++ s)
(o1 |@| o2).tupled
(o1 |@| o2 |@| o3).map(_ + _ + _)
}

{
import cats.{Semigroup, Eq}
import cats.implicits._
case class Foo(a: String, c: List[Double])

(Semigroup[String] |@| Semigroup[List[Double]])
.imap(Foo.apply)(Function.unlift(Foo.unapply))

(Eq[Double] |@| Eq[String]).contramap { (a: Foo) =>
(2, "bar")
}
}
}
44 changes: 44 additions & 0 deletions scalafix/input/src/main/scala/fix/to1_0_0/RemoveUnapply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
rewrite = "scala:fix.to1_0_0.RemoveUnapply"
*/
package fix
package to1_0_0

object RemoveUnapplyTests {
import cats.implicits._
import cats.Foldable
def parseInt(s: String): Either[String, Int] =
Either.catchOnly[NumberFormatException](s.toInt).leftMap(_ => "no number")
val ns = List("1", "2", "3")
ns.traverseU(parseInt)
ns.traverseU_(parseInt)
Foldable[List].traverseU_(ns)(parseInt)

import cats.data.{Validated, ValidatedNel}
val x: List[ValidatedNel[String, Int]] =
List(Validated.valid(1), Validated.invalid("a"), Validated.invalid("b"))
.map(_.toValidatedNel)
x.sequenceU
x.sequenceU_
Foldable[List].sequenceU_(x)

import cats.data.Func.{appFuncU, appFunc}
import cats.data.State.{get, set}
import cats.data.Const
type Count[A] = Const[Int, A]
def liftInt(i: Int): Count[Unit] = Const(i)
def isSpace(c: Char): Boolean = (c == ' ' || c == '\n')
def testIf(b: Boolean): Int = if (b) 1 else 0
appFuncU { (c: Char) =>
for {
x <- get[Boolean]
y = !isSpace(c)
_ <- set(y)
} yield testIf(y && !x)
} andThen appFunc(liftInt)

import cats.free.FreeT
val a: Either[String, Int] = Right(42)
FreeT.liftTU(a)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fix
package to1_0_0

object RemoveCartesianBuilderTests {
{
import cats.instances.option._
import cats.syntax.apply._
val o1: Option[Int] = Some(42)
val o2: Option[String] = Some("hello")
val o3: Option[Int] = Some(2)
(o1, o2).mapN((i: Int, s: String) => i.toString ++ s)
(o1, o2).tupled
(o1, o2, o3).mapN(_ + _ + _)
}

{
import cats.{Semigroup, Eq}
import cats.implicits._
case class Foo(a: String, c: List[Double])

(Semigroup[String], Semigroup[List[Double]])
.imapN(Foo.apply)(Function.unlift(Foo.unapply))

(Eq[Double], Eq[String]).contramapN { (a: Foo) =>
(2, "bar")
}
}
}
41 changes: 41 additions & 0 deletions scalafix/output/src/main/scala/fix/to1_0_0/RemoveUnapply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package fix
package to1_0_0

object RemoveUnapplyTests {
import cats.implicits._
import cats.Foldable
def parseInt(s: String): Either[String, Int] =
Either.catchOnly[NumberFormatException](s.toInt).leftMap(_ => "no number")
val ns = List("1", "2", "3")
ns.traverse(parseInt)
ns.traverse_(parseInt)
Foldable[List].traverse_(ns)(parseInt)

import cats.data.{Validated, ValidatedNel}
val x: List[ValidatedNel[String, Int]] =
List(Validated.valid(1), Validated.invalid("a"), Validated.invalid("b"))
.map(_.toValidatedNel)
x.sequence
x.sequence_
Foldable[List].sequence_(x)

import cats.data.Func.appFunc
import cats.data.State.{get, set}
import cats.data.Const
type Count[A] = Const[Int, A]
def liftInt(i: Int): Count[Unit] = Const(i)
def isSpace(c: Char): Boolean = (c == ' ' || c == '\n')
def testIf(b: Boolean): Int = if (b) 1 else 0
appFunc { (c: Char) =>
for {
x <- get[Boolean]
y = !isSpace(c)
_ <- set(y)
} yield testIf(y && !x)
} andThen appFunc(liftInt)

import cats.free.FreeT
val a: Either[String, Int] = Right(42)
FreeT.liftT(a)

}
1 change: 1 addition & 0 deletions scalafix/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.13
5 changes: 5 additions & 0 deletions scalafix/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.5.0-M1")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.5.0-M1")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC3")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.6.1")
addSbtPlugin("org.lyranthe.sbt" % "partial-unification" % "1.0.0")
29 changes: 29 additions & 0 deletions scalafix/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Scalafix rewrites for cats

## 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".

- [ ] cats.free.Inject is moved from cats-free to cats-core and renamed to cats.InjectK; cats.data.Prod is renamed to cats.data.Tuple2K; cats.data.Coproduct is renamed to cats.data.EitherK

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

- [ ] FunctorFilter, MonadCombine, MonadFilter, MonadReader, MonadState, MonadTrans, MonadWriter and TraverseFilter are no longer in cats, the functionalities they provided are inhereted by the new cats-mtl project. Please check here for migration guide.

- [x] CartesianBuilder (i.e. |@|) syntax is deprecated, use the apply syntax on tuples instead. E.g. (x |@| y |@| z).map(...) should be replaced by (x, y, z).mapN(...)

- [ ] Apply syntax on tuple (e.g. (x, y, z).map3(...)) was moved from cats.syntax.tuple._ to cats.syntax.apply._ and renamed to mapN, contramapN and imapN respectively.

- [ ] The creation methods (left, right, apply, pure, etc.) in EitherT were improved to take less type arguments.

- [ ] Several cats-core type class instances for cats.kernel were moved from their companion objects to separate traits and thus require imports from cats.instances.xxx._ (or the recommended import cats.implicits._) now. See #1659 for more details.

- [ ] Free.suspend is renamed to Free.defer for consistency.

- [ ] traverse1_, intercalate1 and sequence1_ in Reducible were renamed to nonEmptyTraverse_, nonEmptyIntercalate and nonEmptySequence_ respectively.

- [ ] foldLeftM is removed from Free, use foldM on Foldable instead, see #1117 for detail.

- [ ] iteratorFoldM was removed from Foldable due to #1716

- [ ] 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
14 changes: 14 additions & 0 deletions scalafix/rewrites/src/main/scala/fix/to1_0_0/All.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package fix
package to1_0_0

import scalafix._
import scala.meta._

case class All(mirror: Mirror) extends SemanticRewrite(mirror) {

def rewrite(ctx: RewriteCtx): Patch =
Seq(
RemoveUnapply(mirror),
RemoveCartesianBuilder(mirror)
).reduce(Rewrite.merge).rewrite(ctx)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package fix
package to1_0_0

import scalafix._
import scalafix.syntax._
import scala.meta.{Symbol => _, _}

// ref: https://github.com/typelevel/cats/pull/1745
case class RemoveCartesianBuilder(mirror: Mirror)
extends SemanticRewrite(mirror) {

private[this] val cartesianImport =
"cats."

private[this] val cartesianBuilders =
(1 to 22)
.map(arity =>
s"_root_.cats.syntax.CartesianBuilder#CartesianBuilder$arity.`|@|`.")
.toSet +
"_root_.cats.syntax.CartesianOps.`|@|`."

private[this] val cartesianFixes: Map[String, String] =
(1 to 22)
.map { arity =>
Seq(
s"_root_.cats.syntax.CartesianBuilder#CartesianBuilder$arity.map." -> "mapN",
s"_root_.cats.syntax.CartesianBuilder#CartesianBuilder$arity.imap." -> "imapN",
s"_root_.cats.syntax.CartesianBuilder#CartesianBuilder$arity.contramap." -> "contramapN"
)
}
.flatten
.toMap

private[this] def replace(
ctx: RewriteCtx,
t: Term.Name,
fixes: Map[String, String]): Patch = {
fixes.collect {
case (target, fix) if t.symbolOpt.exists(_.normalized.syntax == target) =>
ctx.replaceTree(t, fix)
}.asPatch
}

// Hackish to work around duplicate fixes due to recursion
val alreadyFixedOps = collection.mutable.Set.empty[Term.Name]
private[this] def replaceOpWithComma(ctx: RewriteCtx, op: Term.Name): Patch =
if (op.symbolOpt.exists(s =>
cartesianBuilders.contains(s.normalized.syntax)) && !alreadyFixedOps
.contains(op)) {
alreadyFixedOps += op
// remove the space before |@|
ctx.removeToken(ctx.tokenList.prev(op.tokens.head)) +
// replace |@| with ,
ctx.replaceTree(op, ",")
} else {
Patch.empty
}

private[this] def removeCartesianBuilderOp(
ctx: RewriteCtx,
applyInfix: Term.ApplyInfix): Patch = {
applyInfix match {
case Term.ApplyInfix(lhs: Term.ApplyInfix, op, _, _) =>
removeCartesianBuilderOp(ctx, lhs) + replaceOpWithComma(ctx, op)
case Term.ApplyInfix(_, op, _, _) =>
replaceOpWithComma(ctx, op)
}
}

def rewrite(ctx: RewriteCtx): Patch = {
ctx.tree.collect {
case t: Term.ApplyInfix => removeCartesianBuilderOp(ctx, t)
case Term.Select(_, fun) => replace(ctx, fun, cartesianFixes)
case t @ q"import cats.syntax.cartesian._" =>
ctx.replaceTree(t, "import cats.syntax.apply._")
}.asPatch
}
}
57 changes: 57 additions & 0 deletions scalafix/rewrites/src/main/scala/fix/to1_0_0/RemoveUnapply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package fix
package to1_0_0

import scalafix._
import scalafix.syntax._
import scala.meta.{Symbol => _, _}

// ref: https://github.com/typelevel/cats/pull/1583
case class RemoveUnapply(mirror: Mirror) extends SemanticRewrite(mirror) {
private[this] val fixes = Map(
"_root_.cats.Traverse.Ops.traverseU." -> "traverse",
"_root_.cats.Foldable.Ops.traverseU_." -> "traverse_",
"_root_.cats.Foldable.traverseU_." -> "traverse_",
"_root_.cats.Traverse.Ops.sequenceU." -> "sequence",
"_root_.cats.Foldable.Ops.sequenceU_." -> "sequence_",
"_root_.cats.Foldable.sequenceU_." -> "sequence_",
"_root_.cats.data.Func.appFuncU." -> "appFunc",
"_root_.cats.free.FreeT.liftTU." -> "liftT"
)

private[this] def replace(
ctx: RewriteCtx,
t: Term.Name,
fixes: Map[String, String]): Patch = {
fixes.collect {
case (target, fix) if t.symbolOpt.exists(_.normalized.syntax == target) =>
ctx.replaceTree(t, fix)
}.asPatch
}

private[this] def importeeName(importee: Importee): Option[Name] =
importee match {
case Importee.Name(name) => Some(name)
case Importee.Rename(name, _) => Some(name)
case _ => None
}

private[this] def removeImportee(
ctx: RewriteCtx,
importee: Importee,
fixes: Map[String, String]): Patch =
fixes.collect {
case (target, _)
if importeeName(importee)
.flatMap(_.symbolOpt)
.exists(_.normalized.syntax == target) =>
ctx.removeImportee(importee)
}.asPatch

def rewrite(ctx: RewriteCtx): Patch = {
ctx.tree.collect {
case Term.Select(_, fun) => replace(ctx, fun, fixes)
case Term.Apply(fun: Term.Name, _) => replace(ctx, fun, fixes)
case t: Importee.Name => removeImportee(ctx, t, fixes)
}.asPatch
}
}
13 changes: 13 additions & 0 deletions scalafix/tests/src/test/scala/fix/Cats_Tests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package fix

import scala.meta._
import scalafix.testkit._

class Cats_Tests
extends SemanticRewriteSuite(
Database.load(Classpath(AbsolutePath(BuildInfo.inputClassdirectory))),
AbsolutePath(BuildInfo.inputSourceroot),
Seq(AbsolutePath(BuildInfo.outputSourceroot))
) {
runAllTests()
}

0 comments on commit d537b03

Please sign in to comment.