Skip to content

Scaladoc/lookup inherited members #11850

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
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
3 changes: 1 addition & 2 deletions scaladoc-testcases/src/tests/links.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ object AnObject:
/**
* Broken link, that should result a warning not break compilation
* [[tests.links.AnObject]]

*/
class LinksTest:
def verifyIfLinksTestIsGenerated(b: Int): Int
= 123
= 123
20 changes: 20 additions & 0 deletions scaladoc-testcases/src/tests/lookupInheritedMembers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tests
package lookupInheritedMembers.pack1 {
class A:
def x = 1
val y = 1
type MyType

}
package lookupInheritedMembers.pack2 {
class B extends tests.lookupInheritedMembers.pack1.A
}

package lookupInheritedMembers {
/**
* [[tests.lookupInheritedMembers.pack2.B.x]]
* [[tests.lookupInheritedMembers.pack2.B.y]]
* [[tests.lookupInheritedMembers.pack2.B.MyType]]
*/
class LookupInheritedMembers
}
6 changes: 5 additions & 1 deletion scaladoc-testcases/src/tests/tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class B extends A {
class BB
}

class BModule {
def foo = "foo"
}

/** Companion object to test linking.
*
* This is my member: [[B$.Z]]
Expand All @@ -141,7 +145,7 @@ class B extends A {
*
* And this is my term member, addressed differently: [[this.Z$]]
*/
object B {
object B extends BModule {
type Z = Int
val Z: Int = 0
}
Expand Down
5 changes: 5 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,8 @@ class SymOps[Q <: Quotes](val q: Q) extends JavadocAnchorCreator with Scaladoc2A
// For some reason it contains `$$$` instrad of symbol name
s"${sym.name}${sym.fullName}/${sym.signature.resultSig}/[${sym.signature.paramSigs.mkString("/")}]"
)

