Skip to content
Open
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
2 changes: 2 additions & 0 deletions fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ trait CellarSugar {
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 withDefault(a: Int, b: String = "b"): String
def curriedDefault(a: Int)(b: Int = a): Int
}
2 changes: 2 additions & 0 deletions fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ trait CellarSugar:
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
def withDefault(a: Int, b: String = "b"): String
def curriedDefault(a: Int)(b: Int = a): Int
8 changes: 7 additions & 1 deletion lib/src/cellar/PublicApiFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ object PublicApiFilter:
private def isSyntheticSym(sym: Symbol): Boolean =
sym match
case s: (ClassSymbol | TermSymbol | TypeSymbol) =>
s.isSynthetic || s.name.toString.startsWith("$") || isUncallableConstructor(s)
s.isSynthetic || s.name.toString.startsWith("$") || isDefaultGetter(s) || isUncallableConstructor(s)
case _ => false

/** Scala 2 pickles flag default getters synthetic; TASTy does not, so they surface by name.
* The default is already shown as `= ...` on the parameter itself.
*/
private def isDefaultGetter(sym: Symbol): Boolean =
sym.name.toString.contains("$default$")

/** Only a concrete class has a user-callable constructor; for an object or a
* trait `<init>` is pure noise. Class constructors are kept: for a Java type
* they are the only listing that shows how the type is built.
Expand Down
28 changes: 18 additions & 10 deletions lib/src/cellar/TypePrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import tastyquery.Contexts.Context
import tastyquery.Symbols.{
ClassSymbol,
ClassTypeParamSymbol,
ParamSymbolsClause,
Symbol,
TermOrTypeSymbol,
TermSymbol,
Expand Down Expand Up @@ -86,25 +87,32 @@ object TypePrinter:
case _: NothingType => "Nothing"
case _ => tpe.getClass.getSimpleName

def printMethodic(tpe: TypeOrMethodic)(using ctx: Context): String =
/** `paramSymss` runs alongside the type: the type alone has no notion of a default argument,
* only the parameter symbol does, and a method's clauses line up one-to-one with them.
*/
def printMethodic(tpe: TypeOrMethodic, paramSymss: List[ParamSymbolsClause] = Nil)(using ctx: Context): String =
tpe match
case t: MethodType =>
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 defaults = paramSymss.headOption match
case Some(Left(syms)) => syms.map(_.isParamWithDefault)
case _ => Nil
val params = t.paramNames.zip(t.paramTypes).zipWithIndex.map { case ((n, tp), i) =>
val default = if defaults.lift(i).contains(true) then " = ..." else ""
s"$n: ${printType(tp)}$default"
}
val paramStr = s"($prefix${params.mkString(", ")})"
val rest = t.resultType match
case _: MethodType | _: PolyType => printMethodic(t.resultType)
case r => s": ${printMethodic(r)}"
case _: MethodType | _: PolyType => printMethodic(t.resultType, paramSymss.drop(1))
case r => s": ${printMethodic(r, Nil)}"
s"$paramStr$rest"

case t: PolyType =>
val typeParams = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam)
s"[${typeParams.mkString(", ")}]${printMethodic(t.resultType)}"
s"[${typeParams.mkString(", ")}]${printMethodic(t.resultType, paramSymss.drop(1))}"

case t: Type => printType(t)

Expand Down Expand Up @@ -133,7 +141,7 @@ object TypePrinter:
case term: TermSymbol =>
val keyword = termKeyword(term)
if term.isModuleVal then s"$keyword ${term.name}"
else s"$keyword ${term.name}${printTopLevelMethodic(term.declaredType)}"
else s"$keyword ${term.name}${printTopLevelMethodic(term.declaredType, term.paramSymss)}"

case tm: TypeMemberSymbol =>
tm.typeDef match
Expand Down Expand Up @@ -166,13 +174,13 @@ object TypePrinter:
else if sym.isModuleVal then "object"
else "val"

private def printTopLevelMethodic(tpe: TypeOrMethodic)(using ctx: Context): String =
private def printTopLevelMethodic(tpe: TypeOrMethodic, paramSymss: List[ParamSymbolsClause])(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)
s"[${typeParams.mkString(", ")}]${printTopLevelMethodic(t.resultType, paramSymss.drop(1))}"
case t: MethodType => printMethodic(t, paramSymss)

private def printClassTypeParams(params: List[ClassTypeParamSymbol])(using ctx: Context): String =
if params.isEmpty then ""
Expand Down
28 changes: 28 additions & 0 deletions lib/test/src/cellar/GetFormatterTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,34 @@ class GetFormatterTest extends CatsEffectSuite:
}
}

test("formatSymbol lists a method with defaults once, without its default getters (Scala 3)"):
withCtx { ctx =>
IO.blocking {
given Context = ctx
val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarSugar")
val output = GetFormatter.formatSymbol(cls)
val lines = output.linesIterator.filter(_.contains("withDefault")).toList
assertEquals(lines, List("def withDefault(a: Int, b: String = ...): String"), s"Output:\n$output")
assert(!output.contains("$default$"), s"Output:\n$output")
}
}

test("formatSymbol lists a method with defaults once, without its default getters (Scala 2)"):
withScala2Ctx { ctx =>
IO.blocking {
given Context = ctx
val cls = ctx.findStaticClass("cellar.fixture.scala2.CellarSugar")
val output = GetFormatter.formatSymbol(cls)
val lines = output.linesIterator.filter(_.contains("withDefault")).toList
assertEquals(
lines,
List("def withDefault(a: Int, b: String = ...): String // [Scala 2 — limited type information]"),
s"Output:\n$output"
)
assert(!output.contains("$default$"), s"Output:\n$output")
}
}

test("formatSymbol omits the universal parent from a Scala 2 signature"):
withScala2Ctx { ctx =>
IO.blocking {
Expand Down
20 changes: 20 additions & 0 deletions lib/test/src/cellar/TypePrinterTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,26 @@ class TypePrinterTest extends CatsEffectSuite:
}
}

test("printSymbolSignature marks parameters that have a default (Scala 3)"):
withCtx { ctx =>
IO.blocking {
given Context = ctx
val fqn = "cellar.fixture.scala3.CellarSugar"
assertEquals(sugarSig(fqn, "withDefault"), "def withDefault(a: Int, b: String = ...): String")
assertEquals(sugarSig(fqn, "curriedDefault"), "def curriedDefault(a: Int)(b: Int = ...): Int")
}
}

test("printSymbolSignature marks parameters that have a default (Scala 2)"):
withScala2Ctx { ctx =>
IO.blocking {
given Context = ctx
val fqn = "cellar.fixture.scala2.CellarSugar"
assertEquals(sugarSig(fqn, "withDefault"), "def withDefault(a: Int, b: String = ...): String")
assertEquals(sugarSig(fqn, "curriedDefault"), "def curriedDefault(a: Int)(b: Int = ...): Int")
}
}

test("printSymbolSignature renders function types as arrow sugar (Scala 2)"):
withScala2Ctx { ctx =>
IO.blocking {
Expand Down