diff --git a/build.sbt b/build.sbt index 7c82719..c0726b2 100644 --- a/build.sbt +++ b/build.sbt @@ -46,16 +46,6 @@ lazy val commonSettings = Seq( }, ) -lazy val fnGen = (project in file("fnGen")) - .settings(commonSettings) - .settings( - crossScalaVersions := Seq("2.12.15"), - scalaVersion := crossScalaVersions.value.head, - run / fork := true, // Needed if you run this project directly - libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, - libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value - ) - lazy val scalaJava8Compat = (project in file(".")) .settings(ScalaModulePlugin.scalaModuleSettings) .settings(ScalaModulePlugin.scalaModuleOsgiSettings) @@ -65,8 +55,6 @@ lazy val scalaJava8Compat = (project in file(".")) scalaModuleAutomaticModuleName := Some("scala.compat.java8"), ) .settings( - fork := true, // This must be set so that runner task is forked when it runs fnGen and the compiler gets a proper classpath - OsgiKeys.exportPackage := osgiExport(scalaVersion.value, version.value), OsgiKeys.privatePackage := List("scala.concurrent.java8.*"), @@ -77,30 +65,19 @@ lazy val scalaJava8Compat = (project in file(".")) libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test", - mimaBinaryIssueFilters ++= { - import com.typesafe.tools.mima.core._, ProblemFilters._ - Seq( - // bah - exclude[IncompatibleSignatureProblem]("*"), - // mysterious -- see scala/scala-java8-compat#211 - exclude[DirectMissingMethodProblem ]("scala.compat.java8.Priority1FunctionConverters.enrichAsJavaIntFunction"), - exclude[ReversedMissingMethodProblem]("scala.compat.java8.Priority1FunctionConverters.enrichAsJavaIntFunction"), - exclude[DirectMissingMethodProblem ]("scala.compat.java8.FunctionConverters.package.enrichAsJavaIntFunction" ), - exclude[ReversedMissingMethodProblem]("scala.compat.java8.FunctionConverters.package.enrichAsJavaIntFunction" ), - ) + // see https://github.com/scala/scala-java8-compat/issues/247 + versionPolicyPreviousVersions := versionPolicyPreviousVersions.value.flatMap { + case VersionNumber(Seq(0, _*), _, _) => Nil + case VersionNumber(Seq(1, 0, n, _*), _, _) if n <= 1 => Nil + case v => Seq(v) }, testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a"), (Compile / sourceGenerators) += Def.task { - val out = (Compile / sourceManaged).value - if (!out.exists) IO.createDirectory(out) - val canon = out.getCanonicalPath - val args = (new File(canon, "FunctionConverters.scala")).toString :: Nil - val runTarget = (fnGen / Compile / mainClass).value getOrElse "No main class defined for function conversion generator" - val classPath = (fnGen / Compile / fullClasspath).value - runner.value.run(runTarget, classPath.files, args, streams.value.log) - (out ** "*.scala").get + val f = (Compile / sourceManaged).value / "FunctionConverters.scala" + IO.write(f, WrapFnGen.code) + Seq(f) }.taskValue, Compile / sourceGenerators += Def.task { diff --git a/fnGen/WrapFnGen.scala b/fnGen/WrapFnGen.scala deleted file mode 100644 index 794c104..0000000 --- a/fnGen/WrapFnGen.scala +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Scala (https://www.scala-lang.org) - * - * Copyright EPFL and Lightbend, Inc. - * - * Licensed under Apache License 2.0 - * (http://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -object WrapFnGen { - val copyright = - s""" - |/* - | * Copyright EPFL and Lightbend, Inc. - | * This file auto-generated by WrapFnGen.scala. Do not modify directly. - | */ - |""".stripMargin - - val packaging = "package scala.compat.java8" - - import scala.tools.nsc._ - val settings = new Settings(msg => sys.error(msg)) - settings.usejavacp.value = true - val compiler = new Global(settings) - val run = new compiler.Run - - import compiler._, definitions._ - - - implicit class IndentMe(v: Vector[String]) { - def indent: Vector[String] = v.map(" " + _) - } - - implicit class FlattenMe(v: Vector[Vector[String]]) { - def mkVec(join: String = ""): Vector[String] = { - val vb = Vector.newBuilder[String] - var first = true - v.foreach{ vi => - if (!first) vb += join - first = false - vb ++= vi - } - vb.result() - } - } - - implicit class DoubleFlattenMe(v: Vector[Vector[Vector[String]]]) { - def mkVecVec(join: String = ""): Vector[String] = { - val vb = Vector.newBuilder[String] - var first = true - v.foreach{ vi => - if (!first) { vb += join; vb += join } - first = false - var ifirst = true - vi.foreach{ vj => - if (!ifirst) vb += join - ifirst = false - vb ++= vj - } - } - vb.result() - } - } - - implicit class SplitMyLinesAndStuff(s: String) { - def toVec = s.linesIterator.toVector - def nonBlank = s.trim.length > 0 - } - - implicit class TreeToText(t: Tree) { - def text = showCode(t).replace("$", "").linesIterator.toVector - } - - case class Prioritized(lines: Vector[String], priority: Int) { - def withPriority(i: Int) = copy(priority = i) - } - - case class SamConversionCode( - base: String, - wrappedAsScala: Vector[String], - asScalaAnyVal: Vector[String], - implicitToScala: Vector[String], - asScalaDef: Vector[String], - wrappedAsJava: Vector[String], - asJavaAnyVal: Vector[String], - implicitToJava: Prioritized, - asJavaDef: Vector[String] - ) { - def impls: Vector[Vector[String]] = Vector(wrappedAsScala, asScalaAnyVal, wrappedAsJava, asJavaAnyVal) - def defs: Vector[Vector[String]] = Vector(asScalaDef, asJavaDef) - def withPriority(i: Int): SamConversionCode = copy(implicitToJava = implicitToJava.withPriority(i)) - } - object SamConversionCode { - def apply(scc: SamConversionCode*): (Vector[String], Vector[Vector[String]]) = { - val sccDepthSet = scc.map(_.implicitToJava.priority).toSet - val codes = - { - if (sccDepthSet != (0 to sccDepthSet.max).toSet) { - val sccDepthMap = sccDepthSet.toList.sorted.zipWithIndex.toMap - scc.map(x => x.withPriority(sccDepthMap(x.implicitToJava.priority))) - } - else scc - }.toVector.sortBy(_.base) - def priorityName(n: Int, pure: Boolean = false): String = { - val pre = - if (n <= 0) - if (pure) "FunctionConverters" - else s"package object ${priorityName(n, pure = true)}" - else - if (pure) s"Priority${n}FunctionConverters" - else s"trait ${priorityName(n, pure = true)}" - if (!pure && n < (sccDepthSet.size-1)) s"$pre extends ${priorityName(n+1, pure = true)}" else pre - } - val impls = - "package functionConverterImpls {" +: { - codes.map(_.impls).mkVecVec().indent - } :+ "}" - val traits = codes.filter(_.implicitToJava.priority > 0).groupBy(_.implicitToJava.priority).toVector.sortBy(- _._1).map{ case (k,vs) => - s"${priorityName(k)} {" +: - s" import functionConverterImpls._" +: - s" " +: - vs.map(_.implicitToJava.lines).mkVec().indent :+ - s"}" - } - val explicitDefs = codes.map(_.defs).mkVecVec() - val packageObj = - s"${priorityName(0)} {" +: - s" import functionConverterImpls._" +: - s" " +: - { - explicitDefs.indent ++ - Vector.fill(3)(" ") ++ - codes.filter(_.implicitToJava.priority == 0).map(_.implicitToJava.lines).mkVec().indent ++ - Vector.fill(3)(" ") ++ - codes.map(_.implicitToScala).mkVec().indent - } :+ "}" - (impls, traits :+ packageObj) - } - } - - private def buildWrappersViaReflection: Seq[SamConversionCode] = { - - val pack: Symbol = rootMirror.getPackageIfDefined("java.util.function") - - case class Jfn(iface: Symbol, sam: Symbol) { - lazy val genericCount = iface.typeParams.length - lazy val name = sam.name.toTermName - lazy val title = iface.name.encoded - lazy val params = sam.info.params - lazy val sig = sam typeSignatureIn iface.info - lazy val pTypes = sig.params.map(_.info) - lazy val rType = sig.resultType - def arity = params.length - } - - val sams = pack.info.decls. - map(d => (d, d.typeSignature.members.filter(_.isAbstract).toList)). - collect{ case (d, m :: Nil) if d.isAbstract => Jfn(d, m) } - - def generate(jfn: Jfn): SamConversionCode = { - def mkRef(tp: Type): Tree = if (tp.typeSymbol.isTypeParameter) Ident(tp.typeSymbol.name.toTypeName) else tq"$tp" - - // Types for the Java SAM and the corresponding Scala function, plus all type parameters - val scalaType = gen.mkAttributedRef(FunctionClass(jfn.arity)) - val javaType = gen.mkAttributedRef(jfn.iface) - val tnParams: List[TypeName] = jfn.iface.typeParams.map(_.name.toTypeName) - val tdParams: List[TypeDef] = tnParams.map(TypeDef(NoMods, _, Nil, EmptyTree)) - val javaTargs: List[Tree] = tdParams.map(_.name).map(Ident(_)) - val scalaTargs: List[Tree] = jfn.pTypes.map(mkRef) :+ mkRef(jfn.rType) - - // Conversion wrappers have three or four components that we need to name - // (1) The wrapper class that wraps a Java SAM as Scala function, or vice versa (ClassN) - // (2) A value class that provides .asJava or .asScala to request the conversion (ValCN) - // (3) A name for an explicit conversion method (DefN) - // (4) An implicit conversion method name (ImpN) that invokes the value class - - // Names for Java conversions to Scala - val j2sClassN = TypeName("FromJava" + jfn.title) - val j2sValCN = TypeName("Rich" + jfn.title + "As" + scalaType.name.encoded) - val j2sDefN = TermName("asScalaFrom" + jfn.title) - val j2sImpN = TermName("enrichAsScalaFrom" + jfn.title) - - // Names for Scala conversions to Java - val s2jClassN = TypeName("AsJava" + jfn.title) - val s2jValCN = TypeName("Rich" + scalaType.name.encoded + "As" + jfn.title) - val s2jDefN = TermName("asJava" + jfn.title) - val s2jImpN = TermName("enrichAsJava" + jfn.title) - - // Argument lists for the function / SAM - val vParams = (jfn.params zip jfn.pTypes).map{ case (p,t) => - ValDef(NoMods, p.name.toTermName, if (t.typeSymbol.isTypeParameter) Ident(t.typeSymbol.name) else gen.mkAttributedRef(t.typeSymbol), EmptyTree) - } - val vParamRefs = vParams.map(_.name).map(Ident(_)) - - val j2sClassTree = - q"""class $j2sClassN[..$tdParams](jf: $javaType[..$javaTargs]) extends $scalaType[..$scalaTargs] { - def apply(..$vParams) = jf.${jfn.name}(..$vParamRefs) - }""" - - val j2sValCTree = - q"""class $j2sValCN[..$tdParams](private val underlying: $javaType[..$javaTargs]) extends AnyVal { - @inline def asScala: $scalaType[..$scalaTargs] = new $j2sClassN[..$tnParams](underlying) - }""" - - val j2sDefTree = - q"""@inline def $j2sDefN[..$tdParams](jf: $javaType[..$javaTargs]): $scalaType[..$scalaTargs] = new $j2sClassN[..$tnParams](jf)""" - - val j2sImpTree = - q"""@inline implicit def $j2sImpN[..$tdParams](jf: $javaType[..$javaTargs]): $j2sValCN[..$tnParams] = new $j2sValCN[..$tnParams](jf)""" - - val s2jClassTree = - q"""class $s2jClassN[..$tdParams](sf: $scalaType[..$scalaTargs]) extends $javaType[..$javaTargs] { - def ${jfn.name}(..$vParams) = sf.apply(..$vParamRefs) - }""" - - val s2jValCTree = - q"""class $s2jValCN[..$tdParams](private val underlying: $scalaType[..$scalaTargs]) extends AnyVal { - @inline def asJava: $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](underlying) - }""" - - val s2jDefTree = - q"""@inline def $s2jDefN[..$tdParams](sf: $scalaType[..$scalaTargs]): $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](sf)""" - - // This is especially tricky because functions are contravariant in their arguments - // Need to prevent e.g. Any => String from "downcasting" itself to Int => String; we want the more exact conversion - val s2jImpTree: (Tree, Int) = - if (jfn.pTypes.forall(! _.isFinalType) && jfn.sig == jfn.sam.typeSignature) - ( - q"""@inline implicit def $s2jImpN[..$tdParams](sf: $scalaType[..$scalaTargs]): $s2jValCN[..$tnParams] = new $s2jValCN[..$tnParams](sf)""", - tdParams.length - ) - else { - // Some types are not generic or are re-used; we had better catch those. - // Making up new type names, so switch everything to TypeName or TypeDef - // Instead of foo[A](f: (Int, A) => Long): Fuu[A] = new Foo[A](f) - // we want foo[X, A](f: (X, A) => Long)(implicit evX: Int =:= X): Fuu[A] = new Foo[A](f.asInstanceOf[(Int, A) => Long]) - // Instead of bar[A](f: A => A): Brr[A] = new Foo[A](f) - // we want bar[A, B](f: A => B)(implicit evB: A =:= B): Brr[A] = new Foo[A](f.asInstanceOf[A => B]) - val An = "A(\\d+)".r - val numberedA = collection.mutable.Set.empty[Int] - val evidences = collection.mutable.ArrayBuffer.empty[(TypeName, TypeName)] - numberedA ++= scalaTargs.map(_.toString).collect{ case An(digits) if (digits.length < 10) => digits.toInt } - val scalafnTnames = (jfn.pTypes :+ jfn.rType).zipWithIndex.map{ - case (pt, i) if (i < jfn.pTypes.length && pt.isFinalType) || (!pt.isFinalType && jfn.pTypes.take(i).exists(_ == pt)) => - val j = Iterator.from(i).dropWhile(numberedA).next() - val genericName = TypeName(s"A$j") - numberedA += j - evidences += ((genericName, pt.typeSymbol.name.toTypeName)) - genericName - case (pt, _) => pt.typeSymbol.name.toTypeName - } - val scalafnTdefs = scalafnTnames. - map(TypeDef(NoMods, _, Nil, EmptyTree)). - dropRight(if (jfn.rType.isFinalType) 1 else 0) - val evs = evidences.map{ case (generic, specific) => ValDef(NoMods, TermName("ev"+generic.toString), tq"$generic =:= $specific", EmptyTree) } - val tree = - q"""@inline implicit def $s2jImpN[..$scalafnTdefs](sf: $scalaType[..$scalafnTnames])(implicit ..$evs): $s2jValCN[..$tnParams] = - new $s2jValCN[..$tnParams](sf.asInstanceOf[$scalaType[..$scalaTargs]]) - """ - (tree, tdParams.length) - } - - SamConversionCode( - base = jfn.title, - wrappedAsScala = j2sClassTree.text, - asScalaAnyVal = j2sValCTree.text, - implicitToScala = j2sImpTree.text, - asScalaDef = j2sDefTree.text, - wrappedAsJava = s2jClassTree.text, - asJavaAnyVal = s2jValCTree.text, - implicitToJava = s2jImpTree match { case (t,d) => Prioritized(t.text, d) }, - asJavaDef = s2jDefTree.text - ) - } - - sams.toSeq.map(generate) - } - - lazy val converterContents = - s""" - |$copyright - | - |$packaging - | - |import language.implicitConversions - | - | - |""".stripMargin + - (SamConversionCode(buildWrappersViaReflection: _*) match { - case (impls, defs) => impls.mkString("\n") + "\n\n\n\n" + defs.map(_.mkString("\n")).mkString("\n\n\n\n") - }) - - def sameText(f: java.io.File, text: String): Boolean = { - val x = scala.io.Source.fromFile(f) - val lines = try { x.getLines.toVector } finally { x.close } - lines.iterator.filter(_.nonBlank) == text.linesIterator.filter(_.nonBlank) - } - - def write(f: java.io.File, text: String): Unit = { - if (!f.exists || !sameText(f, text)) { - val p = new java.io.PrintWriter(f) - try { p.println(text) } - finally { p.close() } - } - } - - def main(args: Array[String]): Unit = { - val names = args.iterator.map(x => new java.io.File(x)) - write(names.next(), converterContents) - } -} diff --git a/project/WrapFnGen.scala b/project/WrapFnGen.scala new file mode 100644 index 0000000..eaf4482 --- /dev/null +++ b/project/WrapFnGen.scala @@ -0,0 +1,236 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +import scala.collection.mutable + +object WrapFnGen { + /** all 43 interfaces in java.util.function package */ + private lazy val allJfn = Seq( + "BiConsumer[T, U]: accept(T, U): Unit", + "BiFunction[T, U, R]: apply(T, U): R", + "BiPredicate[T, U]: test(T, U): Boolean", + "BinaryOperator[T]: apply(T, T): T", + "BooleanSupplier: getAsBoolean: Boolean", + "Consumer[T]: accept(T): Unit", + "DoubleBinaryOperator: applyAsDouble(Double, Double): Double", + "DoubleConsumer: accept(Double): Unit", + "DoubleFunction[R]: apply(Double): R", + "DoublePredicate: test(Double): Boolean", + "DoubleSupplier: getAsDouble: Double", + "DoubleToIntFunction: applyAsInt(Double): Int", + "DoubleToLongFunction: applyAsLong(Double): Long", + "DoubleUnaryOperator: applyAsDouble(Double): Double", + "Function[T, R]: apply(T): R", + "IntBinaryOperator: applyAsInt(Int, Int): Int", + "IntConsumer: accept(Int): Unit", + "IntFunction[R]: apply(Int): R", + "IntPredicate: test(Int): Boolean", + "IntSupplier: getAsInt: Int", + "IntToDoubleFunction: applyAsDouble(Int): Double", + "IntToLongFunction: applyAsLong(Int): Long", + "IntUnaryOperator: applyAsInt(Int): Int", + "LongBinaryOperator: applyAsLong(Long, Long): Long", + "LongConsumer: accept(Long): Unit", + "LongFunction[R]: apply(Long): R", + "LongPredicate: test(Long): Boolean", + "LongSupplier: getAsLong: Long", + "LongToDoubleFunction: applyAsDouble(Long): Double", + "LongToIntFunction: applyAsInt(Long): Int", + "LongUnaryOperator: applyAsLong(Long): Long", + "ObjDoubleConsumer[T]: accept(T, Double): Unit", + "ObjIntConsumer[T]: accept(T, Int): Unit", + "ObjLongConsumer[T]: accept(T, Long): Unit", + "Predicate[T]: test(T): Boolean", + "Supplier[T]: get: T", + "ToDoubleBiFunction[T, U]: applyAsDouble(T, U): Double", + "ToDoubleFunction[T]: applyAsDouble(T): Double", + "ToIntBiFunction[T, U]: applyAsInt(T, U): Int", + "ToIntFunction[T]: applyAsInt(T): Int", + "ToLongBiFunction[T, U]: applyAsLong(T, U): Long", + "ToLongFunction[T]: applyAsLong(T): Long", + "UnaryOperator[T]: apply(T): T", + ).map(Jfn.apply) + + /** @param sig - ex: "BiConsumer[T,U]: accept(T,U): Unit" + * or "DoubleToIntFunction: applyAsInt(Double): Int" */ + case class Jfn(sig: String) { + val Array( + iface, // interface name included type args, ex: BiConsumer[T,U] | DoubleToIntFunction + _method, // Temp val, ex: accept(T,U) | applyAsInt(Double) + rType // java function return type, ex: Unit | Int + ) = sig.split(':').map(_.trim) + + // interface name and java interface's type args, + // ex: ("BiConsumer", "[T,U]") | ("DoubleToIntFunction", "") + val (ifaceName, jtargs) = iface.span(_ != '[') + + // java method name and temp val, ex: "accept" -> "(T,U)" | "applyAsInt" -> "(Double)" + val (jmethod, _targs) = _method.span(_ != '(') + + // java method's type args, ex: Seq("T", "U") | Seq("Double") + val pTypes: Seq[String] = _targs.unwrapMe + + // arguments names, ex: Seq("x1", "x2") + val args: Seq[String] = pTypes.indices.map { i => "x" + (i+1) } + // ex: "(x1: T, x2: U)" | "(x1: Double)" + val argsDecl: String = args.zip(pTypes).map { + // Don't really need this case. Only here so the generated code is + // exactly == the code gen by the old method using scala-compiler + scala-reflect + case (p, t @ ("Double"|"Long"|"Int")) => s"$p: scala.$t" + case (p, t) => s"$p: $t" + }.mkString("(", ", ", ")") + // ex: "(x1, x2)" + val argsCall: String = args.mkString("(", ", ", ")") + + // arity of scala.Function + val arity: Int = args.length + + // ex: "java.util.function.BiConsumer[T,U]" | "java.util.function.DoubleToIntFunction" + val javaFn = s"java.util.function.$iface" + + // ex: "scala.Function2[T, U, Unit]" | "scala.Function1[Double, Int]" + val scalaFn = s"scala.Function$arity[${(pTypes :+ rType).mkString(", ")}]" + + def fromJavaCls: String = + s"""class FromJava$iface(jf: $javaFn) extends $scalaFn { + | def apply$argsDecl = jf.$jmethod$argsCall + |}""".stripMargin + + val richAsFnClsName = s"Rich${ifaceName}AsFunction$arity$jtargs" + def richAsFnCls: String = + s"""class $richAsFnClsName(private val underlying: $javaFn) extends AnyVal { + | @inline def asScala: $scalaFn = new FromJava$iface(underlying) + |}""".stripMargin + + def asJavaCls: String = + s"""class AsJava$iface(sf: $scalaFn) extends $javaFn { + | def $jmethod$argsDecl = sf.apply$argsCall + |}""".stripMargin + + val richFnAsClsName = s"RichFunction${arity}As$iface" + def richFnAsCls: String = + s"""class $richFnAsClsName(private val underlying: $scalaFn) extends AnyVal { + | @inline def asJava: $javaFn = new AsJava$iface(underlying) + |}""".stripMargin + + def converterImpls: String = + s"""$fromJavaCls\n + |$richAsFnCls\n + |$asJavaCls\n + |$richFnAsCls\n + |""".stripMargin + + /** @return "implicit def enrichAsJavaXX.." code */ + def enrichAsJavaDef: String = { + // This is especially tricky because functions are contravariant in their arguments + // Need to prevent e.g. Any => String from "downcasting" itself to Int => String; we want the more exact conversion + // Instead of foo[A](f: (Int, A) => Long): Fuu[A] = new Foo[A](f) + // we want foo[X, A](f: (X, A) => Long)(implicit evX: Int =:= X): Fuu[A] = new Foo[A](f.asInstanceOf[(Int, A) => Long]) + // Instead of bar[A](f: A => A): Brr[A] = new Foo[A](f) + // we want bar[A, B](f: A => B)(implicit evB: A =:= B): Brr[A] = new Foo[A](f.asInstanceOf[A => B]) + + val finalTypes = Set("Double", "Long", "Int", "Boolean", "Unit") + val An = "A(\\d+)".r + val numberedA = mutable.Set.empty[Int] + val evidences = mutable.ArrayBuffer.empty[(String, String)] // ex: "A0" -> "Double" + numberedA ++= pTypes.collect{ case An(digits) if (digits.length < 10) => digits.toInt } + val scalafnTnames = (pTypes :+ rType).zipWithIndex.map { + case (pt, i) if i < pTypes.length && finalTypes(pt) || !finalTypes(pt) && pTypes.take(i).contains(pt) => + val j = Iterator.from(i).dropWhile(numberedA).next() + val genericName = s"A$j" + numberedA += j + evidences += (genericName -> pt) + genericName + case (pt, _) => pt + } + val scalafnTdefs = scalafnTnames.dropRight(if (finalTypes(rType)) 1 else 0).wrapMe() + val scalaFnGeneric = s"scala.Function${scalafnTnames.length - 1}[${scalafnTnames.mkString(", ")}]" + val evs = evidences + .map { case (generic, specific) => s"ev$generic: =:=[$generic, $specific]" } + .wrapMe("(implicit ", ")") + val sf = if (evs.isEmpty) "sf" else s"sf.asInstanceOf[$scalaFn]" + s"@inline implicit def enrichAsJava$ifaceName$scalafnTdefs(sf: $scalaFnGeneric)$evs: $richFnAsClsName = new $richFnAsClsName($sf)" + } + + def asScalaFromDef = s"@inline def asScalaFrom$iface(jf: $javaFn): $scalaFn = new FromJava$iface(jf)" + + def asJavaDef = s"@inline def asJava$iface(sf: $scalaFn): $javaFn = new AsJava$iface(sf)" + + def enrichAsScalaDef = s"@inline implicit def enrichAsScalaFrom$iface(jf: $javaFn): $richAsFnClsName = new $richAsFnClsName(jf)" + } + + def converters: String = { + val groups = allJfn + .map(jfn => jfn.jtargs.unwrapMe.length -> jfn.enrichAsJavaDef) + .groupBy(_._1) + .toSeq + .sortBy(_._1) + .reverse + val maxPriority = groups.head._1 + groups.map { case (priority, seq) => + val parent = + if (priority == maxPriority) "" + else s" extends Priority${priority + 1}FunctionConverters" + val me = + if (priority == 0) "package object FunctionConverters" + else s"trait Priority${priority}FunctionConverters" + + val enrichAsJava = seq.map(_._2) + val (asXx, enrichAsScala) = + if (priority != 0) Nil -> Nil + else allJfn.map { jfn => jfn.asScalaFromDef + "\n\n" + jfn.asJavaDef } -> + allJfn.map(_.enrichAsScalaDef) + + s"""$me$parent { + | import functionConverterImpls._ + |${asXx.mkString("\n\n\n").indentMe} + |${enrichAsJava.mkString("\n\n").indentMe} + |${enrichAsScala.mkString("\n\n").indentMe} + |}""".stripMargin + }.mkString("\n\n\n") + } + + def code: String = + s""" + |/* + | * Copyright EPFL and Lightbend, Inc. + | * This file auto-generated by WrapFnGen.scala. Do not modify directly. + | */ + | + |package scala.compat.java8 + | + |import language.implicitConversions + | + |package functionConverterImpls { + |${allJfn.map(_.converterImpls).mkString("\n").indentMe} + |} + |\n + |$converters + |""".stripMargin + + implicit class StringExt(private val s: String) extends AnyVal { + def indentMe: String = s.linesIterator.map(" " + _).mkString("\n") + def unwrapMe: Seq[String] = s match { + case "" => Nil + case _ => s + .substring(1, s.length - 1) // drop "(" and ")" or "[" and "]" + .split(',').map(_.trim).toSeq + } + } + + implicit class WrapMe(private val s: Seq[String]) extends AnyVal { + def wrapMe(start: String = "[", end: String = "]"): String = s match { + case Nil => "" + case _ => s.mkString(start, ", ", end) + } + } +} diff --git a/src/test/scala/scala/compat/java8/Issue247Test.scala b/src/test/scala/scala/compat/java8/Issue247Test.scala new file mode 100644 index 0000000..6a2ddc0 --- /dev/null +++ b/src/test/scala/scala/compat/java8/Issue247Test.scala @@ -0,0 +1,83 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.compat.java8 + +import org.junit.Assert._ +import org.junit.Assume.assumeTrue +import org.junit.Test +import org.junit.function.ThrowingRunnable + +import java.nio.file.{Files, Paths} +import scala.compat.java8.StreamConverters._ +import scala.compat.java8.issue247.Main +import scala.sys.process._ +import scala.util.Try + +class Issue247Test { + @Test + def runMainDirectly(): Unit = Main.main(Array.empty) + + val mainCls = "scala.compat.java8.issue247.Main" + + @Test + def runMainMatrix(): Unit = { + assumeTrue("only run in Linux/OSX", "which which".! == 0) + + val pwd = "pwd".!!.trim + + val coursier = Try { + ("which cs" #|| "which coursier").!!.trim + }.getOrElse { + val cs = s"$pwd/target/coursier" + if (!Files.isExecutable(Paths.get(cs))) + ( s"curl -fLo $cs https://git.io/coursier-cli" #&& + s"chmod +x $cs" + ).!.ensuring(_ == 0) + cs + } + + for { + scalaBinV <- Seq("2.11", "2.12", "2.13", "3") + compatV <- Seq("0.9.1", "1.0.0", "1.0.1") + // scala-java8-compat for scala3 don't have version 0.9.1 + if scalaBinV != "3" || compatV != "0.9.1" + scalaDir <- Files.list(Paths.get(pwd, "target")).toScala[List] + if scalaDir.toFile.getName.startsWith(s"scala-$scalaBinV") + classesDir = scalaDir.resolve("test-classes") + if classesDir.resolve("scala/compat/java8/issue247/Main.class").toFile.isFile + } { + val classpath = Process( + Seq( + coursier, // may contain spaces + "fetch", "--classpath", + s"org.scala-lang.modules:scala-java8-compat_$scalaBinV:$compatV" + ) + ).!!.trim + + val testCmd = s"java -cp $classpath:$classesDir $mainCls" + + val run: ThrowingRunnable = new ThrowingRunnable { + def run(): Unit = { + println(testCmd) + testCmd.!! + } + } + + if ((scalaBinV, compatV) == ("2.13", "0.9.1")) { + run.run() // no Exception + } else { + assertThrows(classOf[RuntimeException], run) + } + } + } +} diff --git a/src/test/scala/scala/compat/java8/issue247/Main.scala b/src/test/scala/scala/compat/java8/issue247/Main.scala new file mode 100644 index 0000000..25fcc29 --- /dev/null +++ b/src/test/scala/scala/compat/java8/issue247/Main.scala @@ -0,0 +1,27 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.compat.java8.issue247 + +import scala.compat.java8.FunctionConverters._ +import java.util.function.IntFunction + +object Main { + def invoke(jfun: IntFunction[String]): String = jfun(2) + + def main(args: Array[String]): Unit = { + val sfun = (i: Int) => s"ret: $i" + val ret = invoke(sfun.asJava) + assert(ret == "ret: 2") + println(s"OK. $ret") + } +}