Skip to content

Commit 23150e6

Browse files
authored
Merge pull request #10455 from pikinier20/origin-info
Scala3doc: Fix implicit conversions extraction. Add testcases
2 parents 08918c2 + b9ea710 commit 23150e6

File tree

5 files changed

+134
-61
lines changed

5 files changed

+134
-61
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package tests
2+
3+
package implicitConversions2
4+
5+
class Methods //unexpected
6+
{
7+
def shouldBeImplicitlyAdded1: String
8+
= ???
9+
val shouldBeImplicitlyAdded2: String
10+
= ???
11+
class ShouldBeImplicitlyAdded3
12+
type ShouldBeImplicitlyAdded4
13+
}
14+
15+
class OuterClass //unexpected
16+
{
17+
implicit def conversionMethodWithOneParam(param: ClassWithConversionWithOneParam): Methods //unexpected
18+
= ???
19+
20+
class ClassWithConversionWithOneParam //unexpected
21+
22+
class ClassWithConversionWithProperType extends InheritedClass //unexpected
23+
24+
class InheritedClass //unexpected
25+
26+
object InheritedClass //unexpected
27+
{
28+
implicit def conversionMethodWithProperType: Conversion[ClassWithConversionWithProperType, Methods] //unexpected
29+
= ???
30+
}
31+
32+
given conversionFromVal as Conversion[ClassWithConversionFromVal, Methods] //unexpected
33+
{
34+
def apply(a: ClassWithConversionFromVal): Methods //unexpected
35+
= ???
36+
}
37+
38+
class ClassWithConversionFromVal //unexpected
39+
40+
}

scala3doc/src/dotty/dokka/tasty/ClassLikeSupport.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@ trait ClassLikeSupport:
9898
private val conversionSymbol = Symbol.requiredClass("scala.Conversion")
9999

100100
def extractImplicitConversion(tpe: TypeRepr): Option[ImplicitConversion] =
101-
if tpe.derivesFrom(conversionSymbol) then None
102-
else tpe.baseType(conversionSymbol) match
101+
if tpe.derivesFrom(conversionSymbol) then tpe.baseType(conversionSymbol) match
103102
case AppliedType(tpe, List(from: TypeRepr, to: TypeRepr)) =>
104103
Some(ImplicitConversion(from.typeSymbol.dri, to.typeSymbol.dri))
105104
case _ =>
106105
None
106+
else None
107107

