Skip to content

Commit e604e36

Browse files
authored
Merge pull request #13172 from romanowski/scaladoc/context-bounds
Add heuristic to support Context Bounds
2 parents 5c6d352 + 45ed5e2 commit e604e36

File tree

4 files changed

+120
-7
lines changed

4 files changed

+120
-7
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package tests
2+
package contextBounds
3+
4+
import scala.reflect.ClassTag
5+
6+
class A:
7+
def basic[A : ClassTag]: A
8+
= ???
9+
10+
def basic2[A : ClassTag, B : List]: A
11+
= ???
12+
13+
trait Build[X, Y]
14+
trait From[A, B]
15+
def b[T : ([T] =>> Build[From[T, T], T])](t: T): T
16+
= t
17+
18+
trait Build2[X[_], Y]
19+
trait From2[A, B]
20+
21+
def b2[T : ([T] =>> Build2[[Y] =>> From2[T, Y], T])](t: T): T
22+
= t
23+
24+
// Tests not support multiline signatures
25+
def a[T <: String | Int : ([T] =>> T match { case String => A case Int => B })](t: T): T
26+
= t
27+
28+
def falsePositive[T](evidence$1: ClassTag[T]): Int
29+
= 1
30+
31+
// Scala spec stats that behaviour of names with `$` is undefined.
32+
// Scaladoc documents definition below as `def falsePositive2[T: ClassTag]: Int`
33+
// that is equivalent of methods below
34+
// def falsePositive2[T](implicit evidence$3: ClassTag[T]): Int
35+
// = 1
36+
37+
class Outer[A]:
38+
def falsePositiveInner[T](implicit evidence$3: ClassTag[A]): Int
39+
= 1

scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import scala.quoted._
1010
import SymOps._
1111
import NameNormalizer._
1212
import SyntheticsSupport._
13+
import dotty.tools.dotc.core.NameKinds
1314

1415
trait ClassLikeSupport:
1516
self: TastyParser =>
@@ -476,9 +477,14 @@ trait ClassLikeSupport:
476477
val memberInfo = unwrapMemberInfo(c, methodSymbol)
477478

478479
val basicKind: Kind.Def = Kind.Def(
479-
genericTypes.map(mkTypeArgument(_, memberInfo.genericTypes)),
480-
paramLists.zipWithIndex.map { (pList, index) =>
481-
ParametersList(pList.params.map(mkParameter(_, paramPrefix, memberInfo = memberInfo.paramLists(index))), paramListModifier(pList.params))
480+
genericTypes.map(mkTypeArgument(_, memberInfo.genericTypes, memberInfo.contextBounds)),
481+
paramLists.zipWithIndex.flatMap { (pList, index) =>
482+
memberInfo.paramLists(index) match
483+
case EvidenceOnlyParameterList => Nil
484+
case info: RegularParameterList =>
485+
Seq(ParametersList(pList.params.map(
486+
mkParameter(_, paramPrefix, memberInfo = info)), paramListModifier(pList.params)
487+
))
482488
}
483489
)
484490

@@ -523,20 +529,30 @@ trait ClassLikeSupport:
523529
isGrouped
524530
)
525531

526-
def mkTypeArgument(argument: TypeDef, memberInfo: Map[String, TypeBounds] = Map.empty): TypeParameter =
532+
def mkTypeArgument(
533+
argument: TypeDef,
534+
memberInfo: Map[String, TypeBounds] = Map.empty,
535+
contextBounds: Map[String, DSignature] = Map.empty
536+
): TypeParameter =
527537
val variancePrefix: "+" | "-" | "" =
528538
if argument.symbol.flags.is(Flags.Covariant) then "+"
529539
else if argument.symbol.flags.is(Flags.Contravariant) then "-"
530540
else ""
531541

532542
val name = argument.symbol.normalizedName
533543
val normalizedName = if name.matches("_\\$\\d*") then "_" else name
544+
val boundsSignature = memberInfo.get(name).fold(argument.rhs.asSignature)(_.asSignature)
545+
val signature = contextBounds.get(name) match
546+
case None => boundsSignature
547+
case Some(contextBoundsSignature) =>
548+
boundsSignature ++ DSignature(" : ") ++ contextBoundsSignature
549+
534550
TypeParameter(
535551
argument.symbol.getAnnotations(),
536552
variancePrefix,
537553
normalizedName,
538554
argument.symbol.dri,
539-
memberInfo.get(name).fold(argument.rhs.asSignature)(_.asSignature)
555+
signature
540556
)
541557

542558
def parseTypeDef(typeDef: TypeDef): Member =
@@ -586,16 +602,71 @@ trait ClassLikeSupport:
586602
deprecated = deprecated
587603
)
588604

