Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add heuristic to support Context Bounds #13172

Merged
merged 3 commits into from
Aug 2, 2021
Merged
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
39 changes: 39 additions & 0 deletions scaladoc-testcases/src/tests/contextBounds.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package tests
package contextBounds

import scala.reflect.ClassTag

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
= 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

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
85 changes: 78 additions & 7 deletions scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import scala.quoted._
import SymOps._
import NameNormalizer._
import SyntheticsSupport._
import dotty.tools.dotc.core.NameKinds

trait ClassLikeSupport:
self: TastyParser =>
Expand Down Expand Up @@ -476,9 +477,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)
))
}
)

Expand Down Expand Up @@ -523,20 +529,30 @@ 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 "-"
else ""

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 =
Expand Down Expand Up @@ -586,16 +602,71 @@ 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 unwrapMemberInfo(c: ClassDef, symbol: Symbol): MemberInfo =
val baseTypeRepr = memberInfo(c, symbol)

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 =
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, notEvidences) = rawParams.partition(e => isSyntheticEvidence(e._1))


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 (paramsThatLookLikeContextBounds, contextBounds) =
evidences.partitionMap {
case (_, AppliedType(tpe, List(typeParam: ParamRef))) =>
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 =
MemberInfo(memberInfo.genericTypes, memberInfo.paramLists, byNameType.underlying)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ 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")}")

val reports = missingReport ++ unexpectedReport

if !reports.isEmpty then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ class ImplicitConversionsTest3 extends SignatureTest(
)

class SpecializedSignature extends SignatureTest("specializedSignature", SignatureTest.all)

class ContextBounds extends SignatureTest("contextBounds", SignatureTest.all)