108108
private def parseMember(s: Tree): Option[Member] = processTreeOpt(s)(s match
109109
case dd: DefDef if !dd.symbol.isHiddenByVisibility && !dd.symbol.isSyntheticFunc && dd.symbol.isExtensionMethod =>
@@ -152,6 +152,7 @@ trait ClassLikeSupport:
152152
val parsedClasslike = parseClasslike(c)
153153
val parentTpe = c.parents(0) match {
154154
case t: TypeTree => Some(t.tpe)
155+
case t: Term => Some(t.tpe)
155156
case _ => None
156157
}
157158
val modifiedClasslikeExtension = ClasslikeExtension.getFrom(parsedClasslike).map(_.copy(
@@ -265,6 +266,13 @@ trait ClassLikeSupport:
265266
else if methodSymbol.flags.is(Flags.Implicit) then extractImplicitConversion(method.returnTpt.tpe) match
266267
case Some(conversion) if paramLists.size == 0 || (paramLists.size == 1 && paramLists.head.size == 0) =>
267268
Kind.Implicit(Kind.Def, Some(conversion))
269+
case None if paramLists.size == 1 && paramLists(0).size == 1 =>
270+
Kind.Implicit(Kind.Def, Some(
271+
ImplicitConversion(
272+
paramLists(0)(0).tpt.tpe.typeSymbol.dri,
273+
method.returnTpt.tpe.typeSymbol.dri
274+
)
275+
))
268276
case _ =>
269277
Kind.Implicit(Kind.Def, None)
270278
else kind

scala3doc/src/dotty/dokka/transformers/ImplicitMembersExtensionTransformer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class ImplicitMembersExtensionTransformer(ctx: DokkaContext) extends Documentabl
5151
case Origin.InheritedFrom(_, _) => true
5252
case _ => false
5353
)
54-
newMembers.map(_.withOrigin(Origin.ImplicitlyAddedBy(owner.name, owner.dri)))
54+
newMembers.map(_.withOrigin(Origin.ImplicitlyAddedBy(m.name, m.dri)))
5555
}
5656
case _ =>
5757
Nil

scala3doc/test/dotty/dokka/SignatureTest.scala

Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import scala.util.matching.Regex
66

77
import org.jetbrains.dokka.pages.{RootPageNode, PageNode, ContentPage, ContentText, ContentNode, ContentComposite}
88

9-
import dotty.dokka.model.api.Link
9+
import dotty.dokka.model.api._
1010

1111
private enum Signature:
1212
case Expected(name: String, signature: String)
@@ -17,7 +17,8 @@ abstract class SignatureTest(
1717
testName: String,
1818
signatureKinds: Seq[String],
1919
sourceFiles: List[String] = Nil,
20-
ignoreMissingSignatures: Boolean = false
20+
ignoreMissingSignatures: Boolean = false,
21+
filterFunc: (Member) => Boolean = _ => true
2122
) extends ScaladocTest(testName):
2223
override def assertions = Assertion.AfterPagesTransformation { root =>
2324
val sources = sourceFiles match
@@ -63,63 +64,64 @@ abstract class SignatureTest(
6364

6465
} :: Nil
6566

67+
// e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/
68+
private val commentRegex = raw"\/\*<-\*\/[^\/]+\/\*->\*\/".r
69+
private val whitespaceRegex = raw"\s+".r
70+
private val expectedRegex = raw".+//expected: (.+)".r
71+
private val unexpectedRegex = raw"(.+)//unexpected".r
72+
private val identifierRegex = raw"^\s*(`.*`|(?:\w+)(?:_[^\[\(\s]+)|\w+|[^\[\(\s]+)".r
73+
74+
private def findMissingSingatures(expected: Seq[String], actual: Seq[String]): Set[String] =
75+
expected.toSet &~ actual.toSet
76+
77+
extension (s: String):
78+
private def startWithAnyOfThese(c: String*) = c.exists(s.startsWith)
79+
private def compactWhitespaces = whitespaceRegex.replaceAllIn(s, " ")
80+
81+
private def findName(signature: String, kinds: Seq[String]): Option[String] =
82+
for
83+
kindMatch <- kinds.flatMap(k => s"\\b$k\\b".r.findFirstMatchIn(signature)).headOption
84+
afterKind <- Option(kindMatch.after(0)) // to filter out nulls
85+
nameMatch <- identifierRegex.findFirstMatchIn(afterKind)
86+
yield nameMatch.group(1)
87+
88+
private def signaturesFromSources(source: Source, kinds: Seq[String]): Seq[Signature] =
89+
source.getLines.map(_.trim)
90+
.filterNot(_.isEmpty)
91+
.filterNot(_.startWithAnyOfThese("=",":","{","}", "//"))
92+
.toSeq
93+
.flatMap {
94+
case unexpectedRegex(signature) => findName(signature, kinds).map(Unexpected(_))
95+
case expectedRegex(signature) => findName(signature, kinds).map(Expected(_, signature))
96+
case signature =>
97+
findName(signature, kinds).map(Expected(_, commentRegex.replaceAllIn(signature, "").compactWhitespaces))
98+
}
99+
100+
private def signaturesFromDocumentation(root: PageNode): Seq[String] =
101+
def flattenToText(node: ContentNode) : Seq[String] = node match
102+
case t: ContentText => Seq(t.getText)
103+
case c: ContentComposite =>
104+
c.getChildren.asScala.flatMap(flattenToText).toSeq
105+
case l: DocumentableElement =>
106+
(l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map {
107+
case s: String => s
108+
case Link(s: String, _) => s
109+
}
110+
case _ => Seq()
111+
112+
def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] =
113+
if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p))
114+
115+
extension (page: PageNode) def allPages: List[PageNode] = page :: page.getChildren.asScala.toList.flatMap(_.allPages)
116+
117+
val nodes = root.allPages
118+
.collect { case p: ContentPage => p }
119+
.filter( p => Option(p.getDocumentable).map(filterFunc).getOrElse(true))
120+
.flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent))
121+
nodes.map(flattenToText(_).mkString.compactWhitespaces.trim)
122+
66123
object SignatureTest {
67124
val classlikeKinds = Seq("class", "object", "trait", "enum") // TODO add docs for packages
68125
val members = Seq("type", "def", "val", "var")
69126
val all = classlikeKinds ++ members
70127
}
71-
72-
// e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/
73-
private val commentRegex = raw"\/\*<-\*\/[^\/]+\/\*->\*\/".r
74-
private val whitespaceRegex = raw"\s+".r
75-
private val expectedRegex = raw".+//expected: (.+)".r
76-
private val unexpectedRegex = raw"(.+)//unexpected".r
77-
private val identifierRegex = raw"^\s*(`.*`|(?:\w+)(?:_[^\[\(\s]+)|\w+|[^\[\(\s]+)".r
78-
79-
private def findMissingSingatures(expected: Seq[String], actual: Seq[String]): Set[String] =
80-
expected.toSet &~ actual.toSet
81-
82-
extension (s: String):
83-
private def startWithAnyOfThese(c: String*) = c.exists(s.startsWith)
84-
private def compactWhitespaces = whitespaceRegex.replaceAllIn(s, " ")
85-
86-
private def findName(signature: String, kinds: Seq[String]): Option[String] =
87-
for
88-
kindMatch <- kinds.flatMap(k => s"\\b$k\\b".r.findFirstMatchIn(signature)).headOption
89-
afterKind <- Option(kindMatch.after(0)) // to filter out nulls
90-
nameMatch <- identifierRegex.findFirstMatchIn(afterKind)
91-
yield nameMatch.group(1)
92-
93-
private def signaturesFromSources(source: Source, kinds: Seq[String]): Seq[Signature] =
94-
source.getLines.map(_.trim)
95-
.filterNot(_.isEmpty)
96-
.filterNot(_.startWithAnyOfThese("=",":","{","}", "//"))
97-
.toSeq
98-
.flatMap {
99-
case unexpectedRegex(signature) => findName(signature, kinds).map(Unexpected(_))
100-
case expectedRegex(signature) => findName(signature, kinds).map(Expected(_, signature))
101-
case signature =>
102-
findName(signature, kinds).map(Expected(_, commentRegex.replaceAllIn(signature, "").compactWhitespaces))
103-
}
104-
105-
private def signaturesFromDocumentation(root: PageNode): Seq[String] =
106-
def flattenToText(node: ContentNode) : Seq[String] = node match
107-
case t: ContentText => Seq(t.getText)
108-
case c: ContentComposite =>
109-
c.getChildren.asScala.flatMap(flattenToText).toSeq
110-
case l: DocumentableElement =>
111-
(l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map {
112-
case s: String => s
113-
case Link(s: String, _) => s
114-
}
115-
case _ => Seq()
116-
117-
def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] =
118-
if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p))
119-
120-
extension (page: PageNode) def allPages: List[PageNode] = page :: page.getChildren.asScala.toList.flatMap(_.allPages)
121-
122-
val nodes = root.allPages
123-
.collect { case p: ContentPage => p }
124-
.flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent))
125-
nodes.map(flattenToText(_).mkString.compactWhitespaces.trim)

