diff --git a/.gitignore b/.gitignore index 8d8e4a778..98ee81995 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,7 @@ project/plugins/project/ .worksheet .idea -# Auto-generated file. compiler-plugin/src/main/scala/scalapb/compiler/Version.scala e2e/project/project/Version.scala e2e/project/Version.scala e2e/.bin -atlassian-ide-plugin.xml diff --git a/.travis.yml b/.travis.yml index 83e1264a0..a1c77cbe3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ matrix: - sudo apt-get update - curl https://raw.githubusercontent.com/scala-native/scala-native/master/scripts/travis_setup.sh | bash -x script: - - sbt runtimeNative/test + - sbt runtimeNative/test lensesNative/test - scala: 2.11.12 env: TEST_SCRIPT=__misc__ @@ -77,4 +77,3 @@ deploy: tags: true scala: 2.11.12 condition: "$TRAVIS_PULL_REQUEST = false && $TEST_SCRIPT = __misc__" - diff --git a/README.md b/README.md index 60000c34c..e65f4ce4d 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,3 @@ The tests take a few minutes to run. There is a smaller test suite called ScalaChecks on the outputs. To run it: $ ./e2e.sh - diff --git a/build.sbt b/build.sbt index 9d74237c5..7661da014 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} val Scala211 = "2.11.12" scalaVersion in ThisBuild := Scala211 -crossScalaVersions in ThisBuild := Seq("2.10.7", Scala211, "2.12.4") +crossScalaVersions in ThisBuild := Seq("2.10.7", Scala211, "2.12.6") scalacOptions in ThisBuild ++= { CrossVersion.partialVersion(scalaVersion.value) match { @@ -37,7 +37,7 @@ releaseProcess := Seq[ReleaseStep]( setReleaseVersion, commitReleaseVersion, tagRelease, - releaseStepCommandAndRemaining(s";++${Scala211};runtimeNative/publishSigned"), + releaseStepCommandAndRemaining(s";++${Scala211};runtimeNative/publishSigned;lensesNative/publishSigned"), ReleaseStep(action = "publishSigned" :: _, enableCrossBuild = true), setNextVersion, commitNextVersion, @@ -53,7 +53,7 @@ lazy val root = publishLocal := {}, siteSubdirName in ScalaUnidoc := "api/scalapb/latest", addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), siteSubdirName in ScalaUnidoc), - unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(runtimeJVM, grpcRuntime), + unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(lensesJVM, runtimeJVM, grpcRuntime), git.remoteRepo := "git@github.com:scalapb/scalapb.github.io.git", ghpagesBranch := "master", ghpagesNoJekyll := false, @@ -61,6 +61,8 @@ lazy val root = ) .enablePlugins(ScalaUnidocPlugin, GhpagesPlugin) .aggregate( + lensesJS, + lensesJVM, runtimeJS, runtimeJVM, grpcRuntime, @@ -74,7 +76,6 @@ lazy val runtime = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "scalapb-runtime", libraryDependencies ++= Seq( - "com.thesamet.scalapb" %%% "lenses" % "0.7.0", "com.lihaoyi" %%% "fastparse" % "1.0.0", "com.lihaoyi" %%% "utest" % "0.6.4" % "test" ), @@ -89,6 +90,7 @@ lazy val runtime = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) } ) + .dependsOn(lenses) .platformsSettings(JSPlatform, NativePlatform)( libraryDependencies ++= Seq( "com.thesamet.scalapb" %%% "protobuf-runtime-scala" % "0.7.1" @@ -224,7 +226,6 @@ lazy val proptest = project.in(file("proptest")) "com.google.protobuf" % "protobuf-java" % protobufVersion, "io.grpc" % "grpc-netty" % grpcVersion % "test", "io.grpc" % "grpc-protobuf" % grpcVersion % "test", - "com.thesamet.scalapb" %% "lenses" % "0.7.0", "org.scalacheck" %% "scalacheck" % "1.13.5" % "test", "org.scalatest" %% "scalatest" % "3.0.5" % "test" ), @@ -274,3 +275,51 @@ createVersionFile := { val f2 = genVersionFile(base / "e2e/project/", v) log.info(s"Created $f2") } + +lazy val lenses = crossProject(JSPlatform, JVMPlatform, NativePlatform).in(file("lenses")) + .settings( + name := "lenses", + sources in Test := { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 13)) => + // TODO utest_2.13.0-M3 + Nil + case _ => + (sources in Test).value + }, + }, + testFrameworks += new TestFramework("utest.runner.Framework"), + libraryDependencies ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 13)) => + // TODO utest_2.13.0-M3 + Nil + case _ => + Seq( + "com.lihaoyi" %%% "utest" % "0.6.3" % "test" + ) + } + }, + mimaPreviousArtifacts := Set("com.thesamet.scalapb" %% "lenses" % "0.7.0"), + mimaBinaryIssueFilters ++= { + import com.typesafe.tools.mima.core._ + Seq( + ProblemFilters.exclude[IncompatibleMethTypeProblem]("scalapb.lenses.Lens#MapLens.:++=$extension"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("scalapb.lenses.Lens#MapLens.:++=") + ) + } + ) + .jsSettings( + scalacOptions += { + val a = (baseDirectory in LocalRootProject).value.toURI.toString + val g = "https://raw.githubusercontent.com/scalapb/ScalaPB/" + sys.process.Process("git rev-parse HEAD").lineStream_!.head + s"-P:scalajs:mapSourceURI:$a->$g/" + } + ) + .nativeSettings( + nativeLinkStubs := true // for utest + ) + +lazy val lensesJVM = lenses.jvm +lazy val lensesJS = lenses.js +lazy val lensesNative = lenses.native diff --git a/lenses/shared/src/main/scala/com/trueaccord/lenses/package.scala b/lenses/shared/src/main/scala/com/trueaccord/lenses/package.scala new file mode 100644 index 000000000..f7110cce3 --- /dev/null +++ b/lenses/shared/src/main/scala/com/trueaccord/lenses/package.scala @@ -0,0 +1,15 @@ +package com.trueaccord + +package object lenses { + @deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0") + type Updatable[A] = scalapb.lenses.Updatable[A] + + @deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0") + type Lens[Container, A] = scalapb.lenses.Lens[Container, A] + + @deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0") + type ObjectLens[U, Container] = scalapb.lenses.ObjectLens[U, Container] + + @deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0") + val Lens = scalapb.lenses.Lens +} \ No newline at end of file diff --git a/lenses/shared/src/main/scala/scalapb/lenses/Lenses.scala b/lenses/shared/src/main/scala/scalapb/lenses/Lenses.scala new file mode 100644 index 000000000..e78c8c361 --- /dev/null +++ b/lenses/shared/src/main/scala/scalapb/lenses/Lenses.scala @@ -0,0 +1,167 @@ +package scalapb.lenses + +import scala.language.higherKinds + +trait Lens[Container, A] extends Any { + self => + /** get knows how to extract some field of type `A` from a container */ + def get(c: Container): A + + /** Represents an assignment operator. + * + * Given a value of type A, sets knows how to transform a container such that `a` is + * assigned to the field. + * + * We must have get(set(a)(c)) == a + */ + def set(a: A): Mutation[Container] + + /** alias to set */ + def :=(a: A) = set(a) + + /** Represent an update operator (like x.y += 1 ) */ + def modify(f: A => A): Mutation[Container] = c => set(f(get(c)))(c) + + /** Composes two lenses, this enables nesting. + * + * If our field of type A has a sub-field of type B, then given a lens for it + * (other: Lens[A, B]) we can create a single lens from Container to B. + */ + def compose[B](other: Lens[A, B]): Lens[Container, B] = new Lens[Container, B] { + def get(c: Container) = other.get(self.get(c)) + + def set(b: B) = self.modify(other.set(b)) + } + + /** Given two lenses with the same origin, returns a new lens that can mutate both values + * represented by both lenses through a tuple. + */ + def zip[B](other: Lens[Container, B]): Lens[Container, (A, B)] = new Lens[Container, (A,B)] { + def get(c: Container): (A,B) = (self.get(c), other.get(c)) + def set(t: (A, B)): Mutation[Container] = self.set(t._1).andThen(other.set(t._2)) + } +} + +object Lens { + /* Create a Lens from getter and setter. */ + def apply[Container, A](getter: Container => A)(setter: (Container, A) => Container): Lens[Container, A] = new Lens[Container, A] { + def get(c: Container) = getter(c) + + def set(a: A): Mutation[Container] = setter(_, a) + } + + /** This is the unit lens, with respect to the compose operation defined above. That is, + * len.compose(unit) == len == unit.compose(len) + * + * More practically, you can view it as a len that mutates the entire object, instead of + * just a field of it: get() gives the original object, and set() returns the assigned value, + * no matter what the original value was. + */ + def unit[U]: Lens[U, U] = Lens(identity[U])((c, v) => v) + + /** Implicit that adds some syntactic sugar if our lens watches a Seq-like collection. */ + implicit class SeqLikeLens[U, A, Coll[A] <: collection.SeqLike[A, Coll[A]]](val lens: Lens[U, Coll[A]]) extends AnyVal { + type CBF = collection.generic.CanBuildFrom[Coll[A], A, Coll[A]] + + private def field(getter: Coll[A] => A)(setter: (Coll[A], A) => Coll[A]): Lens[U, A] = + lens.compose[A](Lens[Coll[A], A](getter)(setter)) + + def apply(i: Int)(implicit cbf: CBF): Lens[U, A] = field(_.apply(i))((c, v) => c.updated(i, v)) + + def head(implicit cbf: CBF): Lens[U, A] = apply(0) + + def last(implicit cbf: CBF): Lens[U, A] = field(_.last)((c, v) => c.updated(c.size - 1, v)) + + def :+=(item: A)(implicit cbf: CBF) = lens.modify(_ :+ item) + + def :++=(item: scala.collection.GenTraversableOnce[A])(implicit cbf: CBF) = lens.modify(_ ++ item) + + def foreach(f: Lens[A, A] => Mutation[A])(implicit cbf: CBF): Mutation[U] = + lens.modify(s => s.map { + (m: A) => + val field: Lens[A, A] = Lens.unit[A] + val p: Mutation[A] = f(field) + p(m) + }) + } + + /** Implicit that adds some syntactic sugar if our lens watches a Set-like collection. */ + implicit class SetLikeLens[U, A, Coll[A] <: collection.SetLike[A, Coll[A]] with Set[A]](val lens: Lens[U, Coll[A]]) extends AnyVal { + type CBF = collection.generic.CanBuildFrom[Coll[A], A, Coll[A]] + + private def field(getter: Coll[A] => A)(setter: (Coll[A], A) => Coll[A]): Lens[U, A] = + lens.compose[A](Lens[Coll[A], A](getter)(setter)) + + def :+=(item: A)(implicit cbf: CBF) = lens.modify(_ + item) + + def :++=(item: scala.collection.GenTraversableOnce[A])(implicit cbf: CBF) = lens.modify(_ ++ item) + + def foreach(f: Lens[A, A] => Mutation[A])(implicit cbf: CBF): Mutation[U] = + lens.modify(s => s.map { + (m: A) => + val field: Lens[A, A] = Lens.unit[A] + val p: Mutation[A] = f(field) + p(m) + }) + } + + /** Implicit that adds some syntactic sugar if our lens watches an Option[_]. */ + implicit class OptLens[U, A](val lens: Lens[U, Option[A]]) extends AnyVal { + def inplaceMap(f: Lens[A, A] => Mutation[A]) = + lens.modify(opt => opt.map { + (m: A) => + val field: Lens[A, A] = Lens.unit[A] + val p: Mutation[A] = f(field) + p(m) + }) + } + + /** Implicit that adds some syntactic sugar if our lens watches a Map[_, _]. */ + implicit class MapLens[U, A, B](val lens: Lens[U, Map[A, B]]) extends AnyVal { + def apply(key: A): Lens[U, B] = lens.compose(Lens[Map[A, B], B](_.apply(key))((map, value) => map.updated(key, value))) + + def :+=(pair: (A, B)) = lens.modify(_ + pair) + + def :++=(item: Iterable[(A, B)]) = lens.modify(_ ++ item) + + def foreach(f: Lens[(A, B), (A, B)] => Mutation[(A, B)]): Mutation[U] = + lens.modify(s => s.map { + (pair: (A, B)) => + val field: Lens[(A, B), (A, B)] = Lens.unit[(A, B)] + val p: Mutation[(A, B)] = f(field) + p(pair) + }) + + def foreachValue(f: Lens[B, B] => Mutation[B]): Mutation[U] = + lens.modify(s => s.mapValues { + (m: B) => + val field: Lens[B, B] = Lens.unit[B] + val p: Mutation[B] = f(field) + p(m) + }) + + def mapValues(f: B => B) = foreachValue(_.modify(f)) + } +} + +/** Represents a lens that has sub-lenses. */ +class ObjectLens[U, Container](self: Lens[U, Container]) extends Lens[U, Container] { + /** Creates a sub-lens */ + def field[A](lens: Lens[Container, A]): Lens[U, A] = self.compose(lens) + + /** Creates a sub-lens */ + def field[A](getter: Container => A)(setter: (Container, A) => Container): Lens[U, A] = + field(Lens(getter)(setter)) + + override def get(u: U) = self.get(u) + + override def set(c: Container) = self.set(c) + + def update(ms: (Lens[Container, Container] => Mutation[Container])*): Mutation[U] = + u => set(ms.foldLeft[Container](get(u))((p, m) => m(Lens.unit[Container])(p)))(u) +} + +trait Updatable[A] extends Any { + self: A => + def update(ms: (Lens[A, A] => Mutation[A])*): A = ms.foldLeft[A](self)((p, m) => m(Lens.unit[A])(p)) +} diff --git a/lenses/shared/src/main/scala/scalapb/lenses/package.scala b/lenses/shared/src/main/scala/scalapb/lenses/package.scala new file mode 100644 index 000000000..8173759fa --- /dev/null +++ b/lenses/shared/src/main/scala/scalapb/lenses/package.scala @@ -0,0 +1,5 @@ +package scalapb + +package object lenses { + type Mutation[C] = C => C +} diff --git a/lenses/shared/src/test/scala/scalapb/lenses/SimpleTest.scala b/lenses/shared/src/test/scala/scalapb/lenses/SimpleTest.scala new file mode 100644 index 000000000..5b5432a22 --- /dev/null +++ b/lenses/shared/src/test/scala/scalapb/lenses/SimpleTest.scala @@ -0,0 +1,230 @@ +package scalapb.lenses + +import utest._ +import scalapb.lenses._ + +case class Person(firstName: String, lastName: String, age: Int, address: Address) extends Updatable[Person] + +case class Address(street: String, + city: String, + state: String, + residents: Seq[Person] = Nil) extends Updatable[Address] + +case class Role(name: String, person: Person, replacement: Option[Person] = None) extends Updatable[Role] + +case class MapTest(intMap: Map[Int, String] = Map.empty, + nameMap: Map[String, Person] = Map.empty, + addressMap: Map[Person, Address] = Map.empty) extends Updatable[MapTest] + +case class CollectionTypes(iSeq: collection.immutable.Seq[String] = Nil, + vector: Vector[String] = Vector.empty, + list: List[String] = Nil, + sett: Set[String] = Set.empty) extends Updatable[CollectionTypes] + +object SimpleTest extends TestSuite { + + implicit class RoleMutation[U](f: Lens[U, Role]) extends ObjectLens[U, Role](f) { + def name = field(_.name)((p, f) => p.copy(name = f)) + + def person = field(_.person)((p, f) => p.copy(person = f)) + + def replacement = field(_.replacement)((p, f) => p.copy(replacement = f)) + } + + implicit class PersonMutation[U](f: Lens[U, Person]) extends ObjectLens[U, Person](f) { + def firstName = field(_.firstName)((p, f) => p.copy(firstName = f)) + + def lastName = field(_.lastName)((p, f) => p.copy(lastName = f)) + + def address: Lens[U, Address] = field(_.address)((p, f) => p.copy(address = f)) + } + + implicit class AddressLens[U](val f: Lens[U, Address]) extends ObjectLens[U, Address](f) { + def city = field(_.city)((p, f) => p.copy(city = f)) + + def street = field(_.street)((p, f) => p.copy(street = f)) + + def residents = field(_.residents)((p, f) => p.copy(residents = f)) + } + + implicit class MapTestLens[U](val f: Lens[U, MapTest]) extends ObjectLens[U, MapTest](f) { + def intMap = field(_.intMap)((p, f) => p.copy(intMap = f)) + + def nameMap = field(_.nameMap)((p, f) => p.copy(nameMap = f)) + + def addressMap = field(_.addressMap)((p, f) => p.copy(addressMap = f)) + } + + implicit class CollectionTypesLens[U](val f: Lens[U, CollectionTypes]) extends ObjectLens[U, CollectionTypes](f) { + def iSeq = field(_.iSeq)((p, f) => p.copy(iSeq = f)) + + def vector = field(_.vector)((p, f) => p.copy(vector = f)) + + def list = field(_.list)((p, f) => p.copy(list = f)) + + def sett = field(_.sett)((p, f) => p.copy(sett = f)) + } + + + object RoleMutation extends RoleMutation(Lens.unit) + + val mosh = Person(firstName = "Mosh", lastName = "Ben", age = 19, + address = Address("Main St.", "San Jose", "CA")) + val josh = Person(firstName = "Josh", lastName = "Z", age = 19, + address = Address("Fremont", "Sunnyvale", "CA")) + val chef = Role(name = "Chef", person=mosh) + + val mapTest = MapTest(intMap = Map(3 -> "three", 4 -> "four"), addressMap = Map( + mosh -> Address("someStreet", "someCity", "someState"))) + + val tests = Tests { + "update should return an updated object" - { + mosh.update(_.firstName := "foo") ==> (mosh.copy(firstName = "foo")) + } + + "it should allow mutating nested fields" - { + mosh.update(_.address.city := "Valejo") ==> (mosh.copy(address = mosh.address.copy(city = "Valejo"))) + } + + "it should allow nested updates" - { + mosh.update( + _.address.update( + _.city := "Valejo", + _.street := "Fourth" + ) + ) ==> (mosh.copy(address = mosh.address.copy(city = "Valejo", street = "Fourth"))) + } + + "it should allow replacing an entire field" - { + val portland = Address("2nd", "Portland", "Oregon") + mosh.update(_.address := portland) ==> (mosh.copy(address = portland)) + } + + "it should allow adding to a sequence" - { + mosh.update(_.address.residents :+= josh) ==> ( + mosh.copy( + address = mosh.address.copy( + residents = mosh.address.residents :+ josh))) + } + + "it should allow replacing a sequence" - { + mosh.update(_.address.residents := Seq(josh, mosh)) ==> ( + mosh.copy(address = + mosh.address.copy(residents = Seq(josh, mosh)))) + } + + "it should allow mutating an element of a sequence by index" - { + mosh.update( + _.address.residents := Seq(josh, mosh), + _.address.residents(1).firstName := "ModName") ==> ( + mosh.copy( + address = mosh.address.copy( + residents = Seq(josh, mosh.copy(firstName = "ModName"))))) + } + + "it should allow mutating all element of a sequence with forEach" - { + mosh.update( + _.address.residents := Seq(josh, mosh), + _.address.residents.foreach(_.lastName.modify(_ + "Suffix"))) ==> ( + mosh.copy( + address = mosh.address.copy( + residents = Seq( + josh.copy(lastName = "ZSuffix"), + mosh.copy(lastName = "BenSuffix"))))) + } + + "it should allow mapping over an option" - { + chef.update( + _.replacement.inplaceMap(_.firstName := "Zoo") + ) ==> (chef) + + chef.update( + _.replacement := Some(josh), + _.replacement.inplaceMap(_.firstName := "Yosh") + ).replacement.get ==> (josh.copy(firstName = "Yosh")) + } + + "it should allow updating a map" - { + mapTest.update(_.intMap(5) := "hello") ==> (mapTest.copy(intMap = mapTest.intMap.updated(5, "hello"))) + mapTest.update(_.intMap(2) := "ttt") ==> (mapTest.copy(intMap = mapTest.intMap.updated(2, "ttt"))) + mapTest.update(_.nameMap("mmm") := mosh) ==> (mapTest.copy(nameMap = mapTest.nameMap.updated("mmm", mosh))) + mapTest.update(_.addressMap(josh) := mosh.address) ==> (mapTest.copy(addressMap = mapTest.addressMap.updated(josh, mosh.address))) + } + + "it should allow nested updated in a map" - { + mapTest.update( + _.nameMap("mosh") := mosh, + _.nameMap("mosh").firstName := "boo") ==> ( + mapTest.copy(nameMap = mapTest.nameMap.updated("mosh", mosh.copy(firstName = "boo")))) + } + + "it should raise an exception on nested key update for a missing key" - { + intercept[NoSuchElementException] { + mapTest.update( + _.nameMap("mosh").firstName := "Boo" + ) + } + } + + "it should allow transforming the map values with forEachValue" - { + mapTest.update( + _.nameMap("mosh") := mosh, + _.nameMap("josh") := josh, + _.nameMap.foreachValue(_.firstName := "ttt") + ).nameMap.values.map(_.firstName) ==> (Seq("ttt", "ttt")) + } + + "it should allow transforming the map values with mapValues" - { + mapTest.update( + _.intMap.mapValues("hello " + _) + ).intMap ==> (Map(3 -> "hello three", 4 -> "hello four")) + + mapTest.update( + _.nameMap("mosh") := mosh, + _.nameMap("josh") := josh, + _.nameMap.mapValues(m => m.update(_.firstName := "*" + m.firstName)) + ).nameMap.values.map(_.firstName) ==> (Seq("*Mosh", "*Josh")) + } + + "it should allow transforming the map values with forEach" - { + mapTest.update( + _.intMap.foreach(_.modify(k => (k._1 - 1, "*" + k._2)))).intMap ==> Map( + 2 -> "*three", 3 -> "*four") + } + + "it should support other collection types" - { + val ct = CollectionTypes().update( + _.iSeq := collection.immutable.Seq("3","4","5"), + _.iSeq :+= "foo", + _.iSeq :++= collection.immutable.Seq("6", "7", "8"), + _.iSeq :++= Seq("6", "7", "8"), + _.iSeq(5) := "11", + _.vector := Vector("3","4","5"), + _.vector :+= "foo", + _.vector :++= collection.immutable.Seq("6", "7", "8"), + _.vector :++= Seq("6", "7", "8"), + _.vector(5) := "11", + _.list := List("3","4","5"), + _.list :+= "foo", + _.list :++= collection.immutable.Seq("6", "7", "8"), + _.list :++= Seq("6", "7", "8"), + _.list(5) := "11", + _.sett := Set("3","4","5"), + _.sett :+= "foo", + _.sett :++= collection.immutable.Seq("6", "7", "8"), + _.sett :++= Seq("6", "7", "8") + ) + val expected = Seq("3", "4", "5", "foo", "6", "11", "8", "6", "7", "8") + ct.iSeq ==> expected + ct.vector ==> expected + ct.list ==> expected + } + + "it should work with zipped lenses" - { + CollectionTypes().update( + k => k.list zip k.vector := ((List("3", "4"), Vector("x", "y"))) + ) ==> CollectionTypes(list = List("3", "4"), vector=Vector("x", "y")) + } + } +} + diff --git a/mima.sh b/mima.sh index b096208ba..3a9fba1c1 100755 --- a/mima.sh +++ b/mima.sh @@ -4,5 +4,6 @@ SCALA_VERSION=${SCALA_VERSION:-${TRAVIS_SCALA_VERSION:-2.11.11}} sbt ++$SCALA_VERSION \ grpcRuntime/mimaReportBinaryIssues \ + lensesJVM/mimaReportBinaryIssues \ runtimeJVM/mimaReportBinaryIssues \ compilerPlugin/mimaReportBinaryIssues