Skip to content

Commit 67ee8ec

Browse files
committed
scaladoc: fixes and improvements to context bounds and extension methods
1 parent c61897d commit 67ee8ec

File tree

6 files changed

+105
-163
lines changed

6 files changed

+105
-163
lines changed

scaladoc-testcases/src/tests/classSignatureTestSource.scala

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ abstract class Documentation[T, A <: Int, B >: String, -X, +Y](c1: String, val c
1717
def this(x: T)
1818
= this()
1919

20+
//expected: def toArray[B >: T : ClassTag]: Array[B]
21+
2022
class innerDocumentationClass
2123
{
2224

scaladoc-testcases/src/tests/contextBounds.scala

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ package contextBounds
44
import scala.reflect.ClassTag
55

66
class A:
7+
type :+:[X, Y] = [Z] =>> Map[Z, (X, Y)]
8+
9+
extension [T : ([X] =>> String) : ([X] =>> Int)](x: Int)
10+
def foo[U : ([X] =>> String)](y: Int): Nothing
11+
= ???
12+
def bar[W : T match { case String => List case Int => Option } : Set]: Nothing
13+
= ???
14+
def baz[V : Int :+: String : Option]: Nothing
15+
= ???
16+
717
def basic[A : ClassTag]: A
818
= ???
919

@@ -35,5 +45,5 @@ class A:
3545
// = 1
3646

3747
class Outer[A]:
38-
def falsePositiveInner[T](implicit evidence$3: ClassTag[A]): Int
39-
= 1
48+
def falsePositiveInner[T](implicit evidence$3: ClassTag[A]): Int //expected: def falsePositiveInner[T]: Int
49+
= 1

scaladoc-testcases/src/tests/exports1.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class A: //unexpected
1414
= 1
1515
var aVar1: 1
1616
= 1
17-
type HKT[T[_], X] //expected: final type HKT = [T[_], X] =>> a.HKT[T, X]
17+
type HKT[T[_], X] //expected: final type HKT = a.HKT
1818
= T[X]
1919
type SomeRandomType = (List[?] | Seq[?]) & String //expected: final type SomeRandomType = a.SomeRandomType
2020
def x[T[_], X](x: X): HKT[T, X] //expected: def x[T[_], X](x: X): A.this.HKT[T, X]

scaladoc-testcases/src/tests/extensionMethodSignatures.scala

+20-1
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,23 @@ case class ClassTwo(a: String, b: String)
4444

4545
}
4646

47-
class ClassOneTwo extends ClassOne
47+
class ClassOneTwo extends ClassOne
48+
49+
trait C[T]
50+
trait Equiv[T]:
51+
extension [U : C](x: U)
52+
def ><[V](y: V): Nothing
53+
= ???
54+
55+
trait Monoid[T]:
56+
extension (a: T)
57+
def \:[U](b: U): Nothing
58+
= ???
59+
extension [U](a: T)
60+
def \\:(b: U): Nothing
61+
= ???
62+
63+
class Clazz[U]:
64+
extension [T : ([X] =>> String) : ([X] =>> String)](x: Int)
65+
def bar[U : ([X] =>> String) : List](y: Int): Nothing
66+
= ???

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

+60-156
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package dotty.tools.scaladoc.tasty
22

3-
import scala.jdk.CollectionConverters._
43
import dotty.tools.scaladoc._
54
import dotty.tools.scaladoc.{Signature => DSignature}
6-
import dotty.tools.scaladoc.Inkuire
75

86
import scala.quoted._
97

@@ -148,18 +146,44 @@ trait ClassLikeSupport:
148146
private def isDocumentableExtension(s: Symbol) =
149147
!s.isHiddenByVisibility && !s.isSyntheticFunc && s.isExtensionMethod
150148

149+
private def isEvidence(tpc: TermParamClause) =
150+
(tpc.isGiven || tpc.isImplicit) && tpc.params.forall(_.name.startsWith(NameKinds.ContextBoundParamName.separator))
151+
152+
private def extractEvidences(tpcs: List[TermParamClause]): (Map[Symbol, List[TypeRepr]], List[TermParamClause]) =
153+
val (evidenceParams, termParams) = tpcs.partition(isEvidence)
154+
val evidenceMap = evidenceParams.flatMap(_.params).map(p => (p.tpt, p.tpt.tpe)).collect {
155+
case (Applied(bound, List(arg: TypeTree)), _) => (arg.tpe.typeSymbol, bound.tpe)
156+
case (_, AppliedType(bound, List(arg))) => (arg.typeSymbol, bound)
157+
// It seems like here we could do:
158+
// (...).map(_.tpt.tpe).collect {
159+
// case AppliedType(bound, List(arg)) => (arg.typeSymbol, bound)
160+
// or:
161+
// (...).map(_.tpt).collect {
162+
// case Applied(bound, List(arg: TypeTree)) => (arg.tpe.typeSymbol, bound.tpe)
163+
//
164+
// First one doesn't always work because .tpe in some cases causes type lambda reductions, eg:
165+
// def foo[T : ([X] =>> String)]
166+
// after desugaring:
167+
// def foo[T](implicit ecidence$1 : ([X] =>> String)[T])
168+
// tree for this evidence looks like: ([X] =>> String)[T]
169+
// but type repr looks like: String
170+
// (see scaladoc-testcases/src/tests/contextBounds.scala)
171+
//
172+
// Second one doesn't always work, because the tree is sometimes `Inferred`
173+
// (see toArray inherited in scaladoc-testcases/src/tests/classSignatureTestSource.scala)
174+
//
175+
// TODO: check if those two cases can occur at the same time
176+
}.groupMap(_._1)(_._2).withDefaultValue(Nil)
177+
(evidenceMap, termParams)
178+
151179
private def parseMember(c: ClassDef)(s: Tree): Option[Member] = processTreeOpt(s) { s match
152180
case dd: DefDef if isDocumentableExtension(dd.symbol) =>
153181
dd.symbol.extendedSymbol.map { extSym =>
154-
val memberInfo = unwrapMemberInfo(c, dd.symbol)
155-
val typeParams = dd.symbol.extendedTypeParams.map(mkTypeArgument(_, c, memberInfo.genericTypes))
156-
val termParams = dd.symbol.extendedTermParamLists.zipWithIndex.flatMap { case (termParamList, index) =>
157-
memberInfo.termParamLists(index) match
158-
case MemberInfo.EvidenceOnlyParameterList => None
159-
case MemberInfo.RegularParameterList(info) =>
160-
Some(api.TermParameterList(termParamList.params.map(mkParameter(_, c, memberInfo = info)), paramListModifier(termParamList.params)))
161-
case _ => assert(false, "memberInfo.termParamLists contains a type parameter list !")
162-
}
182+
val (evidenceMap, termParamClauses) = extractEvidences(dd.symbol.extendedTermParamLists)
183+
val termParams = termParamClauses.map: tpc =>
184+
api.TermParameterList(tpc.params.map(mkParameter(_, c)), paramListModifier(tpc.params))
185+
val typeParams = dd.symbol.extendedTypeParams.map(td => mkTypeArgument(td, c, evidenceMap(td.symbol)))
186+
163187
val target = ExtensionTarget(
164188
extSym.symbol.normalizedName,
165189
typeParams,
@@ -351,45 +375,20 @@ trait ClassLikeSupport:
351375
specificKind: (Kind.Def => Kind) = identity
352376
): Member =
353377
val method = methodSymbol.tree.asInstanceOf[DefDef]
354-
val paramLists = methodSymbol.nonExtensionParamLists
355-
356-
val memberInfo = unwrapMemberInfo(c, methodSymbol)
357-
358-
val unshuffledMemberInfoParamLists =
359-
if methodSymbol.isExtensionMethod && methodSymbol.isRightAssoc then
360-
// Taken from RefinedPrinter.scala
361-
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
362-
val (leftTyParams, rest1) = memberInfo.paramLists match
363-
case fst :: tail if fst.isType => (List(fst), tail)
364-
case other => (List(), other)
365-
val (leadingUsing, rest2) = rest1.span(_.isUsing)
366-
val (rightTyParams, rest3) = rest2.span(_.isType)
367-
val (rightParam, rest4) = rest3.splitAt(1)
368-
val (leftParam, rest5) = rest4.splitAt(1)
369-
val (trailingUsing, rest6) = rest5.span(_.isUsing)
370-
if leftParam.nonEmpty then
371-
// leftTyParams ::: leadingUsing ::: leftParam ::: trailingUsing ::: rightTyParams ::: rightParam ::: rest6
372-
// because of takeRight after, this is equivalent to the following:
373-
rightTyParams ::: rightParam ::: rest6
374-
else
375-
memberInfo.paramLists // it wasn't a binary operator, after all.
376-
else
377-
memberInfo.paramLists
378-
379-
val croppedUnshuffledMemberInfoParamLists = unshuffledMemberInfoParamLists.takeRight(paramLists.length)
380-
381-
val basicDefKind: Kind.Def = Kind.Def(
382-
paramLists.zip(croppedUnshuffledMemberInfoParamLists).flatMap{
383-
case (_: TermParamClause, MemberInfo.EvidenceOnlyParameterList) => Nil
384-
case (pList: TermParamClause, MemberInfo.RegularParameterList(info)) =>
385-
Some(Left(api.TermParameterList(pList.params.map(
386-
mkParameter(_, c, paramPrefix, memberInfo = info)), paramListModifier(pList.params)
387-
)))
388-
case (TypeParamClause(genericTypeList), MemberInfo.TypeParameterList(memInfoTypes)) =>
389-
Some(Right(genericTypeList.map(mkTypeArgument(_, c, memInfoTypes, memberInfo.contextBounds))))
390-
case (_,_) =>
391-
assert(false, s"croppedUnshuffledMemberInfoParamLists and SymOps.nonExtensionParamLists disagree on whether this clause is a type or term one")
392-
}
378+
val paramLists = methodSymbol.nonExtensionParamLists.filter:
379+
case TypeParamClause(_) => true
380+
case tpc@TermParamClause(_) => !isEvidence(tpc)
381+
382+
val evidenceMap = extractEvidences(method.termParamss)._1
383+
384+
val basicDefKind: Kind.Def = Kind.Def(paramLists.map:
385+
case TermParamClause(vds) =>
386+
Left(api.TermParameterList(
387+
vds.map(mkParameter(_, c, paramPrefix)),
388+
paramListModifier(vds)
389+
))
390+
case TypeParamClause(genericTypeList) =>
391+
Right(genericTypeList.map(td => mkTypeArgument(td, c, evidenceMap(td.symbol))))
393392
)
394393

395394
val methodKind =
@@ -456,8 +455,7 @@ trait ClassLikeSupport:
456455
def mkTypeArgument(
457456
argument: TypeDef,
458457
classDef: ClassDef,
459-
memberInfo: Map[String, TypeBounds] = Map.empty,
460-
contextBounds: Map[String, DSignature] = Map.empty,
458+
contextBounds: List[TypeRepr] = Nil,
461459
): TypeParameter =
462460
val variancePrefix: "+" | "-" | "" =
463461
if argument.symbol.flags.is(Flags.Covariant) then "+"
@@ -466,11 +464,13 @@ trait ClassLikeSupport:
466464

467465
val name = argument.symbol.normalizedName
468466
val normalizedName = if name.matches("_\\$\\d*") then "_" else name
469-
val boundsSignature = memberInfo.get(name).fold(argument.rhs.asSignature(classDef))(_.asSignature(classDef))
470-
val signature = contextBounds.get(name) match
471-
case None => boundsSignature
472-
case Some(contextBoundsSignature) =>
473-
boundsSignature ++ DSignature(Plain(" : ")) ++ contextBoundsSignature
467+
val boundsSignature = argument.rhs.asSignature(classDef)
468+
val signature = boundsSignature ++ contextBounds.flatMap(tr =>
469+
val wrap = tr match
470+
case _: TypeLambda => true
471+
case _ => false
472+
Plain(" : ") +: inParens(tr.asSignature(classDef), wrap)
473+
)
474474

475475
TypeParameter(
476476
argument.symbol.getAnnotations(),
@@ -511,9 +511,9 @@ trait ClassLikeSupport:
511511

512512
def parseValDef(c: ClassDef, valDef: ValDef): Member =
513513
def defaultKind = if valDef.symbol.flags.is(Flags.Mutable) then Kind.Var else Kind.Val
514-
val memberInfo = unwrapMemberInfo(c, valDef.symbol)
514+
val sig = valDef.tpt.tpe.asSignature(c)
515515
val kind = if valDef.symbol.flags.is(Flags.Implicit) then Kind.Implicit(Kind.Val, extractImplicitConversion(valDef.tpt.tpe))
516-
else if valDef.symbol.flags.is(Flags.Given) then Kind.Given(Kind.Val, Some(memberInfo.res.asSignature(c)), extractImplicitConversion(valDef.tpt.tpe))
516+
else if valDef.symbol.flags.is(Flags.Given) then Kind.Given(Kind.Val, Some(sig), extractImplicitConversion(valDef.tpt.tpe))
517517
else if valDef.symbol.flags.is(Flags.Enum) then Kind.EnumCase(Kind.Val)
518518
else defaultKind
519519

@@ -523,7 +523,7 @@ trait ClassLikeSupport:
523523
.filterNot(m => m == Modifier.Lazy || m == Modifier.Final)
524524
case _ => valDef.symbol.getExtraModifiers()
525525

526-
mkMember(valDef.symbol, kind, memberInfo.res.asSignature(c))(
526+
mkMember(valDef.symbol, kind, sig)(
527527
modifiers = modifiers,
528528
deprecated = valDef.symbol.isDeprecated(),
529529
experimental = valDef.symbol.isExperimental()
@@ -554,102 +554,6 @@ trait ClassLikeSupport:
554554
experimental = experimental
555555
)
556556

557-
558-
case class MemberInfo(
559-
paramLists: List[MemberInfo.ParameterList],
560-
res: TypeRepr,
561-
contextBounds: Map[String, DSignature] = Map.empty,
562-
){
563-
val genericTypes: Map[String, TypeBounds] = paramLists.collect{ case MemberInfo.TypeParameterList(types) => types }.headOption.getOrElse(Map())
564-
565-
val termParamLists: List[MemberInfo.ParameterList] = paramLists.filter(_.isTerm)
566-
}
567-
568-
object MemberInfo:
569-
enum ParameterList(val isTerm: Boolean, val isUsing: Boolean):
570-
inline def isType = !isTerm
571-
case EvidenceOnlyParameterList extends ParameterList(isTerm = true, isUsing = false)
572-
case RegularParameterList(m: Map[String, TypeRepr])(isUsing: Boolean) extends ParameterList(isTerm = true, isUsing)
573-
case TypeParameterList(m: Map[String, TypeBounds]) extends ParameterList(isTerm = false, isUsing = false)
574-
575-
export ParameterList.{RegularParameterList, EvidenceOnlyParameterList, TypeParameterList}
576-
577-
578-
579-
def unwrapMemberInfo(c: ClassDef, symbol: Symbol): MemberInfo =
580-
val qualTypeRepr = if c.symbol.isClassDef then This(c.symbol).tpe else typeForClass(c)
581-
val baseTypeRepr = qualTypeRepr.memberType(symbol)
582-
583-
def isSyntheticEvidence(name: String) =
584-
if !name.startsWith(NameKinds.ContextBoundParamName.separator) then false else
585-
// This assumes that every parameter that starts with `evidence$` and is implicit is generated by compiler to desugar context bound.
586-
// Howrever, this is just a heuristic, so
587-
// `def foo[A](evidence$1: ClassTag[A]) = 1`
588-
// will be documented as
589-
// `def foo[A: ClassTag] = 1`.
590-
// Scala spec states that `$` should not be used in names and behaviour may be undefiend in such case.
591-
// Documenting method slightly different then its definition is withing the 'undefiend behaviour'.
592-
symbol.paramSymss.flatten.find(_.name == name).exists(p =>
593-
p.flags.is(Flags.Given) || p.flags.is(Flags.Implicit))
594-
595-
def handlePolyType(memberInfo: MemberInfo, polyType: PolyType): MemberInfo =
596-
val typeParamList = MemberInfo.TypeParameterList(polyType.paramNames.zip(polyType.paramBounds).toMap)
597-
MemberInfo(memberInfo.paramLists :+ typeParamList, polyType.resType)
598-
599-
def handleMethodType(memberInfo: MemberInfo, methodType: MethodType): MemberInfo =
600-
val rawParams = methodType.paramNames.zip(methodType.paramTypes).toMap
601-
val isUsing = methodType.isImplicit
602-
val (evidences, notEvidences) = rawParams.partition(e => isSyntheticEvidence(e._1))
603-
604-
def findParamRefs(t: TypeRepr): Seq[ParamRef] = t match
605-
case paramRef: ParamRef => Seq(paramRef)
606-
case AppliedType(_, args) => args.flatMap(findParamRefs)
607-
case MatchType(bound, scrutinee, cases) =>
608-
findParamRefs(bound) ++ findParamRefs(scrutinee)
609-
case _ => Nil
610-
611-
def nameForRef(ref: ParamRef): String =
612-
val PolyType(names, _, _) = ref.binder: @unchecked
613-
names(ref.paramNum)
614-
615-
val (paramsThatLookLikeContextBounds, contextBounds) =
616-
evidences.partitionMap {
617-
case (_, AppliedType(tpe, List(typeParam: ParamRef))) =>
618-
Right(nameForRef(typeParam) -> tpe.asSignature(c))
619-
case (name, original) =>
620-
findParamRefs(original) match
621-
case Nil => Left((name, original))
622-
case typeParam :: _ =>
623-
val name = nameForRef(typeParam)
624-
val signature = Seq(
625-
Plain("(["),
626-
dotty.tools.scaladoc.Type(name, None),
627-
Plain("]"),
628-
Keyword(" =>> "),
629-
) ++ original.asSignature(c) ++ Seq(Plain(")"))
630-
Right(name -> signature.toList)
631-
}
632-
633-
val newParams = notEvidences ++ paramsThatLookLikeContextBounds
634-
635-
val termParamList = if newParams.isEmpty && contextBounds.nonEmpty
636-
then MemberInfo.EvidenceOnlyParameterList
637-
else MemberInfo.RegularParameterList(newParams)(isUsing)
638-
639-
640-
MemberInfo(memberInfo.paramLists :+ termParamList, methodType.resType, contextBounds.toMap)
641-
642-
def handleByNameType(memberInfo: MemberInfo, byNameType: ByNameType): MemberInfo =
643-
MemberInfo(memberInfo.paramLists, byNameType.underlying)
644-
645-
def recursivelyCalculateMemberInfo(memberInfo: MemberInfo): MemberInfo = memberInfo.res match
646-
case p: PolyType => recursivelyCalculateMemberInfo(handlePolyType(memberInfo, p))
647-
case m: MethodType => recursivelyCalculateMemberInfo(handleMethodType(memberInfo, m))
648-
case b: ByNameType => handleByNameType(memberInfo, b)
649-
case _ => memberInfo
650-
651-
recursivelyCalculateMemberInfo(MemberInfo(List.empty, baseTypeRepr))
652-
653557
private def paramListModifier(parameters: Seq[ValDef]): String =
654558
if parameters.size > 0 then
655559
if parameters(0).symbol.flags.is(Flags.Given) then "using "

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ trait TypesSupport:
3737

3838
private def tpe(str: String): SignaturePart = dotty.tools.scaladoc.Type(str, None)
3939

40-
private def inParens(s: SSignature, wrap: Boolean = true) =
40+
protected def inParens(s: SSignature, wrap: Boolean = true) =
4141
if wrap then plain("(").l ++ s ++ plain(")").l else s
4242

4343
extension (on: SignaturePart) def l: List[SignaturePart] = List(on)
@@ -115,8 +115,11 @@ trait TypesSupport:
115115
case AnnotatedType(tpe, _) =>
116116
inner(tpe)
117117
case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args))
118-
if paramBounds.map(inner).forall(_.isEmpty) && params.zip(args.map(inner).flatten.map(_.name)).forall(_ == _) =>
119-
inner(tpe)
118+
if paramBounds.forall { case TypeBounds(low, hi) => low.typeSymbol == defn.NothingClass && hi.typeSymbol == defn.AnyClass }
119+
&& params.length == args.length
120+
&& args.zipWithIndex.forall(_ == tl.param(_)) =>
121+
// simplify type lambdas such as [X, Y] =>> Map[X, Y] to just Map
122+
inner(tpe)
120123
case tl @ TypeLambda(params, paramBounds, resType) =>
121124
plain("[").l ++ commas(params.zip(paramBounds).map { (name, typ) =>
122125
val normalizedName = if name.matches("_\\$\\d*") then "_" else name
@@ -412,6 +415,10 @@ trait TypesSupport:
412415
try method.isParamDependent || method.isResultDependent
413416
catch case NonFatal(_) => true
414417

418+
private def isEmptyBounds(using Quotes)(tb: reflect.TypeBounds) =
419+
import reflect.*
420+
tb.low.typeSymbol == defn.NothingClass && tb.hi.typeSymbol == defn.AnyClass
421+
415422
private def stripAnnotated(using Quotes)(tr: reflect.TypeRepr): reflect.TypeRepr =
416423
import reflect.*
417424
tr match

0 commit comments

Comments
 (0)