diff --git a/scala3doc-testcases/src/tests/externalLocations/javadoc.scala b/scala3doc-testcases/src/tests/externalLocations/javadoc.scala new file mode 100644 index 000000000000..e50313e9d039 --- /dev/null +++ b/scala3doc-testcases/src/tests/externalLocations/javadoc.scala @@ -0,0 +1,12 @@ +package tests.externalJavadoc + +import java.util._ + +class Test { + def a: Map.Entry[String, String] = ??? + + def b: java.util.Map[String, Int] = ??? + + def c: java.util.stream.Stream.Builder[String] = ??? +} + diff --git a/scala3doc-testcases/src/tests/externalLocations/scala3doc.scala b/scala3doc-testcases/src/tests/externalLocations/scala3doc.scala new file mode 100644 index 000000000000..abf0b176183e --- /dev/null +++ b/scala3doc-testcases/src/tests/externalLocations/scala3doc.scala @@ -0,0 +1,12 @@ +package tests.externalScala3doc + +import scala.util.matching._ + +class Test { + def a: String = ??? + + def b: Map[String, Int] = ??? + + def c: Regex.Match = ??? +} + diff --git a/scala3doc-testcases/src/tests/externalLocations/scaladoc.scala b/scala3doc-testcases/src/tests/externalLocations/scaladoc.scala new file mode 100644 index 000000000000..bf91b0f296c0 --- /dev/null +++ b/scala3doc-testcases/src/tests/externalLocations/scaladoc.scala @@ -0,0 +1,12 @@ +package tests.externalScaladoc + +import scala.util.matching._ + +class Test { + def a: String = ??? + + def b: Map[String, Int] = ??? + + def c: Regex.Match = ??? +} + diff --git a/scala3doc-testcases/src/tests/objectSignatures.scala b/scala3doc-testcases/src/tests/objectSignatures.scala index 9f053f3fbb48..54f65ef3be50 100644 --- a/scala3doc-testcases/src/tests/objectSignatures.scala +++ b/scala3doc-testcases/src/tests/objectSignatures.scala @@ -15,5 +15,9 @@ object Base object A2 extends A[String] with C +object < + +object > + // We are not going to add final below // final object B diff --git a/scala3doc-testcases/src/tests/specializedSignature.scala b/scala3doc-testcases/src/tests/specializedSignature.scala new file mode 100644 index 000000000000..0bf6f71a8910 --- /dev/null +++ b/scala3doc-testcases/src/tests/specializedSignature.scala @@ -0,0 +1,14 @@ +package tests + +package specializedSignature + +import scala.{specialized} + +trait AdditiveMonoid[@specialized(Int, Long, Float, Double) A] +{ + def a: A + = ??? + + def b[@specialized(Int, Float) B]: B + = ??? +} \ No newline at end of file diff --git a/scala3doc/src/dotty/dokka/ExternalDocLink.scala b/scala3doc/src/dotty/dokka/ExternalDocLink.scala index a67d93ddebd1..05f34a9e1cc5 100644 --- a/scala3doc/src/dotty/dokka/ExternalDocLink.scala +++ b/scala3doc/src/dotty/dokka/ExternalDocLink.scala @@ -2,7 +2,7 @@ package dotty.dokka import java.net.URL import scala.util.matching._ -import scala.util.Try +import scala.util.{ Try, Success, Failure } case class ExternalDocLink( originRegexes: List[Regex], @@ -18,25 +18,23 @@ enum DocumentationKind: case Scala3doc extends DocumentationKind object ExternalDocLink: - def parse(mapping: String)(using CompilerContext): Option[ExternalDocLink] = - def fail(msg: String) = - report.warning(s"Unable to parocess external mapping $mapping. $msg") - None + def parse(mapping: String): Either[String, ExternalDocLink] = + def fail(msg: String) = Left(s"Unable to process external mapping $mapping. $msg") - def tryParse[T](descr: String)(op: => T): Option[T] = try Some(op) catch - case e: RuntimeException => - report.warn(s"Unable to parse $descr", e) - None + def tryParse[T](descr: String)(op: => T): Either[String, T] = Try(op) match { + case Success(v) => Right(v) + case Failure(e) => fail(s"Unable to parse $descr. Exception $e occured") + } def parsePackageList(elements: List[String]) = elements match - case List(urlStr) => tryParse("packageList")(Option(URL(urlStr))) - case Nil => Some(None) + case List(urlStr) => tryParse("packageList")(Some(URL(urlStr))) + case Nil => Right(None) case other => fail(s"Provided multiple package lists: $other") def doctoolByName(name: String) = name match - case "javadoc" => Some(DocumentationKind.Javadoc) - case "scaladoc" => Some(DocumentationKind.Scaladoc) - case "scala3doc" => Some(DocumentationKind.Scala3doc) + case "javadoc" => Right(DocumentationKind.Javadoc) + case "scaladoc" => Right(DocumentationKind.Scaladoc) + case "scala3doc" => Right(DocumentationKind.Scala3doc) case other => fail(s"Unknown doctool: $other") @@ -48,10 +46,10 @@ object ExternalDocLink: doctool <- doctoolByName(docToolStr) packageList <- parsePackageList(rest) } yield ExternalDocLink( - List(regex), - url, - doctool, - packageList - ) + List(regex), + url, + doctool, + packageList + ) case _ => fail("Accepted format: `regexStr::docToolStr::urlStr[::rest]`") \ No newline at end of file diff --git a/scala3doc/src/dotty/dokka/Scala3docArgs.scala b/scala3doc/src/dotty/dokka/Scala3docArgs.scala index b075e45373b8..0012b886e2ec 100644 --- a/scala3doc/src/dotty/dokka/Scala3docArgs.scala +++ b/scala3doc/src/dotty/dokka/Scala3docArgs.scala @@ -114,7 +114,13 @@ object Scala3docArgs: } } val externalMappings = - externalDocumentationMappings.get.flatMap(ExternalDocLink.parse) + externalDocumentationMappings.get.flatMap( s => + ExternalDocLink.parse(s).fold(left => { + report.warning(left) + None + }, right => Some(right) + ) + ) unsupportedSettings.filter(s => s.get != s.default).foreach { s => report.warning(s"Setting ${s.name} is currently not supported.") diff --git a/scala3doc/src/dotty/dokka/location/ScalaExternalLocationProvider.scala b/scala3doc/src/dotty/dokka/location/ScalaExternalLocationProvider.scala index 1db247b39a43..01f19d1d6a8b 100644 --- a/scala3doc/src/dotty/dokka/location/ScalaExternalLocationProvider.scala +++ b/scala3doc/src/dotty/dokka/location/ScalaExternalLocationProvider.scala @@ -18,37 +18,40 @@ class ScalaExternalLocationProvider( externalDocumentation: ExternalDocumentation, extension: String, kind: DocumentationKind -)(using ctx: DokkaContext) extends DefaultExternalLocationProvider(externalDocumentation, extension, ctx): +) extends ExternalLocationProvider: + def docURL = externalDocumentation.getDocumentationURL.toString.stripSuffix("/") + "/" override def resolve(dri: DRI): String = Option(externalDocumentation.getPackageList).map(_.getLocations.asScala.toMap).flatMap(_.get(dri.toString)) .fold(constructPath(dri))( l => { - this.getDocURL + l + this.docURL + l } ) private val originRegex = raw"\[origin:(.*)\]".r - override def constructPath(dri: DRI): String = kind match { + def constructPath(dri: DRI): String = kind match { case DocumentationKind.Javadoc => constructPathForJavadoc(dri) case DocumentationKind.Scaladoc => constructPathForScaladoc(dri) case DocumentationKind.Scala3doc => constructPathForScala3doc(dri) } + //TODO #263: Add anchor support + private def constructPathForJavadoc(dri: DRI): String = { val location = "\\$+".r.replaceAllIn(dri.location.replace(".","/"), _ => ".") val origin = originRegex.findFirstIn(dri.extra) val anchor = dri.anchor - getDocURL + location + extension + anchor.fold("")(a => s"#$a") + docURL + location + extension } private def constructPathForScaladoc(dri: DRI): String = { val location = dri.location.replace(".","/") val anchor = dri.anchor - getDocURL + location + extension + anchor.fold("")(a => s"#$a") + docURL + location + extension } private def constructPathForScala3doc(dri: DRI): String = { val location = dri.location.replace(".","/") val anchor = dri.anchor - getDocURL + location + anchor.fold(extension)(a => s"/$a$extension") + docURL + location + extension } diff --git a/scala3doc/src/dotty/dokka/model/api/api.scala b/scala3doc/src/dotty/dokka/model/api/api.scala index 9eb41b8a6366..483f80a50c7d 100644 --- a/scala3doc/src/dotty/dokka/model/api/api.scala +++ b/scala3doc/src/dotty/dokka/model/api/api.scala @@ -116,6 +116,7 @@ case class Parameter( ) case class TypeParameter( + annotations: Seq[Annotation], variance: "" | "+" | "-", name: String, dri: DRI, diff --git a/scala3doc/src/dotty/dokka/tasty/BasicSupport.scala b/scala3doc/src/dotty/dokka/tasty/BasicSupport.scala index 4aabb5f57ba0..da25037236cd 100644 --- a/scala3doc/src/dotty/dokka/tasty/BasicSupport.scala +++ b/scala3doc/src/dotty/dokka/tasty/BasicSupport.scala @@ -15,15 +15,22 @@ trait BasicSupport: export SymOps._ def parseAnnotation(annotTerm: Term): Annotation = + import dotty.tools.dotc.ast.Trees.{SeqLiteral} val dri = annotTerm.tpe.typeSymbol.dri + def inner(t: Term): List[Annotation.AnnotationParameter] = t match { + case i: Ident => List(Annotation.LinkParameter(None, i.tpe.typeSymbol.dri, i.name)) + case Typed(term, tpeTree) => inner(term) + case SeqLiteral(args, tpeTree) => args.map(_.asInstanceOf[Term]).flatMap(inner) + case Literal(constant) => List(Annotation.PrimitiveParameter(None, constant.show)) + case NamedArg(name, Literal(constant)) => List(Annotation.PrimitiveParameter(Some(name), constant.show)) + case x @ Select(qual, name) => List.empty + case other => List(Annotation.UnresolvedParameter(None, other.show)) + } + + val params = annotTerm match case Apply(target, appliedWith) => { - appliedWith.flatMap { - case Literal(constant) => Some(Annotation.PrimitiveParameter(None, constant.show)) - case NamedArg(name, Literal(constant)) => Some(Annotation.PrimitiveParameter(Some(name), constant.show)) - case x @ Select(qual, name) => None - case other => Some(Annotation.UnresolvedParameter(None, other.show)) - } + appliedWith.flatMap(inner) } Annotation(dri, params) diff --git a/scala3doc/src/dotty/dokka/tasty/ClassLikeSupport.scala b/scala3doc/src/dotty/dokka/tasty/ClassLikeSupport.scala index 1c99f5714de4..b823d6f6d7d7 100644 --- a/scala3doc/src/dotty/dokka/tasty/ClassLikeSupport.scala +++ b/scala3doc/src/dotty/dokka/tasty/ClassLikeSupport.scala @@ -395,6 +395,7 @@ trait ClassLikeSupport: else "" TypeParameter( + argument.symbol.getAnnotations(), variancePrefix, argument.symbol.normalizedName, argument.symbol.dri, diff --git a/scala3doc/src/dotty/dokka/translators/ScalaSignatureUtils.scala b/scala3doc/src/dotty/dokka/translators/ScalaSignatureUtils.scala index b280e7aa2e74..ad48910b0c32 100644 --- a/scala3doc/src/dotty/dokka/translators/ScalaSignatureUtils.scala +++ b/scala3doc/src/dotty/dokka/translators/ScalaSignatureUtils.scala @@ -48,6 +48,9 @@ trait SignatureBuilder extends ScalaSignatureUtils { def annotationsInline(d: Parameter): SignatureBuilder = d.annotations.foldLeft(this){ (bdr, annotation) => bdr.buildAnnotation(annotation) } + def annotationsInline(t: TypeParameter): SignatureBuilder = + t.annotations.foldLeft(this){ (bdr, annotation) => bdr.buildAnnotation(annotation) } + private def buildAnnotation(a: Annotation): SignatureBuilder = text("@").driLink(a.dri.location.split('.').last, a.dri).buildAnnotationParams(a).text(" ") @@ -77,7 +80,7 @@ trait SignatureBuilder extends ScalaSignatureUtils { text(all.toSignatureString()).text(kind + " ") def generics(on: Seq[TypeParameter]) = list(on.toList, "[", "]"){ (bdr, e) => - bdr.text(e.variance).memberName(e.name, e.dri).signature(e.signature) + bdr.annotationsInline(e).text(e.variance).memberName(e.name, e.dri).signature(e.signature) } def functionParameters(params: Seq[ParametersList]) = diff --git a/scala3doc/test/dotty/dokka/ExternalLocationProviderIntegrationTest.scala b/scala3doc/test/dotty/dokka/ExternalLocationProviderIntegrationTest.scala new file mode 100644 index 000000000000..bde7d1a406a7 --- /dev/null +++ b/scala3doc/test/dotty/dokka/ExternalLocationProviderIntegrationTest.scala @@ -0,0 +1,78 @@ +package dotty.dokka + +import scala.io.Source +import scala.jdk.CollectionConverters._ +import scala.util.matching.Regex +import dotty.dokka.test.BuildInfo +import java.nio.file.Path; +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.pages.{RootPageNode, PageNode, ContentPage, ContentText, ContentNode, ContentComposite} +import org.jsoup.Jsoup + +class JavadocExternalLocationProviderIntegrationTest extends ExternalLocationProviderIntegrationTest( + "externalJavadoc", + List(".*java.*::javadoc::https://docs.oracle.com/javase/8/docs/api/"), + List( + "https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html", + "https://docs.oracle.com/javase/8/docs/api/java/util/Map.Entry.html", + "https://docs.oracle.com/javase/8/docs/api/java/util/Map.html" + ) +) + +class ScaladocExternalLocationProviderIntegrationTest extends ExternalLocationProviderIntegrationTest( + "externalScaladoc", + List(".*scala.*::scaladoc::https://www.scala-lang.org/api/current/"), + List( + "https://www.scala-lang.org/api/current/scala/util/matching/Regex$$Match.html", + "https://www.scala-lang.org/api/current/scala/Predef$.html", + "https://www.scala-lang.org/api/current/scala/collection/immutable/Map.html" + ) +) + +class Scala3docExternalLocationProviderIntegrationTest extends ExternalLocationProviderIntegrationTest( + "externalScala3doc", + List(".*scala.*::scala3doc::https://dotty.epfl.ch/api/"), + List( + "https://dotty.epfl.ch/api/scala/collection/immutable/Map.html", + "https://dotty.epfl.ch/api/scala/Predef$.html", + "https://dotty.epfl.ch/api/scala/util/matching/Regex$$Match.html" + ) +) + + +abstract class ExternalLocationProviderIntegrationTest(name: String, mappings: Seq[String], expectedLinks: Seq[String]) extends ScaladocTest(name): + override def args = super.args.copy( + externalMappings = mappings.flatMap( s => + ExternalDocLink.parse(s).fold(left => None, right => Some(right) + ) + ).toList + ) + + def assertions = Assertion.AfterRendering { (root, ctx) => + given DokkaContext = ctx + val output = summon[DocContext].args.output.toPath.resolve("api") + val linksBuilder = List.newBuilder[String] + + def processFile(path: Path): Unit = + val document = Jsoup.parse(IO.read(path)) + val content = document.select(".documentableElement").forEach { elem => + val hrefValues = elem.select("a").asScala.map { a => + a.attr("href") + } + linksBuilder ++= hrefValues + } + + + IO.foreachFileIn(output, processFile) + val links = linksBuilder.result + val errors = expectedLinks.flatMap(expect => Option.when(!links.contains(expect))(expect)) + if !errors.isEmpty then { + val reportMessage = + "External location provider integration test failed.\n" + + "Missing links:\n" + + errors.mkString("\n","\n","\n") + + "Found links:" + links.mkString("\n","\n","\n") + reportError(reportMessage) + } + } :: Nil + diff --git a/scala3doc/test/dotty/dokka/ExternalLocationProviderTest.scala b/scala3doc/test/dotty/dokka/ExternalLocationProviderTest.scala new file mode 100644 index 000000000000..fb71e0ff02f2 --- /dev/null +++ b/scala3doc/test/dotty/dokka/ExternalLocationProviderTest.scala @@ -0,0 +1,76 @@ +package dotty.dokka + +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.pages.ModulePage +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.base.resolvers.external._ +import org.jetbrains.dokka.base.resolvers.shared.{ExternalDocumentation => ED, _} +import org.jetbrains.dokka.base.resolvers.local._ +import org.jetbrains.dokka.model.DisplaySourceSet +import dotty.dokka.withNoOrigin +import dotty.dokka.tasty._ + +import scala.collection.JavaConverters._ +import java.nio.file.Paths +import java.nio.file.Path +import scala.util.matching._ +import dotty.dokka.model.api._ +import java.net.URL +import org.junit.{Test} +import org.junit.Assert._ + +import scala.quoted._ + +class ExternalLocationProviderTest: + def createExternalLocationProvider(docURL: String, ext: String, kind: DocumentationKind) = { + val emptyExtDoc = ED( + URL(docURL), + PackageList( + RecognizedLinkFormat.Javadoc1, JSet(), JMap(), URL(docURL) + ) + ) + ScalaExternalLocationProvider(emptyExtDoc, ext, kind) + } + + def testResolvedLinks(provider: ScalaExternalLocationProvider, testcases: List[(DRI, String)]) = testcases.foreach { + case (dri, expect) => assertEquals(provider.resolve(dri), expect) + } + + @Test + def javadocExternalLocationProviderTest(): Unit = { + val provider = createExternalLocationProvider("https://docs.oracle.com/javase/8/docs/api/", ".html", DocumentationKind.Javadoc) + val testcases = List( + (DRI("java.util.Map$$Entry"), "https://docs.oracle.com/javase/8/docs/api/java/util/Map.Entry.html"), + (DRI("javax.swing.plaf.nimbus.AbstractRegionPainter$$PaintContext$$CacheMode"), "https://docs.oracle.com/javase/8/docs/api/javax/swing/plaf/nimbus/AbstractRegionPainter.PaintContext.CacheMode.html"), + (DRI("java.lang.CharSequence"), "https://docs.oracle.com/javase/8/docs/api/java/lang/CharSequence.html") + ) + testResolvedLinks(provider, testcases) + } + + @Test + def scaladocExternalLocationProviderTest(): Unit = { + val provider = createExternalLocationProvider("https://www.scala-lang.org/api/current/", ".html", DocumentationKind.Scaladoc) + val testcases = List( + (DRI("scala.Predef$"),"https://www.scala-lang.org/api/current/scala/Predef$.html"), + (DRI("scala.util.package$$chaining$"), "https://www.scala-lang.org/api/current/scala/util/package$$chaining$.html"), + (DRI("scala.util.Using$"), "https://www.scala-lang.org/api/current/scala/util/Using$.html"), + (DRI("scala.util.matching.Regex$$Match"), "https://www.scala-lang.org/api/current/scala/util/matching/Regex$$Match.html") + ) + testResolvedLinks(provider, testcases) + } + + @Test + def scala3docExternalLocationProviderTest(): Unit = { + val provider = createExternalLocationProvider("https://dotty.epfl.ch/api/", ".html", DocumentationKind.Scala3doc) + val testcases = List( + (DRI("scala.Predef$"),"https://dotty.epfl.ch/api/scala/Predef$.html"), + (DRI("scala.util.package$$chaining$"), "https://dotty.epfl.ch/api/scala/util/package$$chaining$.html"), + (DRI("scala.util.Using$"), "https://dotty.epfl.ch/api/scala/util/Using$.html"), + (DRI("scala.util.matching.Regex$$Match"), "https://dotty.epfl.ch/api/scala/util/matching/Regex$$Match.html") + ) + testResolvedLinks(provider, testcases) + } \ No newline at end of file diff --git a/scala3doc/test/dotty/dokka/ScaladocTest.scala b/scala3doc/test/dotty/dokka/ScaladocTest.scala index 36e3b4241d42..c9644035989d 100644 --- a/scala3doc/test/dotty/dokka/ScaladocTest.scala +++ b/scala3doc/test/dotty/dokka/ScaladocTest.scala @@ -20,7 +20,7 @@ abstract class ScaladocTest(val name: String): folder.create() folder - private def args = Scala3doc.Args( + def args = Scala3doc.Args( name = "test", tastyFiles = tastyFiles(name), output = getTempDir().getRoot, diff --git a/scala3doc/test/dotty/dokka/SignatureTestCases.scala b/scala3doc/test/dotty/dokka/SignatureTestCases.scala index 49ebd624ef01..2320fe51fe3b 100644 --- a/scala3doc/test/dotty/dokka/SignatureTestCases.scala +++ b/scala3doc/test/dotty/dokka/SignatureTestCases.scala @@ -82,4 +82,6 @@ class ImplicitConversionsTest3 extends SignatureTest( SignatureTest.all, sourceFiles = List("implicitConversions2"), filterFunc = _.toString.endsWith("ClassWithConversionWithProperType.html") -) \ No newline at end of file +) + +class SpecializedSignature extends SignatureTest("specializedSignature", SignatureTest.all) \ No newline at end of file