Skip to content

Add support for anchors to old scaladoc and javadoc #11564

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 3 commits into from
Mar 5, 2021
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
6 changes: 6 additions & 0 deletions scaladoc-testcases/src/tests/externalLocations/javadoc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ class Test {
def c: java.util.stream.Stream.Builder[String] = ???
}

class MyException extends java.lang.Exception

class MyArrayList[T] extends java.util.ArrayList[T]

trait MyPrintStream extends java.io.PrintStream

Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ class Test {
def c: Regex.Match = ???
}

abstract class MySeq[T] extends scala.collection.immutable.Seq[T]

4 changes: 2 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/DRI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ val topLevelDri = DRI("/")
final case class DRI(
location: String,
anchor: String = "",
origin: String = "",
externalLink: Option[String] = None,
symbolUUID: String = ""
):
def withNoOrigin = copy(origin = "")
def withNoExternalLink = copy(externalLink = None)

def isStaticFile = symbolUUID == staticFileSymbolUUID

Expand Down
34 changes: 1 addition & 33 deletions scaladoc/src/dotty/tools/scaladoc/renderers/Locations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,7 @@ trait Locations(using ctx: DocContext):
val anchor = if to.anchor.isEmpty then "" else "#" + to.anchor
pathToRaw(rawLocation(from), rawLocation(to)) +".html" + anchor
else
to.origin match
case "" =>
unknownPage(to)
case path =>
val external =
ctx.externalDocumentationLinks.find(_.originRegexes.exists(r => r.matches(path)))
external.fold(unknownPage(to))(constructPath(to))


to.externalLink.fold(unknownPage(to))(l => l)

def pathToRaw(from: Seq[String], to: Seq[String]): String =
import dotty.tools.scaladoc.util.Escape._
Expand Down Expand Up @@ -90,27 +82,3 @@ trait Locations(using ctx: DocContext):
case seq => seq.mkString("", "/", "/")

def driExisits(dri: DRI) = true // TODO implement checks!

def constructPath(dri: DRI)(link: ExternalDocLink): String =
val extension = ".html"
val docURL = link.documentationUrl.toString
def constructPathForJavadoc(dri: DRI): String = {
val location = "\\$+".r.replaceAllIn(dri.location.replace(".","/"), _ => ".")
val anchor = dri.anchor
docURL + location + extension
}

//TODO #263: Add anchor support
def constructPathForScaladoc2(dri: DRI): String =
docURL + dri.asFileLocation + extension

// TODO Add tests for it!
def constructPathForScaladoc3(dri: DRI): String =
val base = docURL + dri.asFileLocation + extension
if dri.anchor.isEmpty then base else base + "#" + dri.anchor

link.kind match {
case DocumentationKind.Javadoc => constructPathForJavadoc(dri)
case DocumentationKind.Scaladoc2 => constructPathForScaladoc2(dri)
case DocumentationKind.Scaladoc3 => constructPathForScaladoc3(dri)
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ trait ClassLikeSupport:
p.copy(
dri = p.dri.copy(
location = parentDRI.location,
origin = parentDRI.origin
externalLink = None
)
)
)
Expand Down
37 changes: 37 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/tasty/JavadocAnchorCreator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dotty.tools.scaladoc
package tasty

import scala.quoted._
import dotty.tools.scaladoc.util.Escape._
import scala.util.matching.Regex

trait JavadocAnchorCreator:
self: SymOps[_] =>

import self.q.reflect._

private val javadocPrimitivesMap = Map(
defn.IntClass -> "int",
defn.FloatClass -> "float",
defn.DoubleClass -> "double",
defn.LongClass -> "long",
defn.ByteClass -> "byte",
defn.BooleanClass -> "boolean",
defn.CharClass -> "char",
defn.ShortClass -> "short",
defn.ObjectClass -> "java.lang.Object"
)

