diff --git a/scaladoc-testcases/src/tests/contextBounds.scala b/scaladoc-testcases/src/tests/contextBounds.scala new file mode 100644 index 000000000000..1925f7f40994 --- /dev/null +++ b/scaladoc-testcases/src/tests/contextBounds.scala @@ -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 \ 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..c9721f54ff18 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 => @@ -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) + )) } ) @@ -523,7 +529,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 +541,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,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) diff --git a/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala b/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala index 8c381a5710d5..c232f03a63a1 100644 --- a/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala +++ b/scaladoc/test/dotty/tools/scaladoc/signatures/SignatureTest.scala @@ -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 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)