Skip to content

Scala3doc: Fix implicit conversions extraction. Add testcases #10455

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 2 commits into from
Nov 23, 2020
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
40 changes: 40 additions & 0 deletions scala3doc-testcases/src/tests/implicitConversions2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package tests

package implicitConversions2

class Methods //unexpected
{
def shouldBeImplicitlyAdded1: String
= ???
val shouldBeImplicitlyAdded2: String
= ???
class ShouldBeImplicitlyAdded3
type ShouldBeImplicitlyAdded4
}

class OuterClass //unexpected
{
implicit def conversionMethodWithOneParam(param: ClassWithConversionWithOneParam): Methods //unexpected
= ???

class ClassWithConversionWithOneParam //unexpected

class ClassWithConversionWithProperType extends InheritedClass //unexpected

class InheritedClass //unexpected

object InheritedClass //unexpected
{
implicit def conversionMethodWithProperType: Conversion[ClassWithConversionWithProperType, Methods] //unexpected
= ???
}

given conversionFromVal as Conversion[ClassWithConversionFromVal, Methods] //unexpected
{
def apply(a: ClassWithConversionFromVal): Methods //unexpected
= ???
}

class ClassWithConversionFromVal //unexpected

}
12 changes: 10 additions & 2 deletions scala3doc/src/dotty/dokka/tasty/ClassLikeSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ trait ClassLikeSupport:
private val conversionSymbol = Symbol.requiredClass("scala.Conversion")

def extractImplicitConversion(tpe: TypeRepr): Option[ImplicitConversion] =
if tpe.derivesFrom(conversionSymbol) then None
else tpe.baseType(conversionSymbol) match
if tpe.derivesFrom(conversionSymbol) then tpe.baseType(conversionSymbol) match
case AppliedType(tpe, List(from: TypeRepr, to: TypeRepr)) =>
Some(ImplicitConversion(from.typeSymbol.dri, to.typeSymbol.dri))
case _ =>
None
else None

private def parseMember(s: Tree): Option[Member] = processTreeOpt(s)(s match
case dd: DefDef if !dd.symbol.isHiddenByVisibility && !dd.symbol.isSyntheticFunc && dd.symbol.isExtensionMethod =>
Expand Down Expand Up @@ -147,6 +147,7 @@ trait ClassLikeSupport:
val parsedClasslike = parseClasslike(c)
val parentTpe = c.parents(0) match {
case t: TypeTree => Some(t.tpe)
case t: Term => Some(t.tpe)
case _ => None
}
val modifiedClasslikeExtension = ClasslikeExtension.getFrom(parsedClasslike).map(_.copy(
Expand Down Expand Up @@ -261,6 +262,13 @@ trait ClassLikeSupport:
else if methodSymbol.flags.is(Flags.Implicit) then extractImplicitConversion(method.returnTpt.tpe) match
case Some(conversion) if paramLists.size == 0 || (paramLists.size == 1 && paramLists.head.size == 0) =>
Kind.Implicit(Kind.Def, Some(conversion))
case None if paramLists.size == 1 && paramLists(0).size == 1 =>
Kind.Implicit(Kind.Def, Some(
ImplicitConversion(
paramLists(0)(0).tpt.tpe.typeSymbol.dri,
method.returnTpt.tpe.typeSymbol.dri
)
))
case _ =>
Kind.Implicit(Kind.Def, None)
else kind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ImplicitMembersExtensionTransformer(ctx: DokkaContext) extends Documentabl
case Origin.InheritedFrom(_, _) => true
case _ => false
)
newMembers.map(_.withOrigin(Origin.ImplicitlyAddedBy(owner.name, owner.dri)))
newMembers.map(_.withOrigin(Origin.ImplicitlyAddedBy(m.name, m.dri)))
}
case _ =>
Nil
Expand Down
116 changes: 59 additions & 57 deletions scala3doc/test/dotty/dokka/SignatureTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import scala.util.matching.Regex

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

import dotty.dokka.model.api.Link
import dotty.dokka.model.api._

private enum Signature:
case Expected(name: String, signature: String)
Expand All @@ -17,7 +17,8 @@ abstract class SignatureTest(
testName: String,
signatureKinds: Seq[String],
sourceFiles: List[String] = Nil,
ignoreMissingSignatures: Boolean = false
ignoreMissingSignatures: Boolean = false,
filterFunc: (Member) => Boolean = _ => true
) extends ScaladocTest(testName):
override def assertions = Assertion.AfterPagesTransformation { root =>
val sources = sourceFiles match
Expand Down Expand Up @@ -63,63 +64,64 @@ abstract class SignatureTest(

} :: Nil

// e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/
private val commentRegex = raw"\/\*<-\*\/[^\/]+\/\*->\*\/".r
private val whitespaceRegex = raw"\s+".r
private val expectedRegex = raw".+//expected: (.+)".r
private val unexpectedRegex = raw"(.+)//unexpected".r
private val identifierRegex = raw"^\s*(`.*`|(?:\w+)(?:_[^\[\(\s]+)|\w+|[^\[\(\s]+)".r

private def findMissingSingatures(expected: Seq[String], actual: Seq[String]): Set[String] =
expected.toSet &~ actual.toSet

extension (s: String):
private def startWithAnyOfThese(c: String*) = c.exists(s.startsWith)
private def compactWhitespaces = whitespaceRegex.replaceAllIn(s, " ")

private def findName(signature: String, kinds: Seq[String]): Option[String] =
for
kindMatch <- kinds.flatMap(k => s"\\b$k\\b".r.findFirstMatchIn(signature)).headOption
afterKind <- Option(kindMatch.after(0)) // to filter out nulls
nameMatch <- identifierRegex.findFirstMatchIn(afterKind)
yield nameMatch.group(1)

private def signaturesFromSources(source: Source, kinds: Seq[String]): Seq[Signature] =
source.getLines.map(_.trim)
.filterNot(_.isEmpty)
.filterNot(_.startWithAnyOfThese("=",":","{","}", "//"))
.toSeq
.flatMap {
case unexpectedRegex(signature) => findName(signature, kinds).map(Unexpected(_))
case expectedRegex(signature) => findName(signature, kinds).map(Expected(_, signature))
case signature =>
findName(signature, kinds).map(Expected(_, commentRegex.replaceAllIn(signature, "").compactWhitespaces))
}

private def signaturesFromDocumentation(root: PageNode): Seq[String] =
def flattenToText(node: ContentNode) : Seq[String] = node match
case t: ContentText => Seq(t.getText)
case c: ContentComposite =>
c.getChildren.asScala.flatMap(flattenToText).toSeq
case l: DocumentableElement =>
(l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map {
case s: String => s
case Link(s: String, _) => s
}
case _ => Seq()

def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] =
if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p))

