Skip to content

Scaladoc: new heuristic for extension method parameter extraction #14810

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

Merged
merged 12 commits into from
Jan 5, 2023
Merged
5 changes: 4 additions & 1 deletion compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2514,7 +2514,10 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
def fullName: String = self.denot.fullName.toString

def pos: Option[Position] =
if self.exists then Some(self.sourcePos) else None
if self.exists && !self.lexicallyEnclosingClass.isOneOf(dotc.core.Flags.JavaDefined | dotc.core.Flags.Permanent) then
Some(self.sourcePos)
else
None

def docstring: Option[String] =
import dotc.core.Comments.CommentsContext
Expand Down
48 changes: 40 additions & 8 deletions scaladoc-testcases/src/tests/extensionParams.scala
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
package tests.extensionParams

trait Animal

extension [A](thiz: A)
def toTuple2[B](that: B): (A, B) = thiz -> that
def toTuple2[B](that: B): (A, B)
= thiz -> that

extension [A](a: A)(using Int)
def f[B](b: B): (A, B) = ???
def f1[B](b: B): (A, B)
= ???

extension [A](a: A)(using Int)
def ff(b: A): (A, A) = ???
def f2(b: A): (A, A)
= ???

extension [A](a: A)(using Int)
def fff(using String)(b: A): (A, A) = ???
def f3(using String)(b: A): (A, A)
= ???

extension (a: Char)(using Int)
def ffff(using String)(b: Int): Unit = ???
def f4(using String)(b: Int): Unit
= ???

extension (a: Char)(using Int)
def fffff[B](using String)(b: B): Unit = ???
def f5[B](using String)(b: B): Unit
= ???

extension [A <: List[Char]](a: Int)(using Int)
def f6[B](b: B): (A, B)
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f7[B, C](b: B)(c: C): (A, B)
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f8(b: Any)(c: Any): Any
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f9[B, C](using Int)(b: B)(c: C): (A, B)
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f10(using Int)(b: Any)(c: Any): Any
= ???

def f12(using Int)(b: A)(c: String): Number
= ???

extension [A <: List[Char]](a: A)(using Int)
def ffffff[B](b: B): (A, B) = ???
extension (using String)(using Unit)(a: Animal)(using Int)(using Number)
def f11(b: Any)(c: Any): Any
= ???
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these tests work? I see test “fixtures” but no test specification.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had answered in person, but I'll put it here for posterity:
This works by comparing the text of the file with the output of scala-meta, similarly to what is done in the compiler with the tests/pos, tests/neg folders.
This is therefore both the declaration and the specification for the tests !

4 changes: 4 additions & 0 deletions scaladoc/resources/dotty_res/styles/scalastyle.css
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,10 @@ footer .socials {
color: var(--type);
}

.groupHeader *[t="t"]:not([href]) { /* Types without links in group headers */
color: var(--type);
}

.signature *[t="t"] { /* Types with links */
color: var(--type-link);
}
Expand Down
2 changes: 1 addition & 1 deletion scaladoc/src/dotty/tools/scaladoc/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ enum Modifier(val name: String, val prefix: Boolean):
case Transparent extends Modifier("transparent", true)
case Infix extends Modifier("infix", true)

case class ExtensionTarget(name: String, typeParams: Seq[TypeParameter], argsLists: Seq[ParametersList], signature: Signature, dri: DRI, position: Long)
case class ExtensionTarget(name: String, typeParams: Seq[TypeParameter], argsLists: Seq[ParametersList], dri: DRI, position: Long)
case class ImplicitConversion(from: DRI, to: DRI)
trait ImplicitConversionProvider { def conversion: Option[ImplicitConversion] }
trait Classlike
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext

def inheritedFrom(m: Member) = m.inheritedFrom match
case Some(InheritedFrom(name, dri, isSourceSuperclassHidden)) =>
val hiddenNameSuffix = if isSourceSuperclassHidden then " (hidden)" else ""
val hiddenNameSuffix = if isSourceSuperclassHidden then " (hidden)" else ""
tableRow("Inherited from:", signatureRenderer.renderLink(name + hiddenNameSuffix, dri))
case _ => Nil

Expand Down Expand Up @@ -287,9 +287,9 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext
val argsSig = InlineSignatureBuilder()
.functionParameters(on.argsLists)
.asInstanceOf[InlineSignatureBuilder].names.reverse
val sig = typeSig ++ Signature(Plain(s"(${on.name}: ")) ++ on.signature ++ Signature(Plain(")")) ++ argsSig
MGroup(span(cls := "groupHeader")(sig.map(renderElement)), members.sortBy(_.name).toSeq, on.name)
}.toSeq
val sig = typeSig ++ argsSig
MGroup(span(cls := "groupHeader")(sig.map(renderElement)), members.sortBy(_.name).toSeq, on.name) -> on.position
}.toSeq.sortBy(_._2).map(_._1)

