From 6f8e56db32a2713f60169d432c363ccf891e3123 Mon Sep 17 00:00:00 2001 From: Krzysztof Romanowski Date: Tue, 27 Jul 2021 23:49:36 +0200 Subject: [PATCH 1/3] WIP: add heuristic to support Context Bounds --- .../src/tests/contextBounds.scala | 23 +++++++ .../scaladoc/tasty/ClassLikeSupport.scala | 68 +++++++++++++++++-- .../scaladoc/signatures/SignatureTest.scala | 3 + .../TranslatableSignaturesTestCases.scala | 2 + 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 scaladoc-testcases/src/tests/contextBounds.scala diff --git a/scaladoc-testcases/src/tests/contextBounds.scala b/scaladoc-testcases/src/tests/contextBounds.scala new file mode 100644 index 000000000000..31c566ebd47f --- /dev/null +++ b/scaladoc-testcases/src/tests/contextBounds.scala @@ -0,0 +1,23 @@ +package tests +package contextBounds + +import scala.reflect.ClassTag + +class A: + def basic[A : ClassTag]: A + = ??? + + trait Build[X, Y] + trait From[A, B] + def b[T : ([T] =>> Build[From[T, T], T])](t: T): T + = t + + trait Build2[X[_], Y] + trait From2[A, B] + + def b2[T : ([T] =>> Build2[[Y] =>> From2[T, Y], T])](t: T): T + = t + + // Tests not support multiline signatures + def a[T <: String | Int : ([T] =>> T match { case String => A case Int => B })](t: T): T + = t \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index bcc0d1a47039..e7eb3277bb86 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -476,9 +476,14 @@ trait ClassLikeSupport: val memberInfo = unwrapMemberInfo(c, methodSymbol) val basicKind: Kind.Def = Kind.Def( - genericTypes.map(mkTypeArgument(_, memberInfo.genericTypes)), - paramLists.zipWithIndex.map { (pList, index) => - ParametersList(pList.params.map(mkParameter(_, paramPrefix, memberInfo = memberInfo.paramLists(index))), paramListModifier(pList.params)) + genericTypes.map(mkTypeArgument(_, memberInfo.genericTypes, memberInfo.contextBounds)), + paramLists.zipWithIndex.flatMap { (pList, index) => + memberInfo.paramLists(index) match + case EvidenceOnlyParameterList => Nil + case info: RegularParameterList => + Seq(ParametersList(pList.params.map( + mkParameter(_, paramPrefix, memberInfo = info)), paramListModifier(pList.params) + )) } ) @@ -523,7 +528,11 @@ trait ClassLikeSupport: isGrouped ) - def mkTypeArgument(argument: TypeDef, memberInfo: Map[String, TypeBounds] = Map.empty): TypeParameter = + def mkTypeArgument( + argument: TypeDef, + memberInfo: Map[String, TypeBounds] = Map.empty, + contextBounds: Map[String, DSignature] = Map.empty + ): TypeParameter = val variancePrefix: "+" | "-" | "" = if argument.symbol.flags.is(Flags.Covariant) then "+" else if argument.symbol.flags.is(Flags.Contravariant) then "-" @@ -531,12 +540,18 @@ trait ClassLikeSupport: val name = argument.symbol.normalizedName val normalizedName = if name.matches("_\\$\\d*") then "_" else name + val boundsSignature = memberInfo.get(name).fold(argument.rhs.asSignature)(_.asSignature) + val signature = contextBounds.get(name) match + case None => boundsSignature + case Some(contextBoundsSignature) => + boundsSignature ++ DSignature(" : ") ++ contextBoundsSignature + TypeParameter( argument.symbol.getAnnotations(), variancePrefix, normalizedName, argument.symbol.dri, - memberInfo.get(name).fold(argument.rhs.asSignature)(_.asSignature) + signature ) def parseTypeDef(typeDef: TypeDef): Member = @@ -586,7 +601,18 @@ trait ClassLikeSupport: deprecated = deprecated ) - case class MemberInfo(genericTypes: Map[String, TypeBounds], paramLists: List[Map[String, TypeRepr]], res: TypeRepr) + object EvidenceOnlyParameterList + type RegularParameterList = Map[String, TypeRepr] + type ParameterList = RegularParameterList | EvidenceOnlyParameterList.type + + case class MemberInfo( + genericTypes: Map[String, TypeBounds], + paramLists: List[ParameterList], + res: TypeRepr, + contextBounds: Map[String, DSignature] = Map.empty, + ) + + def isSyntheticEvidence(name: String) = name.startsWith("evidence$") def unwrapMemberInfo(c: ClassDef, symbol: Symbol): MemberInfo = val baseTypeRepr = memberInfo(c, symbol) @@ -595,7 +621,35 @@ trait ClassLikeSupport: MemberInfo(polyType.paramNames.zip(polyType.paramBounds).toMap, List.empty, polyType.resType) def handleMethodType(memberInfo: MemberInfo, methodType: MethodType): MemberInfo = - MemberInfo(memberInfo.genericTypes, memberInfo.paramLists ++ List(methodType.paramNames.zip(methodType.paramTypes).toMap), methodType.resType) + val rawParams = methodType.paramNames.zip(methodType.paramTypes).toMap + val (evidences, newParams) = rawParams.partition(e => isSyntheticEvidence(e._1)) + val newLists: List[ParameterList] = if newParams.isEmpty && evidences.nonEmpty + then memberInfo.paramLists ++ Seq(EvidenceOnlyParameterList) + else memberInfo.paramLists ++ Seq(newParams) + + def findParamRefs(t: TypeRepr): Seq[ParamRef] = t match + case paramRef: ParamRef => Seq(paramRef) + case AppliedType(_, args) => args.flatMap(findParamRefs) + case MatchType(bound, scrutinee, cases) => + findParamRefs(bound) ++ findParamRefs(scrutinee) + case _ => Nil + + def nameForRef(ref: ParamRef): String = + val PolyType(names, _, _) = ref.binder + names(ref.paramNum) + + val contextBounds = + evidences.collect { + case (_, AppliedType(tpe, List(typeParam: ParamRef))) => + nameForRef(typeParam) -> tpe.asSignature + case (_, original) => + val typeParam = findParamRefs(original).head // TODO throw nicer error! + val name = nameForRef(typeParam) + val signature = Seq(s"([$name] =>> ") ++ original.asSignature ++ Seq(")") + name -> signature + } + + MemberInfo(memberInfo.genericTypes, newLists , methodType.resType, contextBounds.toMap) def handleByNameType(memberInfo: MemberInfo, byNameType: ByNameType): MemberInfo = MemberInfo(memberInfo.genericTypes, memberInfo.paramLists, byNameType.underlying) diff --git a/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala b/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala index 8c381a5710d5..0d5fec9596dd 100644 --- a/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala +++ b/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala @@ -50,6 +50,9 @@ abstract class SignatureTest( (s"Not documented signatures:\n${expectedButNotFound.mkString("\n")}") val unexpectedReport = Option.when(!unexpected.isEmpty) (s"Unexpectedly documented signatures:\n${unexpected.mkString("\n")}") + + println("Expecting following signatures: " + expectedFromSources) + val reports = missingReport ++ unexpectedReport if !reports.isEmpty then diff --git a/scaladoc/test/dotty/tools/scaladoc/signatures/TranslatableSignaturesTestCases.scala b/scaladoc/test/dotty/tools/scaladoc/signatures/TranslatableSignaturesTestCases.scala index b07e9d010e55..7743dd256b0d 100644 --- a/scaladoc/test/dotty/tools/scaladoc/signatures/TranslatableSignaturesTestCases.scala +++ b/scaladoc/test/dotty/tools/scaladoc/signatures/TranslatableSignaturesTestCases.scala @@ -84,3 +84,5 @@ class ImplicitConversionsTest3 extends SignatureTest( ) class SpecializedSignature extends SignatureTest("specializedSignature", SignatureTest.all) + +class ContextBounds extends SignatureTest("contextBounds", SignatureTest.all) From c3029e678cb7b5ba186a8128352ee26f8cdccf9e Mon Sep 17 00:00:00 2001 From: Krzysztof Romanowski Date: Wed, 28 Jul 2021 15:21:04 +0200 Subject: [PATCH 2/3] More proper way to detect if context bound is used --- .../src/tests/contextBounds.scala | 18 +++++++++- .../scaladoc/tasty/ClassLikeSupport.scala | 36 ++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/scaladoc-testcases/src/tests/contextBounds.scala b/scaladoc-testcases/src/tests/contextBounds.scala index 31c566ebd47f..1925f7f40994 100644 --- a/scaladoc-testcases/src/tests/contextBounds.scala +++ b/scaladoc-testcases/src/tests/contextBounds.scala @@ -7,6 +7,9 @@ class A: def basic[A : ClassTag]: A = ??? + def basic2[A : ClassTag, B : List]: A + = ??? + trait Build[X, Y] trait From[A, B] def b[T : ([T] =>> Build[From[T, T], T])](t: T): T @@ -20,4 +23,17 @@ class A: // Tests not support multiline signatures def a[T <: String | Int : ([T] =>> T match { case String => A case Int => B })](t: T): T - = t \ No newline at end of file + = t + + def falsePositive[T](evidence$1: ClassTag[T]): Int + = 1 + + // Scala spec stats that behaviour of names with `$` is undefined. + // Scaladoc documents definition below as `def falsePositive2[T: ClassTag]: Int` + // that is equivalent of methods below + // def falsePositive2[T](implicit evidence$3: ClassTag[T]): Int + // = 1 + + class Outer[A]: + def falsePositiveInner[T](implicit evidence$3: ClassTag[A]): Int + = 1 \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index e7eb3277bb86..fffd6325721a 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -10,6 +10,7 @@ import scala.quoted._ import SymOps._ import NameNormalizer._ import SyntheticsSupport._ +import dotty.tools.dotc.core.NameKinds trait ClassLikeSupport: self: TastyParser => @@ -612,20 +613,21 @@ trait ClassLikeSupport: contextBounds: Map[String, DSignature] = Map.empty, ) - def isSyntheticEvidence(name: String) = name.startsWith("evidence$") def unwrapMemberInfo(c: ClassDef, symbol: Symbol): MemberInfo = val baseTypeRepr = memberInfo(c, symbol) + def isSyntheticEvidence(name: String) = + if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else + symbol.paramSymss.flatten.find(_.name == name).exists(_.flags.is(Flags.Implicit)) + def handlePolyType(polyType: PolyType): MemberInfo = MemberInfo(polyType.paramNames.zip(polyType.paramBounds).toMap, List.empty, polyType.resType) def handleMethodType(memberInfo: MemberInfo, methodType: MethodType): MemberInfo = val rawParams = methodType.paramNames.zip(methodType.paramTypes).toMap - val (evidences, newParams) = rawParams.partition(e => isSyntheticEvidence(e._1)) - val newLists: List[ParameterList] = if newParams.isEmpty && evidences.nonEmpty - then memberInfo.paramLists ++ Seq(EvidenceOnlyParameterList) - else memberInfo.paramLists ++ Seq(newParams) + val (evidences, notEvidences) = rawParams.partition(e => isSyntheticEvidence(e._1)) + def findParamRefs(t: TypeRepr): Seq[ParamRef] = t match case paramRef: ParamRef => Seq(paramRef) @@ -638,17 +640,25 @@ trait ClassLikeSupport: val PolyType(names, _, _) = ref.binder names(ref.paramNum) - val contextBounds = - evidences.collect { + val (paramsThatLookLikeContextBounds, contextBounds) = + evidences.partitionMap { case (_, AppliedType(tpe, List(typeParam: ParamRef))) => - nameForRef(typeParam) -> tpe.asSignature - case (_, original) => - val typeParam = findParamRefs(original).head // TODO throw nicer error! - val name = nameForRef(typeParam) - val signature = Seq(s"([$name] =>> ") ++ original.asSignature ++ Seq(")") - name -> signature + Right(nameForRef(typeParam) -> tpe.asSignature) + case (name, original) => + findParamRefs(original) match + case Nil => Left((name, original)) + case typeParam :: _ => + val name = nameForRef(typeParam) + val signature = Seq(s"([$name] =>> ") ++ original.asSignature ++ Seq(")") + Right(name -> signature) } + val newParams = notEvidences ++ paramsThatLookLikeContextBounds + + val newLists: List[ParameterList] = if newParams.isEmpty && contextBounds.nonEmpty + then memberInfo.paramLists ++ Seq(EvidenceOnlyParameterList) + else memberInfo.paramLists ++ Seq(newParams) + MemberInfo(memberInfo.genericTypes, newLists , methodType.resType, contextBounds.toMap) def handleByNameType(memberInfo: MemberInfo, byNameType: ByNameType): MemberInfo = From 45ed5e2f5ac9a62ecc214d6da17f4b7f6e4bdf74 Mon Sep 17 00:00:00 2001 From: Krzysztof Romanowski Date: Wed, 28 Jul 2021 15:48:07 +0200 Subject: [PATCH 3/3] Add explanation how `isSyntheticEvidence` works --- .../src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala | 7 +++++++ .../dotty/tools/scaladoc/signatures/SignatureTest.scala | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index fffd6325721a..c9721f54ff18 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -619,6 +619,13 @@ trait ClassLikeSupport: def isSyntheticEvidence(name: String) = if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else + // This assumes that every parameter that starts with `evidence$` and is implicit is generated by compiler to desugar context bound. + // Howrever, this is just a heuristic, so + // `def foo[A](evidence$1: ClassTag[A]) = 1` + // will be documented as + // `def foo[A: ClassTag] = 1`. + // Scala spec states that `$` should not be used in names and behaviour may be undefiend in such case. + // Documenting method slightly different then its definition is withing the 'undefiend behaviour'. symbol.paramSymss.flatten.find(_.name == name).exists(_.flags.is(Flags.Implicit)) def handlePolyType(polyType: PolyType): MemberInfo = diff --git a/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala b/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala index 0d5fec9596dd..c232f03a63a1 100644 --- a/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala +++ b/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala @@ -51,8 +51,6 @@ abstract class SignatureTest( val unexpectedReport = Option.when(!unexpected.isEmpty) (s"Unexpectedly documented signatures:\n${unexpected.mkString("\n")}") - println("Expecting following signatures: " + expectedFromSources) - val reports = missingReport ++ unexpectedReport if !reports.isEmpty then