diff --git a/README.md b/README.md index 0490da4..ed2b8ab 100644 --- a/README.md +++ b/README.md @@ -320,10 +320,10 @@ profiling { **Origin:** cats.Monad **Members:** ```scala - def flatMap[A, B](fa: F[A])(f: Function1[A, F[B]]): F[B] + def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] def pure[A](x: A): F[A] def flatten[A](ffa: F[F[A]]): F[A] - def iterateWhile[A](f: F[A])(p: Function1[A, Boolean]): F[A] + def iterateWhile[A](f: F[A])(p: A => Boolean): F[A] ... ``` diff --git a/fixtureScala2/src/cellar/fixture/scala2/CellarCurried.scala b/fixtureScala2/src/cellar/fixture/scala2/CellarCurried.scala new file mode 100644 index 0000000..d261aac --- /dev/null +++ b/fixtureScala2/src/cellar/fixture/scala2/CellarCurried.scala @@ -0,0 +1,6 @@ +package cellar.fixture.scala2 + +/** Fixture for signature rendering: unbounded type params + curried parameter lists. */ +trait CellarCurried { + def combine[A, B](a: A)(b: B): B +} diff --git a/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala b/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala new file mode 100644 index 0000000..5f654cb --- /dev/null +++ b/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala @@ -0,0 +1,14 @@ +package cellar.fixture.scala2 + +/** Fixture for signature rendering: function-type sugar + implicit lists. */ +trait CellarShow[A] { + def show(a: A): A +} + +trait CellarSugar { + def transform[A, B](f: A => B): B + def zip[A, B, C](f: (A, B) => C): C + def nested[A, B, C](f: (A => B) => C): C + def thunk[A](f: () => A): A + def withImplicit[A](a: A)(implicit s: CellarShow[A]): A +} diff --git a/fixtureScala3/src/cellar/fixture/scala3/CellarCurried.scala b/fixtureScala3/src/cellar/fixture/scala3/CellarCurried.scala new file mode 100644 index 0000000..c84ac82 --- /dev/null +++ b/fixtureScala3/src/cellar/fixture/scala3/CellarCurried.scala @@ -0,0 +1,5 @@ +package cellar.fixture.scala3 + +/** Fixture for signature rendering: unbounded type params + curried parameter lists. */ +trait CellarCurried: + def combine[A, B](a: A)(b: B): B diff --git a/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala b/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala new file mode 100644 index 0000000..0a24060 --- /dev/null +++ b/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala @@ -0,0 +1,14 @@ +package cellar.fixture.scala3 + +/** Fixture for signature rendering: function-type sugar + implicit/using lists. */ +trait CellarShow[A]: + def show(a: A): A + +trait CellarSugar: + def transform[A, B](f: A => B): B + def zip[A, B, C](f: (A, B) => C): C + def nested[A, B, C](f: (A => B) => C): C + def thunk[A](f: () => A): A + def ctx[A, B](f: A ?=> B): B + def withImplicit[A](a: A)(implicit s: CellarShow[A]): A + def withUsing[A](a: A)(using s: CellarShow[A]): A diff --git a/lib/src/cellar/TypePrinter.scala b/lib/src/cellar/TypePrinter.scala index 062dbc5..a9da09c 100644 --- a/lib/src/cellar/TypePrinter.scala +++ b/lib/src/cellar/TypePrinter.scala @@ -30,8 +30,16 @@ object TypePrinter: case _ => name case t: AppliedType => - val args = t.args.map(printTypeOrWildcard).mkString(", ") - s"${printType(t.tycon)}[$args]" + asFunction(t) match + case Some((contextual, params, result)) => + val arrow = if contextual then " ?=> " else " => " + val lhs = params match + case single :: Nil if !functionArgNeedsParens(single) => printTypeOrWildcard(single) + case _ => params.map(printTypeOrWildcard).mkString("(", ", ", ")") + s"$lhs$arrow${printTypeOrWildcard(result)}" + case None => + val args = t.args.map(printTypeOrWildcard).mkString(", ") + s"${printType(t.tycon)}[$args]" case t: ByNameType => s"=> ${printType(t.resultType)}" case t: AndType => s"${printType(t.first)} & ${printType(t.second)}" @@ -53,19 +61,25 @@ object TypePrinter: def printMethodic(tpe: TypeOrMethodic)(using ctx: Context): String = tpe match case t: MethodType => - val prefix = if t.isContextual then "using " else "" + val prefix = + if t.isContextual then "using " + else if t.isImplicit then "implicit " + else "" val params = t.paramNames.zip(t.paramTypes).map { (n, tp) => s"$n: ${printType(tp)}" } val paramStr = s"($prefix${params.mkString(", ")})" - s"$paramStr: ${printMethodic(t.resultType)}" + val rest = t.resultType match + case _: MethodType | _: PolyType => printMethodic(t.resultType) + case r => s": ${printMethodic(r)}" + s"$paramStr$rest" case t: PolyType => val typeParams = t.paramNames.zip(t.paramTypeBounds).map { (n, bounds) => bounds match case b: AbstractTypeBounds => - val lo = if b.low.toString == "Nothing" then "" else s" >: ${printType(b.low)}" - val hi = if b.high.toString == "Any" then "" else s" <: ${printType(b.high)}" + val lo = if printType(b.low) == "Nothing" then "" else s" >: ${printType(b.low)}" + val hi = if printType(b.high) == "Any" then "" else s" <: ${printType(b.high)}" s"$n$lo$hi" case _ => n.toString } @@ -87,7 +101,7 @@ object TypePrinter: case cls: ClassSymbol => val kind = if cls.isTrait then "trait" else if cls.isModuleClass then "object" else "class" val typeParams = printClassTypeParams(cls.typeParams) - val parents = cls.parents.map(printType).filter(p => p != "Object" && p != "Any") + val parents = cls.parents.map(printParent).filter(p => p != "Object" && p != "Any") val extendsStr = if parents.isEmpty then "" else s" extends ${parents.mkString(" with ")}" s"$kind ${cls.name}$typeParams$extendsStr" @@ -130,7 +144,41 @@ object TypePrinter: case _ => "?" case t: Type => printType(t) + /** Render a class parent, parenthesising function-arrow sugar so it is valid in `extends` position. */ + private def printParent(tpe: Type)(using ctx: Context): String = + val rendered = printType(tpe) + tpe match + case t: AppliedType if asFunction(t).isDefined => s"($rendered)" + case _ => rendered + private def isPackageOrNone(prefix: Type): Boolean = prefix match case _: ThisType => true case _ => false + + /** Decompose `scala.FunctionN` / `scala.ContextFunctionN` into (isContextual, params, result). */ + private def asFunction(t: AppliedType): Option[(Boolean, List[TypeOrWildcard], TypeOrWildcard)] = + t.tycon match + case tycon: TypeRef if isScalaPackage(tycon.prefix) => + val name = tycon.name.toString + val decoded = + if name.startsWith("ContextFunction") then Some((true, name.stripPrefix("ContextFunction"))) + else if name.startsWith("Function") then Some((false, name.stripPrefix("Function"))) + else None + decoded.flatMap { (contextual, digits) => + digits.toIntOption + .filter(arity => arity >= 0 && t.args.sizeIs == arity + 1) + .map(_ => (contextual, t.args.init, t.args.last)) + } + case _ => None + + private def isScalaPackage(prefix: Prefix): Boolean = + prefix match + case p: PackageRef => p.fullyQualifiedName.toString == "scala" + case _ => false + + /** A function-typed left operand of `=>` must be parenthesised: `(A => B) => C`. */ + private def functionArgNeedsParens(tow: TypeOrWildcard): Boolean = + tow match + case t: AppliedType => asFunction(t).isDefined + case _ => false diff --git a/lib/test/src/cellar/TypePrinterTest.scala b/lib/test/src/cellar/TypePrinterTest.scala index 6182c05..55d4b32 100644 --- a/lib/test/src/cellar/TypePrinterTest.scala +++ b/lib/test/src/cellar/TypePrinterTest.scala @@ -16,6 +16,15 @@ class TypePrinterTest extends CatsEffectSuite: result <- ContextResource.make(jars, jrePaths).use { (ctx, _) => body(ctx) } yield result + private def withScala2Ctx[A](body: Context => IO[A]): IO[A] = + TestFixtures.assumeFixturesAvailable() + for + jrePaths <- JreClasspath.jrtPath() + jars <- CoursierFetchClient.fetchClasspath( + TestFixtures.scala2Coord, Seq(TestFixtures.localM2Repo)) + result <- ContextResource.make(jars, jrePaths).use { (ctx, _) => body(ctx) } + yield result + test("detectLanguage returns Scala3 for scala3 fixture symbol"): withCtx { ctx => IO.blocking { @@ -99,3 +108,84 @@ class TypePrinterTest extends CatsEffectSuite: assert(sig.nonEmpty) } } + + test("printSymbolSignature renders unbounded type params and curried lists (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarCurried") + val combine = cls.declarations.find(_.name.toString == "combine").get + val sig = TypePrinter.printSymbolSignature(combine) + assertEquals(sig, "def combine[A, B](a: A)(b: B): B") + } + } + + test("printSymbolSignature renders unbounded type params and curried lists (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala2.CellarCurried") + val combine = cls.declarations.find(_.name.toString == "combine").get + val sig = TypePrinter.printSymbolSignature(combine) + assertEquals(sig, "def combine[A, B](a: A)(b: B): B") + } + } + + private def sugarSig(fqn: String, method: String)(using ctx: Context): String = + val cls = ctx.findStaticClass(fqn) + TypePrinter.printSymbolSignature(cls.declarations.find(_.name.toString == method).get) + + test("printSymbolSignature renders function types as arrow sugar (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val fqn = "cellar.fixture.scala3.CellarSugar" + assertEquals(sugarSig(fqn, "transform"), "def transform[A, B](f: A => B): B") + assertEquals(sugarSig(fqn, "zip"), "def zip[A, B, C](f: (A, B) => C): C") + assertEquals(sugarSig(fqn, "nested"), "def nested[A, B, C](f: (A => B) => C): C") + assertEquals(sugarSig(fqn, "thunk"), "def thunk[A](f: () => A): A") + assertEquals(sugarSig(fqn, "ctx"), "def ctx[A, B](f: A ?=> B): B") + } + } + + test("printSymbolSignature parenthesises a function-typed parent in extends position"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("scala.PartialFunction") + val sig = TypePrinter.printSymbolSignature(cls) + assert(sig.contains("extends (A => B)"), s"Expected parenthesised function parent in: $sig") + } + } + + test("printSymbolSignature renders implicit and using param lists (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val fqn = "cellar.fixture.scala3.CellarSugar" + assertEquals(sugarSig(fqn, "withImplicit"), "def withImplicit[A](a: A)(implicit s: CellarShow[A]): A") + assertEquals(sugarSig(fqn, "withUsing"), "def withUsing[A](a: A)(using s: CellarShow[A]): A") + } + } + + test("printSymbolSignature renders function types as arrow sugar (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + assertEquals( + sugarSig("cellar.fixture.scala2.CellarSugar", "transform"), + "def transform[A, B](f: A => B): B" + ) + } + } + + test("printSymbolSignature renders an implicit param list (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + assertEquals( + sugarSig("cellar.fixture.scala2.CellarSugar", "withImplicit"), + "def withImplicit[A](a: A)(implicit s: CellarShow[A]): A" + ) + } + }