Skip to content

Commit 8ae5611

Browse files
authored
Merge pull request #11850 from dotty-staging/scaladoc/lookup-inherited-members
Scaladoc/lookup inherited members
2 parents c0b3fde + 421af1e commit 8ae5611

File tree

8 files changed

+146
-49
lines changed

8 files changed

+146
-49
lines changed

scaladoc-testcases/src/tests/links.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ object AnObject:
99
/**
1010
* Broken link, that should result a warning not break compilation
1111
* [[tests.links.AnObject]]
12-
1312
*/
1413
class LinksTest:
1514
def verifyIfLinksTestIsGenerated(b: Int): Int
16-
= 123
15+
= 123
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package tests
2+
package lookupInheritedMembers.pack1 {
3+
class A:
4+
def x = 1
5+
val y = 1
6+
type MyType
7+
8+
}
9+
package lookupInheritedMembers.pack2 {
10+
class B extends tests.lookupInheritedMembers.pack1.A
11+
}
12+
13+
package lookupInheritedMembers {
14+
/**
15+
* [[tests.lookupInheritedMembers.pack2.B.x]]
16+
* [[tests.lookupInheritedMembers.pack2.B.y]]
17+
* [[tests.lookupInheritedMembers.pack2.B.MyType]]
18+
*/
19+
class LookupInheritedMembers
20+
}

scaladoc-testcases/src/tests/tests.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ class B extends A {
131131
class BB
132132
}
133133

