Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
...
```

Expand Down
6 changes: 6 additions & 0 deletions fixtureScala2/src/cellar/fixture/scala2/CellarCurried.scala
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 5 additions & 0 deletions fixtureScala3/src/cellar/fixture/scala3/CellarCurried.scala
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala
Original file line number Diff line number Diff line change
@@ -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
62 changes: 55 additions & 7 deletions lib/src/cellar/TypePrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)}"
Expand All @@ -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
}
Expand All @@ -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"

Expand Down Expand Up @@ -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
90 changes: 90 additions & 0 deletions lib/test/src/cellar/TypePrinterTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
)
}
}