private def transformPrimitiveType(tpe: TypeRepr): String = tpe.classSymbol
.flatMap(javadocPrimitivesMap.get)
.filter(_ => !tpe.typeSymbol.isTypeParam)
.getOrElse(tpe.show)

private def transformType(tpe: TypeRepr): String = tpe.simplified match {
case AppliedType(tpe, typeList) if tpe.classSymbol.fold(false)(_ == defn.ArrayClass) => transformType(typeList.head) + ":A"
case AppliedType(tpe, typeList) if tpe.classSymbol.fold(false)(_ == defn.RepeatedParamClass) => transformType(typeList.head) + "..."
case AppliedType(tpe, typeList) => transformPrimitiveType(tpe)
case other => transformPrimitiveType(other)
}

def getJavadocType(s: TypeRepr) = transformType(s)
7 changes: 1 addition & 6 deletions scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@ trait PackageSupport:
import qctx.reflect._

def parsePackage(pck: PackageClause): (String, Member) =
val name = extractPackageName(pck.pid.show)
val name = pck.symbol.fullName
(name, Member(name, pck.symbol.dri, Kind.Package))

def parsePackageObject(pckObj: ClassDef): (String, Member) =
pckObj.symbol.packageName -> parseClasslike(pckObj).withKind(Kind.Package)

