diff --git a/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala b/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala index 069b35f..102e36f 100644 --- a/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala +++ b/fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala @@ -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 } diff --git a/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala b/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala index 30b8b5c..225f7b4 100644 --- a/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala +++ b/fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala @@ -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 diff --git a/lib/src/cellar/PublicApiFilter.scala b/lib/src/cellar/PublicApiFilter.scala index 8f9fee1..6c6833c 100644 --- a/lib/src/cellar/PublicApiFilter.scala +++ b/lib/src/cellar/PublicApiFilter.scala @@ -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 `` is pure noise. Class constructors are kept: for a Java type * they are the only listing that shows how the type is built. diff --git a/lib/src/cellar/TypePrinter.scala b/lib/src/cellar/TypePrinter.scala index 89cff55..ec5abe7 100644 --- a/lib/src/cellar/TypePrinter.scala +++ b/lib/src/cellar/TypePrinter.scala @@ -4,6 +4,7 @@ import tastyquery.Contexts.Context import tastyquery.Symbols.{ ClassSymbol, ClassTypeParamSymbol, + ParamSymbolsClause, Symbol, TermOrTypeSymbol, TermSymbol, @@ -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) @@ -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 @@ -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 "" diff --git a/lib/test/src/cellar/GetFormatterTest.scala b/lib/test/src/cellar/GetFormatterTest.scala index 7904256..0f9f875 100644 --- a/lib/test/src/cellar/GetFormatterTest.scala +++ b/lib/test/src/cellar/GetFormatterTest.scala @@ -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 { diff --git a/lib/test/src/cellar/TypePrinterTest.scala b/lib/test/src/cellar/TypePrinterTest.scala index ec7ee5b..e52a252 100644 --- a/lib/test/src/cellar/TypePrinterTest.scala +++ b/lib/test/src/cellar/TypePrinterTest.scala @@ -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 {