From e5c550eefbd7e721b44f1a3692a3b2b0325b159c Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 24 Jul 2018 15:49:41 +0200 Subject: [PATCH 1/4] Preserve binary compatibility for Factory Replace the trait Factory by an abstract type upper-bounded by CanBuildFrom, this way usages of CanBuildFrom can be replaced by Factory without breaking binary compatibility. This has the unfortunate consequence that all methods available on CanBuildFrom are still available on Factory when compiling under old Scala versions, but that seems like a price worth paying for BC. --- .../scala/collection/compat/Factory.scala | 37 ------------------- .../collection/compat/PackageShared.scala | 27 ++++++++++++++ 2 files changed, 27 insertions(+), 37 deletions(-) delete mode 100644 compat/src/main/scala-2.11_2.12/scala/collection/compat/Factory.scala diff --git a/compat/src/main/scala-2.11_2.12/scala/collection/compat/Factory.scala b/compat/src/main/scala-2.11_2.12/scala/collection/compat/Factory.scala deleted file mode 100644 index 054e3de6..00000000 --- a/compat/src/main/scala-2.11_2.12/scala/collection/compat/Factory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package scala.collection.compat - -import scala.collection.generic.CanBuildFrom -import scala.collection.{TraversableOnce, mutable} - -/** - * A factory that builds a collection of type `C` with elements of type `A`. - * - * @tparam A Type of elements (e.g. `Int`, `Boolean`, etc.) - * @tparam C Type of collection (e.g. `List[Int]`, `TreeMap[Int, String]`, etc.) - */ -trait Factory[-A, +C] extends Any { - - /** - * @return A collection of type `C` containing the same elements - * as the source collection `it`. - * @param it Source collection - */ - def fromSpecific(it: TraversableOnce[A]): C - - /** Get a Builder for the collection. For non-strict collection types this will use an intermediate buffer. - * Building collections with `fromSpecific` is preferred because it can be lazy for lazy collections. */ - def newBuilder: mutable.Builder[A, C] -} - -object Factory { - - implicit def fromCanBuildFrom[A, C](implicit cbf: CanBuildFrom[Nothing, A, C]): Factory[A, C] = - new Factory[A, C] { - def fromSpecific(it: TraversableOnce[A]): C = (cbf() ++= it).result() - def newBuilder: mutable.Builder[A, C] = cbf() - } - - implicit def fromCanBuildFromConversion[X, A, C](x: X)(implicit toCanBuildFrom: X => CanBuildFrom[Nothing, A, C]): Factory[A, C] = - fromCanBuildFrom(toCanBuildFrom(x)) - -} diff --git a/compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala b/compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala index 72780986..fe18fdf9 100644 --- a/compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala +++ b/compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala @@ -10,6 +10,33 @@ import scala.{collection => c} private[compat] trait PackageShared { import CompatImpl._ + /** + * A factory that builds a collection of type `C` with elements of type `A`. + * + * @tparam A Type of elements (e.g. `Int`, `Boolean`, etc.) + * @tparam C Type of collection (e.g. `List[Int]`, `TreeMap[Int, String]`, etc.) + */ + type Factory[-A, +C] <: CanBuildFrom[Nothing, A, C] // Ideally, this would be an opaque type + + implicit class FactoryOps[-A, +C](private val factory: Factory[A, C]) { + /** + * @return A collection of type `C` containing the same elements + * as the source collection `it`. + * @param it Source collection + */ + def fromSpecific(it: TraversableOnce[A]): C = (factory() ++= it).result() + + /** Get a Builder for the collection. For non-strict collection types this will use an intermediate buffer. + * Building collections with `fromSpecific` is preferred because it can be lazy for lazy collections. */ + def newBuilder: m.Builder[A, C] = factory() + } + + implicit def fromCanBuildFrom[A, C](implicit cbf: CanBuildFrom[Nothing, A, C]): Factory[A, C] = + cbf.asInstanceOf[Factory[A, C]] + + implicit def fromCanBuildFromConversion[X, A, C](x: X)(implicit toCanBuildFrom: X => CanBuildFrom[Nothing, A, C]): Factory[A, C] = + fromCanBuildFrom(toCanBuildFrom(x)) + implicit def genericCompanionToCBF[A, CC[X] <: GenTraversable[X]]( fact: GenericCompanion[CC]): CanBuildFrom[Any, A, CC[A]] = simpleCBF(fact.newBuilder[A]) From 0436b16528c0af2d8ef7dd76fcb4b7abe3a88564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Mass=C3=A9?= Date: Thu, 26 Jul 2018 13:46:22 +0200 Subject: [PATCH 2/4] Test skeleton for binary compat checks --- .../new/src/main/scala/org.example/Lib.scala | 3 ++ .../old/src/main/scala/org.example/Lib.scala | 3 ++ .../test/src/test/scala/BinaryCompaTest.scala | 16 +++++++++ build.sbt | 34 ++++++++++++++++++- 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 binary-compat/new/src/main/scala/org.example/Lib.scala create mode 100644 binary-compat/old/src/main/scala/org.example/Lib.scala create mode 100644 binary-compat/test/src/test/scala/BinaryCompaTest.scala diff --git a/binary-compat/new/src/main/scala/org.example/Lib.scala b/binary-compat/new/src/main/scala/org.example/Lib.scala new file mode 100644 index 00000000..3084bba6 --- /dev/null +++ b/binary-compat/new/src/main/scala/org.example/Lib.scala @@ -0,0 +1,3 @@ +package org.example + +object Lib \ No newline at end of file diff --git a/binary-compat/old/src/main/scala/org.example/Lib.scala b/binary-compat/old/src/main/scala/org.example/Lib.scala new file mode 100644 index 00000000..3084bba6 --- /dev/null +++ b/binary-compat/old/src/main/scala/org.example/Lib.scala @@ -0,0 +1,3 @@ +package org.example + +object Lib \ No newline at end of file diff --git a/binary-compat/test/src/test/scala/BinaryCompaTest.scala b/binary-compat/test/src/test/scala/BinaryCompaTest.scala new file mode 100644 index 00000000..2c929fbf --- /dev/null +++ b/binary-compat/test/src/test/scala/BinaryCompaTest.scala @@ -0,0 +1,16 @@ +import org.junit.{Assert, Test} + +import build.BuildInfo._ + +import com.typesafe.tools.mima.lib.MiMaLib +import com.typesafe.tools.mima.core.Config + +class BinaryCompaTest { + @Test + def compat(): Unit = { + Config.setup("foo", Array(oldClasspath, newClasspath)) + val mima = new MiMaLib(Config.baseClassPath) + val allProblems = mima.collectProblems(oldClasspath, newClasspath) + Assert.assertTrue(allProblems.isEmpty) + } +} \ No newline at end of file diff --git a/build.sbt b/build.sbt index bca893c1..f93c19d3 100644 --- a/build.sbt +++ b/build.sbt @@ -14,6 +14,8 @@ lazy val root = project // == Core Libraries == +lazy val junit = libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test + lazy val compat = crossProject(JSPlatform, JVMPlatform) .withoutSuffixFor(JVMPlatform) .crossType(CrossType.Pure) @@ -32,7 +34,7 @@ lazy val compat = crossProject(JSPlatform, JVMPlatform) ) .jvmSettings( OsgiKeys.exportPackage := Seq(s"scala.collection.compat.*;version=${version.value}"), - libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test", + junit, javaHome in Compile := { val oldValue = (javaHome in Compile).value val isOnCi = sys.env.get("CI").isDefined @@ -65,6 +67,36 @@ lazy val compat = crossProject(JSPlatform, JVMPlatform) lazy val compatJVM = compat.jvm lazy val compatJS = compat.js +lazy val `binary-compat-old` = project + .in(file("binary-compat/old")) + .settings(scalaVersion := scala212) + .disablePlugins(ScalafixPlugin) + +lazy val `binary-compat-new` = project + .in(file("binary-compat/new")) + .settings(scalaVersion := scala212) + .dependsOn(compatJVM) + .disablePlugins(ScalafixPlugin) + +lazy val `binary-compat` = project + .in(file("binary-compat/test")) + .settings( + scalaVersion := scala212, + libraryDependencies += "com.typesafe" %% "mima-reporter" % "0.3.0" % Test, + junit, + buildInfoPackage := "build", + buildInfoKeys := Seq[BuildInfoKey]( + "oldClasspath" -> (classDirectory in (`binary-compat-old`, Compile)).value.toString, + "newClasspath" -> (classDirectory in (`binary-compat-new`, Compile)).value.toString + ), + test in Test := (test in Test).dependsOn( + compile in (`binary-compat-old`, Compile), + compile in (`binary-compat-new`, Compile) + ).value + ) + .enablePlugins(BuildInfoPlugin) + .disablePlugins(ScalafixPlugin) + lazy val scalafixRules = project .in(file("scalafix/rules")) .settings( From 57f0ab0d57c039dc73c497a84e3fe7f7e5177b21 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 26 Jul 2018 14:04:01 +0200 Subject: [PATCH 3/4] Add a test to check that Factory is binary-compatible --- .../new/src/main/scala/org.example/Lib.scala | 14 +++++++++++++- .../old/src/main/scala/org.example/Lib.scala | 13 ++++++++++++- .../test/src/test/scala/BinaryCompaTest.scala | 4 ++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/binary-compat/new/src/main/scala/org.example/Lib.scala b/binary-compat/new/src/main/scala/org.example/Lib.scala index 3084bba6..68c91ef7 100644 --- a/binary-compat/new/src/main/scala/org.example/Lib.scala +++ b/binary-compat/new/src/main/scala/org.example/Lib.scala @@ -1,3 +1,15 @@ package org.example -object Lib \ No newline at end of file +import scala.collection.generic._ +import scala.collection.compat._ + +object Lib { + def id[A, C[X] <: Iterable[X]](x: C[A])(implicit factory: Factory[A, C[A]]) = { + val builder = factory.newBuilder + builder ++= x + builder.result + } + + def test1 = + id(List(1, 2, 3)) +} diff --git a/binary-compat/old/src/main/scala/org.example/Lib.scala b/binary-compat/old/src/main/scala/org.example/Lib.scala index 3084bba6..fa51e475 100644 --- a/binary-compat/old/src/main/scala/org.example/Lib.scala +++ b/binary-compat/old/src/main/scala/org.example/Lib.scala @@ -1,3 +1,14 @@ package org.example -object Lib \ No newline at end of file +import scala.collection.generic._ + +object Lib { + def id[A, C[X] <: Iterable[X]](x: C[A])(implicit cbf: CanBuildFrom[C[A], A, C[A]]) = { + val builder = cbf() + builder ++= x + builder.result + } + + def test1 = + id(List(1, 2, 3)) +} diff --git a/binary-compat/test/src/test/scala/BinaryCompaTest.scala b/binary-compat/test/src/test/scala/BinaryCompaTest.scala index 2c929fbf..86511900 100644 --- a/binary-compat/test/src/test/scala/BinaryCompaTest.scala +++ b/binary-compat/test/src/test/scala/BinaryCompaTest.scala @@ -11,6 +11,6 @@ class BinaryCompaTest { Config.setup("foo", Array(oldClasspath, newClasspath)) val mima = new MiMaLib(Config.baseClassPath) val allProblems = mima.collectProblems(oldClasspath, newClasspath) - Assert.assertTrue(allProblems.isEmpty) + Assert.assertEquals(allProblems, Nil) } -} \ No newline at end of file +} From 72ac06b7811980897478da654c3f70b25626b1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Mass=C3=A9?= Date: Thu, 26 Jul 2018 14:13:27 +0200 Subject: [PATCH 4/4] Enable binary compat test in ci --- .travis.yml | 5 +++++ admin/build.sh | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 98d2b23a..601a2e8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,11 @@ matrix: scala: 2.12.6 env: TEST_SCALAFIX=true + # run binary compatibility test + - jdk: oraclejdk8 + scala: 2.12.6 + env: TEST_BINARY_COMPAT=true + # | jdk | scala | scala target | scala target version | scalafix test | # | ----------- | --------- | ------------ | -------------------- |---------------| # | openjdk6 | 2.11.12 | jvm | | | diff --git a/admin/build.sh b/admin/build.sh index 7993b08e..9060f9ee 100755 --- a/admin/build.sh +++ b/admin/build.sh @@ -22,7 +22,11 @@ if [ "$SCALAJS_VERSION" = "" ]; then if [[ "$TEST_SCALAFIX" == "true" ]]; then projectPrefix="scalafixRules" else - projectPrefix="compat" + if [[ "$TEST_BINARY_COMPAT" == "true" ]]; then + projectPrefix="binary-compat" + else + projectPrefix="compat" + fi fi else projectPrefix="compatJS"