extension (page: PageNode) def allPages: List[PageNode] = page :: page.getChildren.asScala.toList.flatMap(_.allPages)

val nodes = root.allPages
.collect { case p: ContentPage => p }
.filter( p => Option(p.getDocumentable).map(filterFunc).getOrElse(true))
.flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent))
nodes.map(flattenToText(_).mkString.compactWhitespaces.trim)

object SignatureTest {
val classlikeKinds = Seq("class", "object", "trait", "enum") // TODO add docs for packages
val members = Seq("type", "def", "val", "var")
val all = classlikeKinds ++ members
}

// e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/
private val commentRegex = raw"\/\*<-\*\/[^\/]+\/\*->\*\/".r
private val whitespaceRegex = raw"\s+".r
private val expectedRegex = raw".+//expected: (.+)".r
private val unexpectedRegex = raw"(.+)//unexpected".r
private val identifierRegex = raw"^\s*(`.*`|(?:\w+)(?:_[^\[\(\s]+)|\w+|[^\[\(\s]+)".r

private def findMissingSingatures(expected: Seq[String], actual: Seq[String]): Set[String] =
expected.toSet &~ actual.toSet

extension (s: String):
private def startWithAnyOfThese(c: String*) = c.exists(s.startsWith)
private def compactWhitespaces = whitespaceRegex.replaceAllIn(s, " ")

private def findName(signature: String, kinds: Seq[String]): Option[String] =
for
kindMatch <- kinds.flatMap(k => s"\\b$k\\b".r.findFirstMatchIn(signature)).headOption
afterKind <- Option(kindMatch.after(0)) // to filter out nulls
nameMatch <- identifierRegex.findFirstMatchIn(afterKind)
yield nameMatch.group(1)

private def signaturesFromSources(source: Source, kinds: Seq[String]): Seq[Signature] =
source.getLines.map(_.trim)
.filterNot(_.isEmpty)
.filterNot(_.startWithAnyOfThese("=",":","{","}", "//"))
.toSeq
.flatMap {
case unexpectedRegex(signature) => findName(signature, kinds).map(Unexpected(_))
case expectedRegex(signature) => findName(signature, kinds).map(Expected(_, signature))
case signature =>
findName(signature, kinds).map(Expected(_, commentRegex.replaceAllIn(signature, "").compactWhitespaces))
}

private def signaturesFromDocumentation(root: PageNode): Seq[String] =
def flattenToText(node: ContentNode) : Seq[String] = node match
case t: ContentText => Seq(t.getText)
case c: ContentComposite =>
c.getChildren.asScala.flatMap(flattenToText).toSeq
case l: DocumentableElement =>
(l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map {
case s: String => s
case Link(s: String, _) => s
}
case _ => Seq()

def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] =
if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p))

extension (page: PageNode) def allPages: List[PageNode] = page :: page.getChildren.asScala.toList.flatMap(_.allPages)

val nodes = root.allPages
.collect { case p: ContentPage => p }
.flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent))
nodes.map(flattenToText(_).mkString.compactWhitespaces.trim)
25 changes: 24 additions & 1 deletion scala3doc/test/dotty/dokka/SignatureTestCases.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dotty.dokka

import dotty.dokka.model.api._

class GenericSignaftures extends SignatureTest("genericSignatures", Seq("class"))

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

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

class WrongDocumentationLinks extends SignatureTest("links", Seq("def"))
class WrongDocumentationLinks extends SignatureTest("links", Seq("def"))

class ImplicitConversionsTest1 extends SignatureTest(
"implicitConversions2",
SignatureTest.all,
sourceFiles = List("implicitConversions2"),
filterFunc = (member) => member.name == "ClassWithConversionWithOneParam"
)

class ImplicitConversionsTest2 extends SignatureTest(
"implicitConversions2",
SignatureTest.all,
sourceFiles = List("implicitConversions2"),
filterFunc = (member) => member.name == "ClassWithConversionFromVal"
)

class ImplicitConversionsTest3 extends SignatureTest(
"implicitConversions2",
SignatureTest.all,
sourceFiles = List("implicitConversions2"),
filterFunc = (member) => member.name == "ClassWithConversionWithProperType"
)