private def extractPackageName(pidShowNoColor: String): String = {
val pidSplit = pidShowNoColor.split("\\.")
pidSplit.mkString("",".","")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dotty.tools.scaladoc
package tasty

import scala.quoted._
import dotty.tools.scaladoc.util.Escape._
import scala.util.matching.Regex

trait Scaladoc2AnchorCreator:
self: SymOps[_] =>

import self.q.reflect._

implicit private val printer: Printer[Tree] = Printer.TreeShortCode

def getScaladoc2Type(t: Tree) = {
(t match {
case d: DefDef => d.show.split("def", 2)(1)
case t: TypeDef => t.show.split("type", 2)(1)
case v: ValDef => v.show.split("val|var", 2)(1)
}).replace(" ","")
}
72 changes: 62 additions & 10 deletions scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ package dotty.tools.scaladoc
package tasty

import scala.quoted._
import dotty.tools.scaladoc.util.Escape._
import scala.collection.mutable.{ Map => MMap }
import dotty.tools.io.AbstractFile

class SymOps[Q <: Quotes](val q: Q):
class SymOps[Q <: Quotes](val q: Q) extends JavadocAnchorCreator with Scaladoc2AnchorCreator:
import q.reflect._

given Q = q

private val externalLinkCache: scala.collection.mutable.Map[AbstractFile, Option[ExternalDocLink]] = MMap()

extension (sym: Symbol)
def packageName: String = (
if (sym.isPackageDef) sym.fullName
else sym.maybeOwner.packageName
)

def packageNameSplitted: Seq[String] =
sym.packageName.split('.').toList

def className: Option[String] =
if (sym.isClassDef && !sym.flags.is(Flags.Package)) Some(
Some(sym.maybeOwner).filter(s => s.exists).flatMap(_.className).fold("")(cn => cn + "$") + sym.name
Expand Down Expand Up @@ -109,8 +118,40 @@ class SymOps[Q <: Quotes](val q: Q):
else termParamss(1).params(0)
}

private def constructPath(location: Seq[String], anchor: Option[String], link: ExternalDocLink): String =
val extension = ".html"
val docURL = link.documentationUrl.toString
def constructPathForJavadoc: String =
val l = "\\$+".r.replaceAllIn(location.mkString("/"), _ => ".")
val javadocAnchor = if anchor.isDefined then {
val paramSigs = sym.paramSymss.flatten.map(_.tree).collect {
case v: ValDef => v.tpt.tpe
}.map(getJavadocType)
"#" + sym.name + paramSigs.mkString("-","-","-")
} else ""
docURL + l + extension + javadocAnchor

//TODO #263: Add anchor support
def constructPathForScaladoc2: String =
val l = escapeUrl(location.mkString("/"))
val scaladoc2Anchor = if anchor.isDefined then {
"#" + getScaladoc2Type(sym.tree)
} else ""
docURL + l + extension + scaladoc2Anchor

// TODO Add tests for it!
def constructPathForScaladoc3: String =
val base = docURL + escapeUrl(location.mkString("/")) + extension
anchor.fold(base)(a => base + "#" + a)

link.kind match {
case DocumentationKind.Javadoc => constructPathForJavadoc
case DocumentationKind.Scaladoc2 => constructPathForScaladoc2
case DocumentationKind.Scaladoc3 => constructPathForScaladoc3
}

// TODO #22 make sure that DRIs are unique plus probably reuse semantic db code?
def dri: DRI =
def dri(using dctx: DocContext): DRI =
if sym == Symbol.noSymbol then topLevelDri
else if sym.isValDef && sym.moduleClass.exists then sym.moduleClass.dri
else
Expand All @@ -119,21 +160,32 @@ class SymOps[Q <: Quotes](val q: Q):
else if (sym.maybeOwner.isDefDef) Some(sym.owner)
else None

val originPath = {
val className = sym.className

val location = sym.packageNameSplitted ++ className

val anchor = sym.anchor

val externalLink = {
import q.reflect._
import dotty.tools.dotc
given ctx: dotc.core.Contexts.Context = q.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
val csym = sym.asInstanceOf[dotc.core.Symbols.Symbol]
Option(csym.associatedFile).fold("")(_.path)
val extLink = if externalLinkCache.contains(csym.associatedFile) then externalLinkCache(csym.associatedFile)
else {
val calculatedLink = Option(csym.associatedFile).map(_.path).flatMap( path =>
dctx.externalDocumentationLinks.find(_.originRegexes.exists(r => r.matches(path))))
externalLinkCache += (csym.associatedFile -> calculatedLink)
calculatedLink
}
extLink.map(link => constructPath(location, anchor, link))
}

val className = sym.className

DRI(
className.fold(sym.packageName)(cn => s"${sym.packageName}.${cn}"),
anchor = sym.anchor.getOrElse(""),
origin = originPath,
location.mkString("."),
anchor.getOrElse(""),
externalLink = externalLink,
// sym.show returns the same signature for def << = 1 and def >> = 2.
// For some reason it contains `$$$` instrad of symbol name
s"${sym.name}${sym.fullName}/${sym.signature.resultSig}/[${sym.signature.paramSigs.mkString("/")}]$originPath"
s"${sym.name}${sym.fullName}/${sym.signature.resultSig}/[${sym.signature.paramSigs.mkString("/")}]"
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ class JavadocExternalLocationProviderIntegrationTest extends ExternalLocationPro
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"
"https://docs.oracle.com/javase/8/docs/api/java/util/Map.html",
"https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#forEach-java.util.function.Consumer-",
"https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#toArray-T:A-",
"https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#subList-int-int-",
"https://docs.oracle.com/javase/8/docs/api/java/io/PrintStream.html#printf-java.lang.String-java.lang.Object...-",
"https://docs.oracle.com/javase/8/docs/api/java/io/PrintStream.html#write-byte:A-int-int-"
)
)

Expand All @@ -23,8 +28,10 @@ class Scaladoc2ExternalLocationProviderIntegrationTest extends ExternalLocationP
List(".*scala.*::scaladoc2::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"
"https://www.scala-lang.org/api/current/scala/Predef$.html#String",
"https://www.scala-lang.org/api/current/scala/collection/immutable/Map.html",
"https://www.scala-lang.org/api/current/scala/collection/IterableOnceOps.html#addString(b:StringBuilder,start:String,sep:String,end:String):StringBuilder",
"https://www.scala-lang.org/api/current/scala/collection/IterableOnceOps.html#mkString(start:String,sep:String,end:String):String"
)
)

Expand Down