589-
case class MemberInfo(genericTypes: Map[String, TypeBounds], paramLists: List[Map[String, TypeRepr]], res: TypeRepr)
605+
object EvidenceOnlyParameterList
606+
type RegularParameterList = Map[String, TypeRepr]
607+
type ParameterList = RegularParameterList | EvidenceOnlyParameterList.type
608+
609+
case class MemberInfo(
610+
genericTypes: Map[String, TypeBounds],
611+
paramLists: List[ParameterList],
612+
res: TypeRepr,
613+
contextBounds: Map[String, DSignature] = Map.empty,
614+
)
615+
590616

591617
def unwrapMemberInfo(c: ClassDef, symbol: Symbol): MemberInfo =
592618
val baseTypeRepr = memberInfo(c, symbol)
593619

620+
def isSyntheticEvidence(name: String) =
621+
if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else
622+
// This assumes that every parameter that starts with `evidence$` and is implicit is generated by compiler to desugar context bound.
623+
// Howrever, this is just a heuristic, so
624+
// `def foo[A](evidence$1: ClassTag[A]) = 1`
625+
// will be documented as
626+
// `def foo[A: ClassTag] = 1`.
627+
// Scala spec states that `$` should not be used in names and behaviour may be undefiend in such case.
628+
// Documenting method slightly different then its definition is withing the 'undefiend behaviour'.
629+
symbol.paramSymss.flatten.find(_.name == name).exists(_.flags.is(Flags.Implicit))
630+
594631
def handlePolyType(polyType: PolyType): MemberInfo =
595632
MemberInfo(polyType.paramNames.zip(polyType.paramBounds).toMap, List.empty, polyType.resType)
596633

597634
def handleMethodType(memberInfo: MemberInfo, methodType: MethodType): MemberInfo =
598-
MemberInfo(memberInfo.genericTypes, memberInfo.paramLists ++ List(methodType.paramNames.zip(methodType.paramTypes).toMap), methodType.resType)
635+
val rawParams = methodType.paramNames.zip(methodType.paramTypes).toMap
636+
val (evidences, notEvidences) = rawParams.partition(e => isSyntheticEvidence(e._1))
637+
638+
639+
def findParamRefs(t: TypeRepr): Seq[ParamRef] = t match
640+
case paramRef: ParamRef => Seq(paramRef)
641+
case AppliedType(_, args) => args.flatMap(findParamRefs)
642+
case MatchType(bound, scrutinee, cases) =>
643+
findParamRefs(bound) ++ findParamRefs(scrutinee)
644+
case _ => Nil
645+
646+
def nameForRef(ref: ParamRef): String =
647+
val PolyType(names, _, _) = ref.binder
648+
names(ref.paramNum)
649+
650+
val (paramsThatLookLikeContextBounds, contextBounds) =
651+
evidences.partitionMap {
652+
case (_, AppliedType(tpe, List(typeParam: ParamRef))) =>
653+
Right(nameForRef(typeParam) -> tpe.asSignature)
654+
case (name, original) =>
655+
findParamRefs(original) match
656+
case Nil => Left((name, original))
657+
case typeParam :: _ =>
658+
val name = nameForRef(typeParam)
659+
val signature = Seq(s"([$name] =>> ") ++ original.asSignature ++ Seq(")")
660+
Right(name -> signature)
661+
}
662+
663+
val newParams = notEvidences ++ paramsThatLookLikeContextBounds
664+
665+
val newLists: List[ParameterList] = if newParams.isEmpty && contextBounds.nonEmpty
666+
then memberInfo.paramLists ++ Seq(EvidenceOnlyParameterList)
667+
else memberInfo.paramLists ++ Seq(newParams)
668+
669+
MemberInfo(memberInfo.genericTypes, newLists , methodType.resType, contextBounds.toMap)
599670

600671
def handleByNameType(memberInfo: MemberInfo, byNameType: ByNameType): MemberInfo =
601672
MemberInfo(memberInfo.genericTypes, memberInfo.paramLists, byNameType.underlying)

scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ abstract class SignatureTest(
5050
(s"Not documented signatures:\n${expectedButNotFound.mkString("\n")}")
5151
val unexpectedReport = Option.when(!unexpected.isEmpty)
5252
(s"Unexpectedly documented signatures:\n${unexpected.mkString("\n")}")
53+
5354
val reports = missingReport ++ unexpectedReport
5455

5556
if !reports.isEmpty then

scaladoc/test/dotty/tools/scaladoc/signatures/TranslatableSignaturesTestCases.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,5 @@ class ImplicitConversionsTest3 extends SignatureTest(
8484
)
8585

8686
class SpecializedSignature extends SignatureTest("specializedSignature", SignatureTest.all)
87+
88+
class ContextBounds extends SignatureTest("contextBounds", SignatureTest.all)

0 commit comments

Comments
 (0)