134+
class BModule {
135+
def foo = "foo"
136+
}
137+
134138
/** Companion object to test linking.
135139
*
136140
* This is my member: [[B$.Z]]
@@ -141,7 +145,7 @@ class B extends A {
141145
*
142146
* And this is my term member, addressed differently: [[this.Z$]]
143147
*/
144-
object B {
148+
object B extends BModule {
145149
type Z = Int
146150
val Z: Int = 0
147151
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,8 @@ class SymOps[Q <: Quotes](val q: Q) extends JavadocAnchorCreator with Scaladoc2A
190190
// For some reason it contains `$$$` instrad of symbol name
191191
s"${sym.name}${sym.fullName}/${sym.signature.resultSig}/[${sym.signature.paramSigs.mkString("/")}]"
192192
)
193+
194+
def driInContextOfInheritingParent(par: Symbol)(using dctx: DocContext): DRI = sym.dri.copy(
195+
location = par.dri.location,
196+
externalLink = None
197+
)

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,12 @@ case class ScaladocTastyInspector()(using ctx: DocContext) extends DocTastyInspe
8080
def driFor(link: String): Option[DRI] =
8181
val symOps = new SymOps[q.type](q)
8282
import symOps._
83-
Try(QueryParser(link).readQuery()).toOption.flatMap(q =>
84-
MemberLookup.lookupOpt(q, None).map{ case (sym, _) => sym.dri}
83+
Try(QueryParser(link).readQuery()).toOption.flatMap(query =>
84+
MemberLookup.lookupOpt(query, None).map {
85+
case (sym, _, inheritingParent) => inheritingParent match
86+
case Some(parent) => sym.driInContextOfInheritingParent(parent)
87+
case None => sym.dri
88+
}
8589
)
8690

8791
ctx.staticSiteContext.foreach(_.memberLinkResolver = driFor)

scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
8383

8484
object SymOps extends SymOps[qctx.type](qctx)
8585
export SymOps.dri
86+
export SymOps.driInContextOfInheritingParent
8687

8788
def resolveLink(queryStr: String): DocLink =
8889
if SchemeUri.matches(queryStr) then DocLink.ToURL(queryStr)
@@ -94,8 +95,11 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
9495
DocLink.UnresolvedDRI(queryStr, msg)
9596
case Right(query) =>
9697
MemberLookup.lookup(using qctx)(query, owner) match
97-
case Some((sym, targetText)) =>
98-
DocLink.ToDRI(sym.dri, targetText)
98+
case Some((sym, targetText, inheritingParent)) =>
99+
var dri = inheritingParent match
100+
case Some(parent) => sym.driInContextOfInheritingParent(parent)
101+
case None => sym.dri
102+
DocLink.ToDRI(dri, targetText)
99103
case None =>
100104
val txt = s"No DRI found for query"
101105
val msg = s"$txt: $queryStr"

scaladoc/src/dotty/tools/scaladoc/tasty/comments/MemberLookup.scala

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import scala.quoted._
55

66
trait MemberLookup {
77

8+
def memberLookupResult(using Quotes)(
9+
symbol: quotes.reflect.Symbol,
10+
label: String,
11+
inheritingParent: Option[quotes.reflect.Symbol] = None
12+
): (quotes.reflect.Symbol, String, Option[quotes.reflect.Symbol]) =
13+
(symbol, label, inheritingParent)
14+
815
def lookup(using Quotes, DocContext)(
916
query: Query,
1017
owner: quotes.reflect.Symbol,
11-
): Option[(quotes.reflect.Symbol, String)] = lookupOpt(query, Some(owner))
18+
): Option[(quotes.reflect.Symbol, String, Option[quotes.reflect.Symbol])] = lookupOpt(query, Some(owner))
1219

1320
def lookupOpt(using Quotes, DocContext)(
1421
query: Query,
1522
ownerOpt: Option[quotes.reflect.Symbol],
16-
): Option[(quotes.reflect.Symbol, String)] =
23+
): Option[(quotes.reflect.Symbol, String, Option[quotes.reflect.Symbol])] =
1724
try
1825
import quotes.reflect._
1926

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

29-
val res: Option[(Symbol, String)] = {
36+
val res: Option[(Symbol, String, Option[Symbol])] = {
3037
def toplevelLookup(querystrings: List[String]) =
3138
downwardLookup(querystrings, defn.PredefModule.moduleClass)
3239
.orElse(downwardLookup(querystrings, defn.ScalaPackage))
@@ -38,7 +45,7 @@ trait MemberLookup {
3845
val nearest = nearestMembered(owner)
3946
val nearestCls = nearestClass(owner)
4047
val nearestPkg = nearestPackage(owner)
41-
def relativeLookup(querystrings: List[String], owner: Symbol): Option[Symbol] = {
48+
def relativeLookup(querystrings: List[String], owner: Symbol): Option[(Symbol, Option[Symbol])] = {
4249
val isMeaningful =
4350
owner.exists
4451
// those are just an optimisation, they can be dropped if problems show up
@@ -56,20 +63,20 @@ trait MemberLookup {
5663

5764
query match {
5865
case Query.StrictMemberId(id) =>
59-
localLookup(id, nearest).nextOption.map(_ -> id)
66+
downwardLookup(List(id), nearest).map(memberLookupResult(_, id, _))
6067
case Query.QualifiedId(Query.Qual.This, _, rest) =>
61-
downwardLookup(rest.asList, nearestCls).map(_ -> rest.join)
68+
downwardLookup(rest.asList, nearestCls).map(memberLookupResult(_, rest.join, _))
6269
case Query.QualifiedId(Query.Qual.Package, _, rest) =>
63-
downwardLookup(rest.asList, nearestPkg).map(_ -> rest.join)
70+
downwardLookup(rest.asList, nearestPkg).map(memberLookupResult(_, rest.join, _))
6471
case query =>
6572
val ql = query.asList
6673
toplevelLookup(ql)
6774
.orElse(relativeLookup(ql, nearest))
68-
.map(_ -> query.join)
75+
.map(memberLookupResult(_, query.join, _))
6976
}
7077

7178
case None =>
72-
toplevelLookup(query.asList).map(_ -> query.join)
79+
toplevelLookup(query.asList).map(memberLookupResult(_, query.join, _))
7380
}
7481
}
7582

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

104-
private def localLookup(using Quotes)(query: String, owner: quotes.reflect.Symbol): Iterator[quotes.reflect.Symbol] = {
114+
private def localLookup(using Quotes)(
115+
sel: MemberLookup.Selector,
116+
owner: quotes.reflect.Symbol
117+
): Iterator[quotes.reflect.Symbol] = {
105118
import quotes.reflect._
106119

107120
def findMatch(syms: Iterator[Symbol]): Iterator[Symbol] = {
108-
// Scaladoc overloading support allows terminal * (and they're meaningless)
109-
val cleanQuery = query.stripSuffix("*")
110-
val (q, forceTerm, forceType) =
111-
if cleanQuery endsWith "$" then
112-
(cleanQuery.init, true, false)
113-
else if cleanQuery endsWith "!" then
114-
(cleanQuery.init, false, true)
115-
else
116-
(cleanQuery, false, false)
117-
118121
def matches(s: Symbol): Boolean =
119-
s.name == q && (
120-
if forceTerm then s.isTerm
121-
else if forceType then s.isType
122-
else true
123-
)
122+
s.name == sel.ident && sel.kind.match {
123+
case MemberLookup.SelectorKind.ForceTerm => s.isTerm
124+
case MemberLookup.SelectorKind.ForceType => s.isType
125+
case MemberLookup.SelectorKind.NoForce => true
126+
}
124127

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

128131
// val syms0 = syms.toList
129132
// val matched0 = syms0.filter(matches)
130133
// if matched0.isEmpty then
131-
// println(s"Failed to look up $q in $owner; all members: {{{")
134+
// println(s"Failed to look up ${sel.ident} in $owner; all members: {{{")
132135
// syms0.foreach { s => println(s"\t$s") }
133136
// println("}}}")
134137
// val matched = matched0.iterator
135138

136139
// def showMatched() = matched0.foreach { s =>
137140
// println(s"\t $s")
138141
// }
139-
// println(s"localLookup in class ${owner} for `$q`{forceTerm=$forceTerm}:")
142+
// println(s"localLookup in class ${owner} for `${sel.ident}`{kind=${sel.kind}}:{{{")
140143
// showMatched()
144+
// println("}}}")
141145

142146
val matched = syms.filter(matches)
143147
matched.map(hackResolveModule)
@@ -150,8 +154,6 @@ trait MemberLookup {
150154
})
151155
else
152156
owner.tree match {
153-
case tree: ClassDef =>
154-
findMatch(tree.body.iterator.collect { case t: Definition if hackIsNotAbsent(t.symbol) => t.symbol })
155157
case tree: TypeDef =>
156158
val tpe =
157159
tree.rhs match {
@@ -168,14 +170,37 @@ trait MemberLookup {
168170
}
169171
}
170172

171-
private def downwardLookup(using Quotes)(query: List[String], owner: quotes.reflect.Symbol): Option[quotes.reflect.Symbol] = {
173+
private def downwardLookup(using Quotes)(
174+
query: List[String], owner: quotes.reflect.Symbol
175+
): Option[(quotes.reflect.Symbol, Option[quotes.reflect.Symbol])] = {
172176
import quotes.reflect._
173177
query match {
174178
case Nil => None
175-
case q :: Nil => localLookup(q, owner).nextOption
179+
case q :: Nil =>
180+
val sel = MemberLookup.Selector.fromString(q)
181+
val res = sel.kind match {
182+
case MemberLookup.SelectorKind.NoForce =>
183+
val lookedUp = localLookup(sel, owner).toSeq
184+
// note: those flag lookups are necessary b/c for objects we return their classes
185+
lookedUp.find(s => s.isType && !s.flags.is(Flags.Module)).orElse(
186+
lookedUp.find(s => s.isTerm || s.flags.is(Flags.Module))
187+
)
188+
case _ =>
189+
localLookup(sel, owner).nextOption
190+
}
191+
res match {
192+
case None => None
193+
case Some(sym) =>
194+
val externalOwner: Option[quotes.reflect.Symbol] =
195+
if owner eq sym.owner then None
196+
else if owner.flags.is(Flags.Module) then Some(owner.moduleClass)
197+
else if owner.isClassDef then Some(owner)
198+
else None
199+
Some(sym -> externalOwner)
200+
}
176201
case q :: qs =>
177-
val lookedUp =
178-
localLookup(q, owner).toSeq
202+
val sel = MemberLookup.Selector.fromString(q)
203+
val lookedUp = localLookup(sel, owner).toSeq
179204

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

201-
object MemberLookup extends MemberLookup
226+
object MemberLookup extends MemberLookup {
227+
enum SelectorKind {
228+
case ForceTerm
229+
case ForceType
230+
case NoForce
231+
}
232+
233+
case class Selector(ident: String, kind: SelectorKind)
234+
object Selector {
235+
def fromString(str: String) = {
236+
// Scaladoc overloading support allows terminal * (and they're meaningless)
237+
val cleanStr = str.stripSuffix("*")
238+
239+
if cleanStr endsWith "$" then
240+
Selector(cleanStr.init, SelectorKind.ForceTerm)
241+
else if cleanStr endsWith "!" then
242+
Selector(cleanStr.init, SelectorKind.ForceType)
243+
else
244+
Selector(cleanStr, SelectorKind.NoForce)
245+
}
246+
}
247+
}

scaladoc/test/dotty/tools/scaladoc/tasty/comments/MemberLookupTests.scala

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {
2828
"???" -> cls("scala.Predef$").fun("???"),
2929
"scala.List" -> cls("scala.package$").tpe("List"),
3030

31+
"scala.List.lift" -> cls("scala.PartialFunction").fun("lift"),
32+
3133
"tests.A" -> cls("tests.A"),
3234
"tests.A$" -> cls("tests.A$"),
3335
"tests.Methods.simple" -> cls("tests.Methods").fun("simple"),
@@ -36,16 +38,27 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {
3638

3739
"java.util.AbstractCollection" -> cls("java.util.AbstractCollection"),
3840
"java.lang.String" -> cls("java.lang.String"),
41+
42+
"tests.lookupInheritedMembers.pack1.A.x" ->
43+
cls("tests.lookupInheritedMembers.pack1.A").fun("x"),
44+
45+
"tests.lookupInheritedMembers.pack2.B.x" ->
46+
cls("tests.lookupInheritedMembers.pack1.A").fun("x"),
3947
)
4048

41-
cases.foreach { case (query, Sym(sym)) =>
42-
val lookupRes = MemberLookup.lookupOpt(parseQuery(query), None)
43-
assertTrue(s"Couldn't look up: $query", lookupRes.nonEmpty)
44-
val Some((lookedUp, _)) = lookupRes
45-
assertSame(query, sym, lookedUp)
49+
cases.foreach { case (query, sym) =>
50+
testOwnerlessLookup(query, sym)
4651
}
4752
}
4853

54+
def testOwnerlessLookup(query: String, wrappedTarget: Sym): Unit = {
55+
val target = wrappedTarget.symbol
56+
val lookupRes = MemberLookup.lookupOpt(parseQuery(query), None)
57+
assertTrue(s"Couldn't look up: $query", lookupRes.nonEmpty)
58+
val Some((lookedUp, _, _)) = lookupRes
59+
assertSame(query, target, lookedUp)
60+
}
61+
4962
def testOwnedLookup(): Unit = {
5063
val cases = List[((Sym, String), Sym)](
5164
cls("tests.A") -> "tests.Methods.simple" -> cls("tests.Methods").fun("simple"),
@@ -65,7 +78,7 @@ class LookupTestCases[Q <: Quotes](val q: Quotes) {
6578
cls("tests.A") -> "AA!" -> cls("tests.A").tpe("AA"),
6679
cls("tests.A") -> "AA$" -> cls("tests.A").fld("AA"),
6780

68-
cls("tests.C") -> "CC" -> cls("tests.C").fld("CC"),
81+
cls("tests.C") -> "CC" -> cls("tests.C").tpe("CC"),
6982
cls("tests.C") -> "CC$" -> cls("tests.C").fld("CC"),
7083
cls("tests.C") -> "CC!" -> cls("tests.C").tpe("CC"),
7184

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

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

107+
cls("tests.B$") -> "foo" -> cls("tests.BModule").fun("foo"),
108+
94109
cls("tests.D") -> "foo" -> cls("tests.package$").fld("foo"),
95110
cls("tests.D") -> "bar" -> cls("tests.tests$package$").fld("bar"),
96111
cls("tests.inner.A$") -> "foo" -> cls("tests.package$").fld("foo"),
97112
cls("tests.inner.A$") -> "bar" -> cls("tests.tests$package$").fld("bar"),
98113
)
99114

100115
cases.foreach { case ((Sym(owner), query), Sym(target)) =>
101-
val Some((lookedUp, _)) = MemberLookup.lookup(parseQuery(query), owner)
116+
val Some((lookedUp, _, _)) = MemberLookup.lookup(parseQuery(query), owner)
102117
assertSame(s"$owner / $query", target, lookedUp)
103118
}
104119
}

0 commit comments

Comments
 (0)