div(cls := "membersList")(renderTabs(
singleSelection = false,
Expand Down
9 changes: 8 additions & 1 deletion scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:

def extensionTarget(member: Member): String =
member.kind match
case Kind.Extension(on, _) => flattenToText(on.signature)
case Kind.Extension(on, _) =>
val typeSig = InlineSignatureBuilder()
.generics(on.typeParams)
.asInstanceOf[InlineSignatureBuilder].names.reverse
val argsSig = InlineSignatureBuilder()
.functionParameters(on.argsLists)
.asInstanceOf[InlineSignatureBuilder].names.reverse
flattenToText(typeSig ++ argsSig)
case _ => ""

def processPage(page: Page, pageFQName: List[String]): Seq[(JSON, Seq[String])] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ trait ClassLikeSupport:
extSym.symbol.normalizedName,
typeParams,
termParams,
extSym.tpt.asSignature,
extSym.tpt.symbol.dri,
extSym.symbol.pos.get.start
)
Expand Down Expand Up @@ -498,14 +497,13 @@ trait ClassLikeSupport:
// 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 handlePolyType(memberInfo: MemberInfo, polyType: PolyType): MemberInfo =
MemberInfo(polyType.paramNames.zip(polyType.paramBounds).toMap, memberInfo.paramLists, polyType.resType)

def handleMethodType(memberInfo: MemberInfo, methodType: MethodType): MemberInfo =
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)
Expand Down Expand Up @@ -542,7 +540,7 @@ trait ClassLikeSupport:
MemberInfo(memberInfo.genericTypes, memberInfo.paramLists, byNameType.underlying)

def recursivelyCalculateMemberInfo(memberInfo: MemberInfo): MemberInfo = memberInfo.res match
case p: PolyType => recursivelyCalculateMemberInfo(handlePolyType(p))
case p: PolyType => recursivelyCalculateMemberInfo(handlePolyType(memberInfo, p))
case m: MethodType => recursivelyCalculateMemberInfo(handleMethodType(memberInfo, m))
case b: ByNameType => handleByNameType(memberInfo, b)
case _ => memberInfo
Expand Down
51 changes: 27 additions & 24 deletions scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -146,28 +146,38 @@ object SymOps:

def extendedSymbol: Option[reflect.ValDef] =
import reflect.*
Option.when(sym.isExtensionMethod){
val termParamss = sym.tree.asInstanceOf[DefDef].termParamss
if sym.isLeftAssoc || termParamss.size == 1 then termParamss(0).params(0)
else termParamss(1).params(0)
}
if sym.isExtensionMethod then
sym.extendedTermParamLists.find(param => !param.isImplicit && !param.isGiven).flatMap(_.params.headOption)
else None

def splitExtensionParamList: (List[reflect.ParamClause], List[reflect.ParamClause]) =
import reflect.*

def comparePositionStarts(posA: Option[Position], posB: Option[Position]): Option[Boolean] =
for {
a <- posA
b <- posB
} yield a.start < b.start

sym.tree match
case tree: DefDef =>
tree.paramss.partition(_.params.headOption.flatMap(param =>
comparePositionStarts(param.symbol.pos, tree.symbol.pos)).getOrElse(false)
)
case _ => Nil -> Nil

def extendedTypeParams: List[reflect.TypeDef] =
import reflect.*
val method = sym.tree.asInstanceOf[DefDef]
method.leadingTypeParams
sym.tree match
case tree: DefDef =>
tree.leadingTypeParams
case _ => Nil

def extendedTermParamLists: List[reflect.TermParamClause] =
import reflect.*
if sym.nonExtensionLeadingTypeParams.nonEmpty then
sym.nonExtensionParamLists.takeWhile {
case _: TypeParamClause => false
case _ => true
}.collect {
case tpc: TermParamClause => tpc
}
else
List.empty
sym.splitExtensionParamList._1.collect {
case tpc: TermParamClause => tpc
}

def nonExtensionTermParamLists: List[reflect.TermParamClause] =
import reflect.*
Expand All @@ -184,14 +194,7 @@ object SymOps:
}

def nonExtensionParamLists: List[reflect.ParamClause] =
import reflect.*
val method = sym.tree.asInstanceOf[DefDef]
if sym.isExtensionMethod then
val params = method.paramss
val toDrop = if method.leadingTypeParams.nonEmpty then 2 else 1
if sym.isLeftAssoc || params.size == 1 then params.drop(toDrop)
else params.head :: params.tail.drop(toDrop)
else method.paramss
sym.splitExtensionParamList._2

def nonExtensionLeadingTypeParams: List[reflect.TypeDef] =
import reflect.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ImplicitMembersExtensionTransformer(using DocContext) extends(Module => Mo

val MyDri = c.dri
def collectApplicableMembers(source: Member): Seq[Member] = source.members.flatMap {
case m @ Member(_, _, _, Kind.Extension(ExtensionTarget(_, _, _, _, MyDri, _), _), Origin.RegularlyDefined) =>
case m @ Member(_, _, _, Kind.Extension(ExtensionTarget(_, _, _, MyDri, _), _), Origin.RegularlyDefined) =>
val kind = m.kind match
case Kind.Extension(_, d) => d
case _ => Kind.Def(Nil, Nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class MergedPackageSignatures extends SignatureTest("mergedPackage", SignatureTe

class ExtensionMethodSignature extends SignatureTest("extensionMethodSignatures", SignatureTest.all)

class ExtensionMethodParamsSignature extends SignatureTest("extensionParams", SignatureTest.all)

class ClassModifiers extends SignatureTest("classModifiers", SignatureTest.classlikeKinds)

class EnumSignatures extends SignatureTest("enumSignatures", SignatureTest.all)
Expand Down