scala3doc/test/dotty/dokka/SignatureTestCases.scala

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dotty.dokka
22

3+
import dotty.dokka.model.api._
4+
35
class GenericSignaftures extends SignatureTest("genericSignatures", Seq("class"))
46

57
class ObjectSignatures extends SignatureTest("objectSignatures", Seq("object"))
@@ -59,4 +61,25 @@ class InheritedMembers extends SignatureTest("inheritedMembers2", SignatureTest.
5961

6062
class ComplexNames extends SignatureTest("complexNames", Seq("def"))
6163

62-
class WrongDocumentationLinks extends SignatureTest("links", Seq("def"))
64+
class WrongDocumentationLinks extends SignatureTest("links", Seq("def"))
65+
66+
class ImplicitConversionsTest1 extends SignatureTest(
67+
"implicitConversions2",
68+
SignatureTest.all,
69+
sourceFiles = List("implicitConversions2"),
70+
filterFunc = (member) => member.name == "ClassWithConversionWithOneParam"
71+
)
72+
73+
class ImplicitConversionsTest2 extends SignatureTest(
74+
"implicitConversions2",
75+
SignatureTest.all,
76+
sourceFiles = List("implicitConversions2"),
77+
filterFunc = (member) => member.name == "ClassWithConversionFromVal"
78+
)
79+
80+
class ImplicitConversionsTest3 extends SignatureTest(
81+
"implicitConversions2",
82+
SignatureTest.all,
83+
sourceFiles = List("implicitConversions2"),
84+
filterFunc = (member) => member.name == "ClassWithConversionWithProperType"
85+
)

0 commit comments

Comments
 (0)