diff --git a/README.md b/README.md index 0490da4..ac47d52 100644 --- a/README.md +++ b/README.md @@ -320,10 +320,11 @@ profiling { **Origin:** cats.Monad **Members:** ```scala - def flatMap[A, B](fa: F[A])(f: Function1[A, F[B]]): F[B] def pure[A](x: A): F[A] + def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] def flatten[A](ffa: F[F[A]]): F[A] - def iterateWhile[A](f: F[A])(p: Function1[A, Boolean]): F[A] + def untilM[G[_], A](f: F[A])(cond: => F[Boolean])(implicit G: Alternative[G]): F[G[A]] + def compose[G[_]](implicit evidence$1: Applicative[G]): Applicative[[α] =>> F[G[α]]] ... ``` @@ -333,7 +334,7 @@ profiling { cellar list-external org.typelevel:cats-core_3:2.10.0 cats --limit 5 object Eval$ - trait ComposedContravariantCovariant[F, G] extends Contravariant[TypeLambda] + trait ComposedContravariantCovariant[F, G] extends Contravariant[[α] =>> F[G[α]]] object Later$ object Show$ trait EvalSemigroup[A] extends Semigroup[Eval[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/CellarHigherKinded.scala b/fixtureScala2/src/cellar/fixture/scala2/CellarHigherKinded.scala new file mode 100644 index 0000000..83bccbe --- /dev/null +++ b/fixtureScala2/src/cellar/fixture/scala2/CellarHigherKinded.scala @@ -0,0 +1,8 @@ +package cellar.fixture.scala2 + +/** Fixture for signature rendering: higher-kinded type parameters. */ +trait CellarHigherKinded { + def wrap[F[_], A](fa: F[A]): F[A] + def bounded[F[X <: AnyRef]](fa: F[String]): F[String] + def bimap[G[_, _], A, B](g: G[A, B]): G[A, 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..069b35f --- /dev/null +++ b/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala @@ -0,0 +1,20 @@ +package cellar.fixture.scala2 + +/** Fixture for signature rendering: function-type sugar + implicit lists. */ +trait CellarShow[A] { + def show(a: A): A +} + +trait CellarSugar { + def wildcard: List[_] + def boundedWildcard: List[_ <: AnyRef] + + 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 pair[A, B](t: (A, B)): (A, B) + def triple[A, B, C](t: (A, B, C)): (A, B, C) + def tupleArg[A, B](f: ((A, B)) => Boolean): Boolean + 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/CellarHigherKinded.scala b/fixtureScala3/src/cellar/fixture/scala3/CellarHigherKinded.scala new file mode 100644 index 0000000..b8508d1 --- /dev/null +++ b/fixtureScala3/src/cellar/fixture/scala3/CellarHigherKinded.scala @@ -0,0 +1,15 @@ +package cellar.fixture.scala3 + +/** Fixture for signature rendering: higher-kinded type parameters. */ +trait CellarBox[F[_]] + +trait CellarBoundedBox[F[_ <: AnyRef]] + +trait CellarHigherKinded: + def wrap[F[_], A](fa: F[A]): F[A] + def compose[F[_], G[_]](bf: CellarBox[F], bg: CellarBox[G]): CellarBox[[A] =>> F[G[A]]] + def composeBounded[F[_]](bf: CellarBox[F]): CellarBoundedBox[[A <: AnyRef] =>> F[A]] + def bounded[F[X <: AnyRef]](fa: F[String]): F[String] + def upper[F[A] <: Iterable[A]](fa: F[Int]): F[Int] + def selfBounded[F[A <: Comparable[A]]](fa: F[String]): F[String] + def bimap[G[_, _], A, B](g: G[A, B]): G[A, 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..30b8b5c --- /dev/null +++ b/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala @@ -0,0 +1,25 @@ +package cellar.fixture.scala3 + +/** Fixture for signature rendering: function-type sugar + implicit/using lists. */ +trait CellarShow[A]: + def show(a: A): A + +/** Symbolic binary type constructor, to exercise infix rendering (e.g. `F ~> G`). */ +trait ~>[F[_], G[_]]: + def apply[A](fa: F[A]): G[A] + +trait CellarSugar: + def mapK[F[_], G[_]](f: F ~> G): Unit + def wildcard: List[?] + def boundedWildcard: List[? <: AnyRef] + + 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 pair[A, B](t: (A, B)): (A, B) + def triple[A, B, C](t: (A, B, C)): (A, B, C) + def tupleArg[A, B](f: ((A, B)) => Boolean): Boolean + 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..401c57b 100644 --- a/lib/src/cellar/TypePrinter.scala +++ b/lib/src/cellar/TypePrinter.scala @@ -25,13 +25,28 @@ object TypePrinter: t.prefix match case NoPrefix => name case _: ThisType => name - case p: Type if isPackageOrNone(p) => name + case p: Type if isElidedPrefix(p) => name case p: Type => s"${printType(p)}.$name" 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 => + asTuple(t) match + case Some(elems) => elems.map(printTypeOrWildcard).mkString("(", ", ", ")") + case None => + asInfix(t) match + case Some((lhs, op, rhs)) => + s"${printTypeOrWildcard(lhs)} $op ${printTypeOrWildcard(rhs)}" + 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)}" @@ -48,27 +63,29 @@ object TypePrinter: case t: ConstantType => t.value.value.toString case t: MatchType => s"${printType(t.scrutinee)} match { ... }" case t: FlexibleType => printType(t.nonNullableType) + case t: TypeLambda => + val params = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam) + s"[${params.mkString(", ")}] =>> ${printType(t.resultType)}" case _ => tpe.getClass.getSimpleName 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)}" - s"$n$lo$hi" - case _ => n.toString - } + val typeParams = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam) s"[${typeParams.mkString(", ")}]${printMethodic(t.resultType)}" case t: Type => printType(t) @@ -87,16 +104,14 @@ 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" case term: TermSymbol => val keyword = termKeyword(term) if term.isModuleVal then s"$keyword ${term.name}" - else - val sig = printMethodic(term.declaredType) - s"$keyword ${term.name}$sig" + else s"$keyword ${term.name}${printTopLevelMethodic(term.declaredType)}" case other => other.toString @@ -107,30 +122,130 @@ object TypePrinter: else if sym.isModuleVal then "object" else "val" - private def printClassTypeParams(params: List[ClassTypeParamSymbol]): String = + private def printTopLevelMethodic(tpe: TypeOrMethodic)(using ctx: Context): String = + tpe match + case t: Type => s": ${printType(t)}" + case t: PolyType => + val typeParams = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam) + s"[${typeParams.mkString(", ")}]${printTopLevelMethodic(t.resultType)}" + case t: MethodType => printMethodic(t) + + private def printClassTypeParams(params: List[ClassTypeParamSymbol])(using ctx: Context): String = if params.isEmpty then "" else - val rendered = params.map { p => - val variance = p.declaredVariance.productPrefix match - case "Covariant" => "+" - case "Contravariant" => "-" - case _ => "" - s"$variance${p.name}" - } + val rendered = params.map(printClassTypeParam) s"[${rendered.mkString(", ")}]" + private def printClassTypeParam(param: ClassTypeParamSymbol)(using ctx: Context): String = + val variance = param.declaredVariance.productPrefix match + case "Covariant" => "+" + case "Contravariant" => "-" + case _ => "" + s"$variance${printTypeParam(param.name, param.declaredBounds)}" + + private def printTypeParam(name: tastyquery.Names.TypeName, bounds: TypeBounds)(using ctx: Context): String = + bounds match + case b: AbstractTypeBounds => + b.high match + case tl: TypeLambda => s"$name${printHkParams(tl)}" + case _ => + 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"$name$lo$hi" + case _ => name.toString + + private def printHkParams(tl: TypeLambda)(using ctx: Context): String = + val rendered = tl.paramNames.zip(tl.paramTypeBounds).map { (name, bounds) => + val (lo, hi) = bounds match + case b: AbstractTypeBounds => + 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)}" + (lo, hi) + case _ => ("", "") + (name.toString, lo, hi) + } + // A parameter must be named (not `_`) when it is referenced elsewhere in the lambda: + // in a bound (self-referential, e.g. `A <: Comparable[A]`) or in the constructor + // bound (the lambda's result, e.g. `F[A] <: Iterable[A]`). + val resultStr = printType(tl.resultType) + val context = (resultStr :: rendered.flatMap((_, lo, hi) => List(lo, hi))).mkString(" ") + val params = rendered.map { (name, lo, hi) => + val head = if isMentioned(context, name) then name else "_" + s"$head$lo$hi" + } + val constructorBound = if resultStr == "Any" then "" else s" <: $resultStr" + s"[${params.mkString(", ")}]$constructorBound" + + private def isMentioned(context: String, name: String): Boolean = + context.matches(s"(?s).*\\b${java.util.regex.Pattern.quote(name)}\\b.*") + private def printTypeOrWildcard(tow: TypeOrWildcard)(using ctx: Context): String = tow match case w: WildcardTypeArg => w.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)}" if lo.isEmpty && hi.isEmpty then "?" else s"?$lo$hi" case _ => "?" case t: Type => printType(t) - private def isPackageOrNone(prefix: Type): Boolean = + /** 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 isElidedPrefix(prefix: Type): Boolean = prefix match case _: ThisType => true + case t: TermRef => t.name.toString == "Predef" || t.name.toString == "package" 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 + + /** Decompose `scala.TupleN` (arity >= 2) into its element types. */ + private def asTuple(t: AppliedType): Option[List[TypeOrWildcard]] = + t.tycon match + case tycon: TypeRef if isScalaPackage(tycon.prefix) => + tycon.name.toString.stripPrefix("Tuple").toIntOption + .filter(arity => arity >= 2 && t.args.sizeIs == arity) + .map(_ => t.args) + case _ => None + + /** Decompose a binary applied type whose tycon is a symbolic operator (e.g. `F ~> G`). */ + private def asInfix(t: AppliedType): Option[(TypeOrWildcard, String, TypeOrWildcard)] = + t.tycon match + case tycon: TypeRef if t.args.sizeIs == 2 && isOperatorName(tycon.name.toString) => + Some((t.args.head, tycon.name.toString, t.args(1))) + case _ => None + + /** An identifier composed solely of operator characters (no letters/digits/`_`/`$`). */ + private def isOperatorName(name: String): Boolean = + name.nonEmpty && name.forall(c => !c.isLetterOrDigit && c != '_' && c != '$') + + private def isScalaPackage(prefix: Prefix): Boolean = + prefix match + case p: PackageRef => p.fullyQualifiedName.toString == "scala" + case _ => false + + /** A function- or tuple-typed left operand of `=>` must be parenthesised: `(A => B) => C`, `((A, B)) => C`. */ + private def functionArgNeedsParens(tow: TypeOrWildcard): Boolean = + tow match + case t: AppliedType => asFunction(t).isDefined || asTuple(t).isDefined + case _ => false diff --git a/lib/test/src/cellar/TypePrinterTest.scala b/lib/test/src/cellar/TypePrinterTest.scala index 6182c05..871f5a1 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,279 @@ 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") + } + } + + test("printSymbolSignature renders higher-kinded type params (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val wrap = cls.declarations.find(_.name.toString == "wrap").get + val sig = TypePrinter.printSymbolSignature(wrap) + assertEquals(sig, "def wrap[F[_], A](fa: F[A]): F[A]") + } + } + + test("printSymbolSignature renders higher-kinded class type params (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val box = ctx.findStaticClass("cellar.fixture.scala3.CellarBox") + val boundedBox = ctx.findStaticClass("cellar.fixture.scala3.CellarBoundedBox") + assertEquals(TypePrinter.printSymbolSignature(box), "trait CellarBox[F[_]]") + assertEquals(TypePrinter.printSymbolSignature(boundedBox), "trait CellarBoundedBox[F[_ <: AnyRef]]") + } + } + + test("printSymbolSignature renders a standalone type lambda as a type argument (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val compose = cls.declarations.find(_.name.toString == "compose").get + val sig = TypePrinter.printSymbolSignature(compose) + assertEquals( + sig, + "def compose[F[_], G[_]](bf: CellarBox[F], bg: CellarBox[G]): CellarBox[[A] =>> F[G[A]]]" + ) + } + } + + test("printSymbolSignature renders a bounded standalone type lambda as a type argument (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val composeBounded = cls.declarations.find(_.name.toString == "composeBounded").get + val sig = TypePrinter.printSymbolSignature(composeBounded) + assertEquals( + sig, + "def composeBounded[F[_]](bf: CellarBox[F]): CellarBoundedBox[[A <: AnyRef] =>> F[A]]" + ) + } + } + + test("printSymbolSignature renders a bounded higher-kinded type param (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val bounded = cls.declarations.find(_.name.toString == "bounded").get + val sig = TypePrinter.printSymbolSignature(bounded) + assertEquals(sig, "def bounded[F[_ <: AnyRef]](fa: F[String]): F[String]") + } + } + + test("printSymbolSignature renders a constructor-bounded higher-kinded type param (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val upper = cls.declarations.find(_.name.toString == "upper").get + val sig = TypePrinter.printSymbolSignature(upper) + assertEquals(sig, "def upper[F[A] <: Iterable[A]](fa: F[Int]): F[Int]") + } + } + + test("printSymbolSignature renders a self-referential higher-kinded param bound (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val selfBounded = cls.declarations.find(_.name.toString == "selfBounded").get + val sig = TypePrinter.printSymbolSignature(selfBounded) + assertEquals(sig, "def selfBounded[F[A <: Comparable[A]]](fa: F[String]): F[String]") + } + } + + test("printSymbolSignature renders a multi-arity higher-kinded type param (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarHigherKinded") + val bimap = cls.declarations.find(_.name.toString == "bimap").get + val sig = TypePrinter.printSymbolSignature(bimap) + assertEquals(sig, "def bimap[G[_, _], A, B](g: G[A, B]): G[A, B]") + } + } + + test("printSymbolSignature renders higher-kinded type params (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala2.CellarHigherKinded") + val wrap = cls.declarations.find(_.name.toString == "wrap").get + val sig = TypePrinter.printSymbolSignature(wrap) + assertEquals(sig, "def wrap[F[_], A](fa: F[A]): F[A]") + } + } + + test("printSymbolSignature renders a bounded higher-kinded type param (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala2.CellarHigherKinded") + val bounded = cls.declarations.find(_.name.toString == "bounded").get + val sig = TypePrinter.printSymbolSignature(bounded) + assertEquals(sig, "def bounded[F[_ <: AnyRef]](fa: F[String]): F[String]") + } + } + + test("printSymbolSignature renders a multi-arity higher-kinded type param (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val cls = ctx.findStaticClass("cellar.fixture.scala2.CellarHigherKinded") + val bimap = cls.declarations.find(_.name.toString == "bimap").get + val sig = TypePrinter.printSymbolSignature(bimap) + assertEquals(sig, "def bimap[G[_, _], A, B](g: G[A, B]): G[A, 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 renders tuple types as paren sugar (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val fqn = "cellar.fixture.scala3.CellarSugar" + assertEquals(sugarSig(fqn, "pair"), "def pair[A, B](t: (A, B)): (A, B)") + assertEquals(sugarSig(fqn, "triple"), "def triple[A, B, C](t: (A, B, C)): (A, B, C)") + assertEquals(sugarSig(fqn, "tupleArg"), "def tupleArg[A, B](f: ((A, B)) => Boolean): Boolean") + } + } + + 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 symbolic binary types as infix (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val sig = sugarSig("cellar.fixture.scala3.CellarSugar", "mapK") + assertEquals(sig, "def mapK[F[_], G[_]](f: F ~> G): Unit") + } + } + + test("printSymbolSignature collapses trivial wildcard bounds (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val sig = sugarSig("cellar.fixture.scala3.CellarSugar", "wildcard") + assertEquals(sig, "def wildcard: List[?]") + } + } + + test("printSymbolSignature keeps a real wildcard upper bound (Scala 3)"): + withCtx { ctx => + IO.blocking { + given Context = ctx + val sig = sugarSig("cellar.fixture.scala3.CellarSugar", "boundedWildcard") + assertEquals(sig, "def boundedWildcard: List[? <: AnyRef]") + } + } + + 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 + val fqn = "cellar.fixture.scala2.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") + } + } + + test("printSymbolSignature renders tuple types as paren sugar (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val fqn = "cellar.fixture.scala2.CellarSugar" + assertEquals(sugarSig(fqn, "pair"), "def pair[A, B](t: (A, B)): (A, B)") + assertEquals(sugarSig(fqn, "triple"), "def triple[A, B, C](t: (A, B, C)): (A, B, C)") + assertEquals(sugarSig(fqn, "tupleArg"), "def tupleArg[A, B](f: ((A, B)) => Boolean): Boolean") + } + } + + test("printSymbolSignature collapses trivial wildcard bounds (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val sig = sugarSig("cellar.fixture.scala2.CellarSugar", "wildcard") + assertEquals(sig, "def wildcard: List[?]") + } + } + + test("printSymbolSignature keeps a real wildcard upper bound (Scala 2)"): + withScala2Ctx { ctx => + IO.blocking { + given Context = ctx + val sig = sugarSig("cellar.fixture.scala2.CellarSugar", "boundedWildcard") + assertEquals(sig, "def boundedWildcard: List[? <: AnyRef]") + } + } + + 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" + ) + } + }