diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ba80d3c0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing + +## Build + +### Sbt Projects + +- `scala-collection-compat` project (in the root directory): implementation of the compatibility library ; +- In directory `scalafix/` there is an independent build containing the implementation of the migration tool. + +## Migration tool + +Several levels of contribution are possible! + +### Report a missing case + +Create an issue tagged with the +[migration](https://github.com/scala/collection-strawman/labels/migration) label. +Embrace `diff`s to describe differences between the standard collections and +the strawman: + +~~~ diff +- xs.toIterator ++ xs.iterator() +~~~ + +### Add a missing test case + +Even better, instead of providing a diff, you can directly add it as a test case! + +1. Fork this repository and create a separate branch; + +2. Add a file in the `scalafix/input/src/main/scala/fix/` directory with code + that uses the standard collections: + +~~~ scala +class toIteratorVsIterator(xs: Iterable[Int]) { + xs.toIterator +} +~~~ + +3. Add a corresponding file in the `scalafix/output/src/main/scala/fix/` directory + with the same code but using the strawman: + +~~~ scala +import strawman.collection.Iterable + +class toIteratorVsIterator(xs: Iterable[Int]) { + xs.iterator() +} +~~~ + +4. Check that your code example compiles + - run sbt from the `scalafix/` directory + and then run the following tasks `; input/compile ; output/compile`; + +5. Commit your changes, push your branch to your fork and create a pull request. + +Then maybe someone will take over and implement your use case… or maybe you will +(see next section)! + +### Implement a missing case + +Even better, complete the migration tool implementation to support the missing case! + +After you have added the missing case (see previous section), run the following +sbt task (with sbt started from the `scalafix/` directory) to run the +migration tool on the input files and check whether the result matches the +expected output files: + +~~~ +> tests/test +~~~ + +Fix the implementation of the rule (in the +`rules/src/main/scala/fix/Scalacollectioncompat_v0.scala` file) until the +tests are green. You can find more help about the scalafix API in its +[documentation](https://scalacenter.github.io/scalafix/docs/rule-authors/setup). diff --git a/README.md b/README.md index 94d35c61..e6717ba4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ [![Build Status](https://travis-ci.org/scala/scala-collection-compat.svg?branch=master)](https://travis-ci.org/scala/scala-collection-compat) -Scala 2.13 Collection Compatibility Library -=========================================== +Scala 2.13 Collection Compatibility Library And Migration Tool +============================================================== + +## Compatibility Library This library for Scala 2.12 provides limited compatibility with the new collection library in 2.13. We try to keep the 2.13 collections as backward compatible as possible but that is not always possible. For some of these cases this @@ -24,3 +26,25 @@ This project can be cross-built on 2.13 (with new collections) and 2.12. The 2.1 empty `scala.collection.compat` package object that allows you to write `import scala.collection.compat._` in 2.13. The 2.13 version has the compatibility extensions in this package. It also adds backported version of some new collection types to other `scala.collection` subpackages. + +## Migration Tool + +A tool is being developed to automatically migrate code that uses the standard +collection to use the strawman. + +To use it, add the [scalafix](https://scalacenter.github.io/scalafix/) sbt plugin +to your build, as explained in +[its documentation](https://scalacenter.github.io/scalafix/#Installation). + +The migration tool is not exhaustive and we will continue to improve +it over time. If you encounter a use case that’s not supported, please +report it as described in the +[contributing documentation](CONTRIBUTING.md#migration-tool). + +### Migrating a 2.12 code base to 2.13 + +Run the following sbt task on your project: + +~~~ +> scalafix github:scala/scala-collection-compat/v0 +~~~ diff --git a/scalafix/build.sbt b/scalafix/build.sbt new file mode 100644 index 00000000..16b2758c --- /dev/null +++ b/scalafix/build.sbt @@ -0,0 +1,43 @@ +def scalafixVersion = _root_.scalafix.Versions.version +inScope(Global)( + List( + scalaVersion := _root_.scalafix.Versions.scala212 + ) +) + +lazy val root = project + .in(file(".")) + .aggregate( + rules, input, output, tests + ) + +lazy val rules = project.settings( + libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % scalafixVersion +) + +lazy val input = project + .settings( + scalafixSourceroot := sourceDirectory.in(Compile).value + ) + +lazy val output = project + .settings( + resolvers += "scala-pr" at "https://scala-ci.typesafe.com/artifactory/scala-integration/", + scalaVersion := "2.13.0-M4-pre-20d3c21" + ) + +lazy val tests = project + .settings( + libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % scalafixVersion % 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, rules) + .enablePlugins(BuildInfoPlugin) diff --git a/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_Stream.scala b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_Stream.scala new file mode 100644 index 00000000..91492d1b --- /dev/null +++ b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_Stream.scala @@ -0,0 +1,14 @@ +/* +rule = "scala:fix.Collectionstrawman_v0" + */ +package fix + +object Collectionstrawman_v0_Stream { + val s = Stream(1, 2, 3) + s.append(List(4, 5, 6)) + 1 #:: 2 #:: 3 #:: Stream.Empty + val isEmpty: Stream[_] => Boolean = { + case Stream.Empty => true + case x #:: xs => false + } +} diff --git a/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_Traversable.scala b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_Traversable.scala new file mode 100644 index 00000000..604a3c2b --- /dev/null +++ b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_Traversable.scala @@ -0,0 +1,13 @@ +/* +rule = "scala:fix.Collectionstrawman_v0" + */ +package fix + +object Collectionstrawman_v0_Traversable { + def foo(xs: Traversable[(Int, String)], ys: List[Int]): Unit = { + xs.to[List] + xs.to[Set] + xs.toIterator + ys.iterator + } +} diff --git a/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_TupleNZipped.scala b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_TupleNZipped.scala new file mode 100644 index 00000000..6130a012 --- /dev/null +++ b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_TupleNZipped.scala @@ -0,0 +1,36 @@ +/* +rule = "scala:fix.Collectionstrawman_v0" + */ +package fix + +import scala.language.postfixOps +object Collectionstrawman_v0_Tuple2Zipped { + def zipped(xs: List[Int], ys: List[Int]): Unit = { + (xs, ys).zipped + (xs,ys).zipped + ((xs, ys) zipped) + (((xs) , (ys)).zipped) + (xs, // foo + ys).zipped + /* a */(/* b */ xs /* c */, /* d */ ys /* e */)/* f */./* g */zipped/* h */ + (coll(1), coll(2)).zipped + (List(1, 2, 3), Stream.from(1)).zipped + } + def coll(x: Int): List[Int] = ??? +} + +object Collectionstrawman_v0_Tuple3Zipped { + def zipped(xs: List[Int], ys: List[Int], zs: List[Int]): Unit = { + (xs, ys, zs).zipped + (xs,ys,zs).zipped + ((xs, ys, zs) zipped) + (((xs) , (ys) , (zs)).zipped) + (xs, // foo + ys, // bar + zs).zipped + /* a */(/* b */ xs /* c */, /* d */ ys /* e */, /* f */ zs /* g */)/* h */./* i */zipped/* j */ + (coll(1), coll(2), coll(3)).zipped + (List(1, 2, 3), Set(1, 2, 3), Stream.from(1)).zipped + } + def coll(x: Int): List[Int] = ??? +} \ No newline at end of file diff --git a/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_copyToBuffer.scala b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_copyToBuffer.scala new file mode 100644 index 00000000..a75c4fbe --- /dev/null +++ b/scalafix/input/src/main/scala/fix/Collectionstrawman_v0_copyToBuffer.scala @@ -0,0 +1,13 @@ +/* +rule = "scala:fix.Collectionstrawman_v0" + */ +package fix + +import scala.collection.mutable + +class Collectionstrawman_v0_copyToBuffer(xs: List[Int], b: mutable.Buffer[Int]) { + + xs.copyToBuffer(b) + (xs ++ xs).copyToBuffer(b) + +} diff --git a/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_Stream.scala b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_Stream.scala new file mode 100644 index 00000000..350077c8 --- /dev/null +++ b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_Stream.scala @@ -0,0 +1,11 @@ +package fix + +object Collectionstrawman_v0_Stream { + val s = LazyList(1, 2, 3) + s.lazyAppendAll(List(4, 5, 6)) + 1 #:: 2 #:: 3 #:: LazyList.Empty + val isEmpty: LazyList[_] => Boolean = { + case LazyList.Empty => true + case x #:: xs => false + } +} diff --git a/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_Traversable.scala b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_Traversable.scala new file mode 100644 index 00000000..5f53f9c0 --- /dev/null +++ b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_Traversable.scala @@ -0,0 +1,10 @@ +package fix + +object Collectionstrawman_v0_Traversable { + def foo(xs: Iterable[(Int, String)], ys: List[Int]): Unit = { + xs.to(List) + xs.to(Set) + xs.iterator() + ys.iterator() + } +} diff --git a/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_TupleNZipped.scala b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_TupleNZipped.scala new file mode 100644 index 00000000..e43cbc95 --- /dev/null +++ b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_TupleNZipped.scala @@ -0,0 +1,33 @@ +package fix + +import scala.language.postfixOps +object Collectionstrawman_v0_Tuple2Zipped { + def zipped(xs: List[Int], ys: List[Int]): Unit = { + xs.lazyZip(ys) + xs.lazyZip(ys) + (xs.lazyZip(ys) ) + ((xs).lazyZip((ys))) + xs.lazyZip(// foo + ys) + /* a *//* b */ xs /* c */.lazyZip(/* d */ ys /* e */)/* f *//* g *//* h */ + coll(1).lazyZip(coll(2)) + List(1, 2, 3).lazyZip(LazyList.from(1)) + } + def coll(x: Int): List[Int] = ??? +} + +object Collectionstrawman_v0_Tuple3Zipped { + def zipped(xs: List[Int], ys: List[Int], zs: List[Int]): Unit = { + xs.lazyZip(ys).lazyZip(zs) + xs.lazyZip(ys).lazyZip(zs) + (xs.lazyZip(ys).lazyZip(zs) ) + ((xs).lazyZip((ys)).lazyZip((zs))) + xs.lazyZip(// foo + ys).lazyZip(// bar + zs) + /* a *//* b */ xs /* c */.lazyZip(/* d */ ys /* e */).lazyZip(/* f */ zs /* g */)/* h *//* i *//* j */ + coll(1).lazyZip(coll(2)).lazyZip(coll(3)) + List(1, 2, 3).lazyZip(Set(1, 2, 3)).lazyZip(LazyList.from(1)) + } + def coll(x: Int): List[Int] = ??? +} \ No newline at end of file diff --git a/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_copyToBuffer.scala b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_copyToBuffer.scala new file mode 100644 index 00000000..8615a47a --- /dev/null +++ b/scalafix/output/src/main/scala/fix/Collectionstrawman_v0_copyToBuffer.scala @@ -0,0 +1,10 @@ +package fix + +import scala.collection.mutable + +class Collectionstrawman_v0_copyToBuffer(xs: List[Int], b: mutable.Buffer[Int]) { + + b ++= xs + b ++= xs ++ xs + +} diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties new file mode 100644 index 00000000..c091b86c --- /dev/null +++ b/scalafix/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.16 diff --git a/scalafix/project/plugins.sbt b/scalafix/project/plugins.sbt new file mode 100644 index 00000000..b23cfd7c --- /dev/null +++ b/scalafix/project/plugins.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.5.7") +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.6.1") \ No newline at end of file diff --git a/scalafix/rules/src/main/scala/fix/Scalacollectioncompat_v0.scala b/scalafix/rules/src/main/scala/fix/Scalacollectioncompat_v0.scala new file mode 100644 index 00000000..0ddb01a1 --- /dev/null +++ b/scalafix/rules/src/main/scala/fix/Scalacollectioncompat_v0.scala @@ -0,0 +1,116 @@ +package fix + +import scalafix._ +import scalafix.syntax._ +import scalafix.util._ +import scala.meta._ + +case class Scalacollectioncompat_v0(index: SemanticdbIndex) + extends SemanticRule(index, "Collectionstrawman_v0") { + + def replaceSymbols(ctx: RuleCtx): Patch = { + ctx.replaceSymbols( + "scala.Stream" -> "scala.LazyList", + "scala.collection.immutable.Stream" -> "scala.collection.immutable.LazyList", + "scala.Traversable" -> "scala.Iterable", + "scala.collection.Traversable" -> "scala.collection.Iterable", + "scala.TraversableOnce" -> "scala.IterableOnce", + "scala.collection.TraversableOnce" -> "scala.collection.IterableOnce" + ) + } + + val toTpe = SymbolMatcher.normalized( + Symbol("_root_.scala.collection.TraversableLike.to.") + ) + val iterator = SymbolMatcher.normalized( + Symbol("_root_.scala.collection.LinearSeqLike.iterator."), + Symbol("_root_.scala.collection.TraversableLike.toIterator.") + ) + val tupleZipped = SymbolMatcher.normalized( + Symbol("_root_.scala.runtime.Tuple2Zipped.Ops.zipped."), + Symbol("_root_.scala.runtime.Tuple3Zipped.Ops.zipped.") + ) + + def replaceToList(ctx: RuleCtx) = + ctx.tree.collect { + case iterator(t: Name) => + ctx.replaceTree(t, "iterator()") + case toTpe(n: Name) => + (for { + name <- n.tokens.lastOption + open <- ctx.tokenList.find(name)(t => t.is[Token.LeftBracket]) + close <- ctx.matchingParens.close(open.asInstanceOf[Token.LeftBracket]) + } yield + ctx.replaceToken(open, "(") + + ctx.replaceToken(close, ")") + ).asPatch + }.asPatch + + def replaceTupleZipped(ctx: RuleCtx) = + ctx.tree.collect { + case tupleZipped(Term.Select(Term.Tuple(args), name)) => + val removeTokensPatch = + (for { + zipped <- name.tokens.headOption + closeTuple <- ctx.tokenList.leading(zipped).find(_.is[Token.RightParen]) + openTuple <- ctx.matchingParens.open(closeTuple.asInstanceOf[Token.RightParen]) + maybeDot = ctx.tokenList.slice(closeTuple, zipped).find(_.is[Token.Dot]) + } yield { + ctx.removeToken(openTuple) + + maybeDot.map(ctx.removeToken).asPatch + + ctx.removeToken(zipped) + }).asPatch + + def removeSurroundingWhiteSpaces(tk: Token) = + (ctx.tokenList.trailing(tk).takeWhile(_.is[Token.Space]).map(ctx.removeToken) ++ + ctx.tokenList.leading(tk).takeWhile(_.is[Token.Space]).map(ctx.removeToken)).asPatch + + val commas = + for { + (prev, next) <- args.zip(args.tail) + tokensBetweenArgs = ctx.tokenList.slice(prev.tokens.last, next.tokens.head) + comma <- tokensBetweenArgs.find(_.is[Token.Comma]) + } yield comma + + val replaceCommasPatch = commas match { + case head :: tail => + ctx.replaceToken(head, ".lazyZip(") + + removeSurroundingWhiteSpaces(head) ++ + tail.map { comma => + ctx.replaceToken(comma, ").lazyZip(") + + removeSurroundingWhiteSpaces(comma) + } + case _ => Patch.empty + } + + removeTokensPatch + replaceCommasPatch + }.asPatch + + val copyToBuffer = SymbolMatcher.normalized( + Symbol("_root_.scala.collection.TraversableOnce.copyToBuffer.") + ) + + def replaceCopyToBuffer(ctx: RuleCtx): Patch = + ctx.tree.collect { + case t @ q"${copyToBuffer(Term.Select(collection, _))}($buffer)" => + ctx.replaceTree(t, q"$buffer ++= $collection".syntax) + }.asPatch + + val streamAppend = SymbolMatcher.normalized( + Symbol("_root_.scala.collection.immutable.Stream.append.") + ) + + def replaceStreamAppend(ctx: RuleCtx): Patch = + ctx.tree.collect { + case streamAppend(t: Name) => + ctx.replaceTree(t, "lazyAppendAll") + }.asPatch + + override def fix(ctx: RuleCtx): Patch = { + replaceToList(ctx) + + replaceSymbols(ctx) + + replaceTupleZipped(ctx) + + replaceCopyToBuffer(ctx) + + replaceStreamAppend(ctx) + } +} diff --git a/scalafix/tests/src/test/scala/fix/Collectionstrawman_Tests.scala b/scalafix/tests/src/test/scala/fix/Collectionstrawman_Tests.scala new file mode 100644 index 00000000..83e8edb8 --- /dev/null +++ b/scalafix/tests/src/test/scala/fix/Collectionstrawman_Tests.scala @@ -0,0 +1,17 @@ +package fix + +import scala.meta._ +import scalafix.testkit._ +import scalafix._ + +class Collectionstrawman_Tests + extends SemanticRuleSuite( + SemanticdbIndex.load(Classpath(AbsolutePath(BuildInfo.inputClassdirectory))), + AbsolutePath(BuildInfo.inputSourceroot), + Seq(AbsolutePath(BuildInfo.outputSourceroot)) + ) { + override def assertNoDiff(a: String, b: String, c: String) = { + super.assertNoDiff(a, b, c) + } + runAllTests() +}