def driInContextOfInheritingParent(par: Symbol)(using dctx: DocContext): DRI = sym.dri.copy(
location = par.dri.location,
externalLink = None
)
8 changes: 6 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,12 @@ case class ScaladocTastyInspector()(using ctx: DocContext) extends DocTastyInspe
def driFor(link: String): Option[DRI] =
val symOps = new SymOps[q.type](q)
import symOps._
Try(QueryParser(link).readQuery()).toOption.flatMap(q =>
MemberLookup.lookupOpt(q, None).map{ case (sym, _) => sym.dri}
Try(QueryParser(link).readQuery()).toOption.flatMap(query =>
MemberLookup.lookupOpt(query, None).map {
case (sym, _, inheritingParent) => inheritingParent match
case Some(parent) => sym.driInContextOfInheritingParent(parent)
case None => sym.dri
}
)

ctx.staticSiteContext.foreach(_.memberLinkResolver = driFor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {

object SymOps extends SymOps[qctx.type](qctx)
export SymOps.dri
export SymOps.driInContextOfInheritingParent

def resolveLink(queryStr: String): DocLink =
if SchemeUri.matches(queryStr) then DocLink.ToURL(queryStr)
Expand All @@ -94,8 +95,11 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
DocLink.UnresolvedDRI(queryStr, msg)
case Right(query) =>
MemberLookup.lookup(using qctx)(query, owner) match
case Some((sym, targetText)) =>
DocLink.ToDRI(sym.dri, targetText)
case Some((sym, targetText, inheritingParent)) =>
var dri = inheritingParent match
case Some(parent) => sym.driInContextOfInheritingParent(parent)
case None => sym.dri
DocLink.ToDRI(dri, targetText)
case None =>
val txt = s"No DRI found for query"
val msg = s"$txt: $queryStr"
Expand Down
116 changes: 81 additions & 35 deletions scaladoc/src/dotty/tools/scaladoc/tasty/comments/MemberLookup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import scala.quoted._

trait MemberLookup {

def memberLookupResult(using Quotes)(
symbol: quotes.reflect.Symbol,
label: String,
inheritingParent: Option[quotes.reflect.Symbol] = None
): (quotes.reflect.Symbol, String, Option[quotes.reflect.Symbol]) =
(symbol, label, inheritingParent)

def lookup(using Quotes, DocContext)(
query: Query,
owner: quotes.reflect.Symbol,
): Option[(quotes.reflect.Symbol, String)] = lookupOpt(query, Some(owner))
): Option[(quotes.reflect.Symbol, String, Option[quotes.reflect.Symbol])] = lookupOpt(query, Some(owner))

def lookupOpt(using Quotes, DocContext)(
query: Query,
ownerOpt: Option[quotes.reflect.Symbol],
): Option[(quotes.reflect.Symbol, String)] =
): Option[(quotes.reflect.Symbol, String, Option[quotes.reflect.Symbol])] =
try
import quotes.reflect._

Expand All @@ -26,7 +33,7 @@ trait MemberLookup {
def nearestMembered(sym: Symbol): Symbol =
if sym.isClassDef || sym.flags.is(Flags.Package) then sym else nearestMembered(sym.owner)

val res: Option[(Symbol, String)] = {
val res: Option[(Symbol, String, Option[Symbol])] = {
def toplevelLookup(querystrings: List[String]) =
downwardLookup(querystrings, defn.PredefModule.moduleClass)
.orElse(downwardLookup(querystrings, defn.ScalaPackage))
Expand All @@ -38,7 +45,7 @@ trait MemberLookup {
val nearest = nearestMembered(owner)
val nearestCls = nearestClass(owner)
val nearestPkg = nearestPackage(owner)
def relativeLookup(querystrings: List[String], owner: Symbol): Option[Symbol] = {
def relativeLookup(querystrings: List[String], owner: Symbol): Option[(Symbol, Option[Symbol])] = {
val isMeaningful =
owner.exists
// those are just an optimisation, they can be dropped if problems show up
Expand All @@ -56,20 +63,20 @@ trait MemberLookup {

query match {
case Query.StrictMemberId(id) =>
localLookup(id, nearest).nextOption.map(_ -> id)
downwardLookup(List(id), nearest).map(memberLookupResult(_, id, _))
case Query.QualifiedId(Query.Qual.This, _, rest) =>
downwardLookup(rest.asList, nearestCls).map(_ -> rest.join)
downwardLookup(rest.asList, nearestCls).map(memberLookupResult(_, rest.join, _))
case Query.QualifiedId(Query.Qual.Package, _, rest) =>
downwardLookup(rest.asList, nearestPkg).map(_ -> rest.join)
downwardLookup(rest.asList, nearestPkg).map(memberLookupResult(_, rest.join, _))
case query =>
val ql = query.asList
toplevelLookup(ql)
.orElse(relativeLookup(ql, nearest))
.map(_ -> query.join)
.map(memberLookupResult(_, query.join, _))
}

case None =>
toplevelLookup(query.asList).map(_ -> query.join)
toplevelLookup(query.asList).map(memberLookupResult(_, query.join, _))
}
}

Expand All @@ -88,7 +95,10 @@ trait MemberLookup {
import dotty.tools.dotc
given dotc.core.Contexts.Context = quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
val sym = rsym.asInstanceOf[dotc.core.Symbols.Symbol]
val members = sym.info.decls.iterator.filter(s => hackIsNotAbsent(s.asInstanceOf[Symbol]))
val members =
sym.info.allMembers.iterator.map(_.symbol).filter(
s => hackIsNotAbsent(s.asInstanceOf[Symbol])
)
// println(s"members of ${sym.show} : ${members.map(_.show).mkString(", ")}")
members.asInstanceOf[Iterator[Symbol]]
}
Expand All @@ -101,43 +111,37 @@ trait MemberLookup {
sym.isCompleted && sym.info.exists
}

private def localLookup(using Quotes)(query: String, owner: quotes.reflect.Symbol): Iterator[quotes.reflect.Symbol] = {
private def localLookup(using Quotes)(
sel: MemberLookup.Selector,
owner: quotes.reflect.Symbol
): Iterator[quotes.reflect.Symbol] = {
import quotes.reflect._

def findMatch(syms: Iterator[Symbol]): Iterator[Symbol] = {
// Scaladoc overloading support allows terminal * (and they're meaningless)
val cleanQuery = query.stripSuffix("*")
val (q, forceTerm, forceType) =
if cleanQuery endsWith "$" then
(cleanQuery.init, true, false)
else if cleanQuery endsWith "!" then
(cleanQuery.init, false, true)
else
(cleanQuery, false, false)

def matches(s: Symbol): Boolean =
s.name == q && (
if forceTerm then s.isTerm
else if forceType then s.isType
else true
)
s.name == sel.ident && sel.kind.match {
case MemberLookup.SelectorKind.ForceTerm => s.isTerm
case MemberLookup.SelectorKind.ForceType => s.isType
case MemberLookup.SelectorKind.NoForce => true
}

def hackResolveModule(s: Symbol): Symbol =
if s.flags.is(Flags.Module) then s.moduleClass else s

// val syms0 = syms.toList
// val matched0 = syms0.filter(matches)
// if matched0.isEmpty then
// println(s"Failed to look up $q in $owner; all members: {{{")
// println(s"Failed to look up ${sel.ident} in $owner; all members: {{{")
// syms0.foreach { s => println(s"\t$s") }
// println("}}}")
// val matched = matched0.iterator

// def showMatched() = matched0.foreach { s =>
// println(s"\t $s")
// }
// println(s"localLookup in class ${owner} for `$q`{forceTerm=$forceTerm}:")
// println(s"localLookup in class ${owner} for `${sel.ident}`{kind=${sel.kind}}:{{{")
// showMatched()
// println("}}}")

val matched = syms.filter(matches)
matched.map(hackResolveModule)
Expand All @@ -150,8 +154,6 @@ trait MemberLookup {
})
else
owner.tree match {
case tree: ClassDef =>
findMatch(tree.body.iterator.collect { case t: Definition if hackIsNotAbsent(t.symbol) => t.symbol })
case tree: TypeDef =>
val tpe =
tree.rhs match {
Expand All @@ -168,14 +170,37 @@ trait MemberLookup {
}
}

private def downwardLookup(using Quotes)(query: List[String], owner: quotes.reflect.Symbol): Option[quotes.reflect.Symbol] = {
private def downwardLookup(using Quotes)(
query: List[String], owner: quotes.reflect.Symbol
): Option[(quotes.reflect.Symbol, Option[quotes.reflect.Symbol])] = {
import quotes.reflect._
query match {
case Nil => None
case q :: Nil => localLookup(q, owner).nextOption
case q :: Nil =>
val sel = MemberLookup.Selector.fromString(q)
val res = sel.kind match {
case MemberLookup.SelectorKind.NoForce =>
val lookedUp = localLookup(sel, owner).toSeq
// note: those flag lookups are necessary b/c for objects we return their classes
lookedUp.find(s => s.isType && !s.flags.is(Flags.Module)).orElse(
lookedUp.find(s => s.isTerm || s.flags.is(Flags.Module))
)
case _ =>
localLookup(sel, owner).nextOption
}
res match {
case None => None
case Some(sym) =>
val externalOwner: Option[quotes.reflect.Symbol] =
if owner eq sym.owner then None
else if owner.flags.is(Flags.Module) then Some(owner.moduleClass)
else if owner.isClassDef then Some(owner)
else None
Some(sym -> externalOwner)
}
case q :: qs =>
val lookedUp =
localLookup(q, owner).toSeq
val sel = MemberLookup.Selector.fromString(q)
val lookedUp = localLookup(sel, owner).toSeq

if lookedUp.isEmpty then None else {
// tm/tp - term/type symbols which we looked up and which allow further lookup
Expand All @@ -198,4 +223,25 @@ trait MemberLookup {
}
}

object MemberLookup extends MemberLookup
object MemberLookup extends MemberLookup {
enum SelectorKind {
case ForceTerm
case ForceType
case NoForce
}

case class Selector(ident: String, kind: SelectorKind)
object Selector {
def fromString(str: String) = {
// Scaladoc overloading support allows terminal * (and they're meaningless)
val cleanStr = str.stripSuffix("*")

if cleanStr endsWith "$" then
Selector(cleanStr.init, SelectorKind.ForceTerm)
else if cleanStr endsWith "!" then
Selector(cleanStr.init, SelectorKind.ForceType)
else
Selector(cleanStr, SelectorKind.NoForce)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {
"???" -> cls("scala.Predef$").fun("???"),
"scala.List" -> cls("scala.package$").tpe("List"),

"scala.List.lift" -> cls("scala.PartialFunction").fun("lift"),

"tests.A" -> cls("tests.A"),
"tests.A$" -> cls("tests.A$"),
"tests.Methods.simple" -> cls("tests.Methods").fun("simple"),
Expand All @@ -36,16 +38,27 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {

"java.util.AbstractCollection" -> cls("java.util.AbstractCollection"),
"java.lang.String" -> cls("java.lang.String"),

"tests.lookupInheritedMembers.pack1.A.x" ->
cls("tests.lookupInheritedMembers.pack1.A").fun("x"),

"tests.lookupInheritedMembers.pack2.B.x" ->
cls("tests.lookupInheritedMembers.pack1.A").fun("x"),
)

cases.foreach { case (query, Sym(sym)) =>
val lookupRes = MemberLookup.lookupOpt(parseQuery(query), None)
assertTrue(s"Couldn't look up: $query", lookupRes.nonEmpty)
val Some((lookedUp, _)) = lookupRes
assertSame(query, sym, lookedUp)
cases.foreach { case (query, sym) =>
testOwnerlessLookup(query, sym)
}
}

def testOwnerlessLookup(query: String, wrappedTarget: Sym): Unit = {
val target = wrappedTarget.symbol
val lookupRes = MemberLookup.lookupOpt(parseQuery(query), None)
assertTrue(s"Couldn't look up: $query", lookupRes.nonEmpty)
val Some((lookedUp, _, _)) = lookupRes
assertSame(query, target, lookedUp)
}

def testOwnedLookup(): Unit = {
val cases = List[((Sym, String), Sym)](
cls("tests.A") -> "tests.Methods.simple" -> cls("tests.Methods").fun("simple"),
Expand All @@ -65,7 +78,7 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {
cls("tests.A") -> "AA!" -> cls("tests.A").tpe("AA"),
cls("tests.A") -> "AA$" -> cls("tests.A").fld("AA"),

cls("tests.C") -> "CC" -> cls("tests.C").fld("CC"),
cls("tests.C") -> "CC" -> cls("tests.C").tpe("CC"),
cls("tests.C") -> "CC$" -> cls("tests.C").fld("CC"),
cls("tests.C") -> "CC!" -> cls("tests.C").tpe("CC"),

Expand All @@ -91,14 +104,16 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {

cls("tests.inner.B") -> "A" -> cls("tests.inner.A$"),

cls("tests.B$") -> "foo" -> cls("tests.BModule").fun("foo"),

cls("tests.D") -> "foo" -> cls("tests.package$").fld("foo"),
cls("tests.D") -> "bar" -> cls("tests.tests$package$").fld("bar"),
cls("tests.inner.A$") -> "foo" -> cls("tests.package$").fld("foo"),
cls("tests.inner.A$") -> "bar" -> cls("tests.tests$package$").fld("bar"),
)

cases.foreach { case ((Sym(owner), query), Sym(target)) =>
val Some((lookedUp, _)) = MemberLookup.lookup(parseQuery(query), owner)
val Some((lookedUp, _, _)) = MemberLookup.lookup(parseQuery(query), owner)
assertSame(s"$owner / $query", target, lookedUp)
}
}
Expand Down