diff --git a/project/Build.scala b/project/Build.scala index b610a3255489..d8c0852f834c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1605,6 +1605,15 @@ object Build { IO.write(dest / "CNAME", "dotty.epfl.ch") }.dependsOn(generateDocumentation( roots, "Scala 3", dest.getAbsolutePath, "master", + // contains special definitions which are "transplanted" elsewhere + // and which therefore confuse Scala3doc when accessed from this pkg + "-skip-by-id:scala.runtime.stdLibPatches " + + // MatchCase is a special type that represents match type cases, + // Reflect doesn't expect to see it as a standalone definition + // and therefore it's easier just not to document it + "-skip-by-id:scala.runtime.MatchCase " + + "-skip-by-regex:.+\\.internal($|\\..+) " + + "-skip-by-regex:.+\\.impl($|\\..+) " + "-comment-syntax wiki -siteroot scala3doc/scala3-docs -project-logo scala3doc/scala3-docs/logo.svg " + "-external-mappings " + raw".*java.*" + "::" + "javadoc" + "::" + diff --git a/scala3doc/src/dotty/dokka/Scala3doc.scala b/scala3doc/src/dotty/dokka/Scala3doc.scala index b3e100f1660f..f0f7700c6e75 100644 --- a/scala3doc/src/dotty/dokka/Scala3doc.scala +++ b/scala3doc/src/dotty/dokka/Scala3doc.scala @@ -65,7 +65,9 @@ object Scala3doc: defaultSyntax: CommentSyntax = CommentSyntax.Markdown, sourceLinks: List[String] = Nil, revision: Option[String] = None, - externalMappings: List[List[String]] = List.empty + externalMappings: List[List[String]] = List.empty, + identifiersToSkip: List[String] = Nil, + regexesToSkip: List[String] = Nil, ) def run(args: Array[String], rootContext: CompilerContext): Reporter = diff --git a/scala3doc/src/dotty/dokka/Scala3docArgs.scala b/scala3doc/src/dotty/dokka/Scala3docArgs.scala index f14c2efd1e79..7e09aa9280bf 100644 --- a/scala3doc/src/dotty/dokka/Scala3docArgs.scala +++ b/scala3doc/src/dotty/dokka/Scala3docArgs.scala @@ -37,7 +37,13 @@ class Scala3docArgs extends SettingGroup with CommonScalaSettings: val externalDocumentationMappings: Setting[String] = StringSetting("-external-mappings", "external-mappings", "Mapping between regex matching class file and external documentation", "") - def scala3docSpecificSettings: Set[Setting[_]] = Set(sourceLinks, syntax, revision, externalDocumentationMappings) + val skipById: Setting[List[String]] = + MultiStringSetting("-skip-by-id", "package or class identifier", "Identifiers of packages or top-level classes to skip when generating documentation") + + val skipByRegex: Setting[List[String]] = + MultiStringSetting("-skip-by-regex", "regex", "Regexes that match fully qualified names of packages or top-level classes to skip when generating documentation") + + def scala3docSpecificSettings: Set[Setting[_]] = Set(sourceLinks, syntax, revision, externalDocumentationMappings, skipById, skipByRegex) object Scala3docArgs: def extract(args: List[String], rootCtx: CompilerContext):(Scala3doc.Args, CompilerContext) = @@ -76,7 +82,7 @@ object Scala3docArgs: val (existing, nonExisting) = inFiles.partition(_.exists) if nonExisting.nonEmpty then report.warning( - s"Scala3doc will ignore following nonexisiten paths: ${nonExisting.mkString(", ")}" + s"Scala3doc will ignore following non-existent paths: ${nonExisting.mkString(", ")}" ) val (dirs, files) = existing.partition(_.isDirectory) @@ -120,6 +126,8 @@ object Scala3docArgs: parseSyntax, sourceLinks.nonDefault.fold(Nil)(_.split(",").toList), revision.nonDefault, - externalMappings + externalMappings, + skipById.get, + skipByRegex.get, ) - (docArgs, newContext) \ No newline at end of file + (docArgs, newContext) diff --git a/scala3doc/src/dotty/dokka/tasty/TastyParser.scala b/scala3doc/src/dotty/dokka/tasty/TastyParser.scala index 0c78437d6590..ad0a676455f4 100644 --- a/scala3doc/src/dotty/dokka/tasty/TastyParser.scala +++ b/scala3doc/src/dotty/dokka/tasty/TastyParser.scala @@ -14,12 +14,16 @@ import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.model.properties.PropertyContainerKt._ import org.jetbrains.dokka.model.properties.{WithExtraProperties} -import quoted.Quotes +import java.util.regex.Pattern + +import scala.util.{Try, Success, Failure} import scala.tasty.inspector.DocTastyInspector -import dotty.dokka.model.api.withNewMembers +import scala.quoted.Quotes + +import dotty.tools.dotc + import dotty.dokka.tasty.comments.MemberLookup import dotty.dokka.tasty.comments.QueryParser -import scala.util.Try import dotty.dokka.model.api._ /** Responsible for collectively inspecting all the Tasty files we're interested in. @@ -30,23 +34,71 @@ case class DokkaTastyInspector(parser: Parser)(using ctx: DocContext) extends Do private val topLevels = Seq.newBuilder[(String, Member)] - def processCompilationUnit(using q: Quotes)(root: q.reflect.Tree): Unit = - // NOTE we avoid documenting definitions in the magical stdLibPatches directory; - // the symbols there are "patched" through dark Dotty magic onto other stdlib - // definitions, so if we documented their origin, we'd get defs with duplicate DRIs - if !root.symbol.fullName.startsWith("scala.runtime.stdLibPatches") then - val parser = new TastyParser(q, this) - - def driFor(link: String): Option[DRI] = - val symOps = new SymOps[q.type](q) - import symOps._ - Try(QueryParser(link).readQuery()).toOption.flatMap(q => - MemberLookup.lookupOpt(q, None).map{ case (sym, _) => sym.dri} - ) + def processCompilationUnit(using quotes: Quotes)(root: quotes.reflect.Tree): Unit = () + + override def postProcess(using q: Quotes): Unit = + // hack into the compiler to get a list of all top-level trees + // in principle, to do this, one would collect trees in processCompilationUnit + // however, path-dependent types disallow doing so w/o using casts + inline def hackForeachTree(thunk: q.reflect.Tree => Unit): Unit = + given dctx: dotc.core.Contexts.Context = q.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx + dctx.run.units.foreach { compilationUnit => + // mirrors code from TastyInspector + thunk(compilationUnit.tpdTree.asInstanceOf[q.reflect.Tree]) + } + + val symbolsToSkip: Set[q.reflect.Symbol] = + ctx.args.identifiersToSkip.flatMap { ref => + val qrSymbol = q.reflect.Symbol + Try(qrSymbol.requiredPackage(ref)).orElse(Try(qrSymbol.requiredClass(ref))) match { + case Success(sym) => Some(sym) + case Failure(err) => + report.warning( + s"Failed to resolve identifier to skip - $ref - because: ${throwableToString(err)}", + dotc.util.NoSourcePosition, + ) + None + } + }.toSet + + val patternsToSkip: List[Pattern] = + ctx.args.regexesToSkip.flatMap { regexString => + Try(Pattern.compile(regexString)) match + case Success(pat) => Some(pat) + case Failure(err) => + report.warning( + s"Failed to compile regex to skip - $regexString - because: ${throwableToString(err)}", + dotc.util.NoSourcePosition, + ) + None + } + + def isSkipped(sym: q.reflect.Symbol): Boolean = + def isSkippedById(sym: q.reflect.Symbol): Boolean = + if !sym.exists then false else + symbolsToSkip.contains(sym) || isSkipped(sym.owner) + + def isSkippedByRx(sym: q.reflect.Symbol): Boolean = + val symStr = sym.fullName + patternsToSkip.exists(p => p.matcher(symStr).matches()) + + isSkippedById(sym) || isSkippedByRx(sym) + + hackForeachTree { root => + if !isSkipped(root.symbol) then + val parser = new TastyParser(q, this)(isSkipped) + + def driFor(link: String): Option[DRI] = + val symOps = new SymOps[q.type](q) + import symOps._ + Try(QueryParser(link).readQuery()).toOption.flatMap(q => + MemberLookup.lookupOpt(q, None).map{ case (sym, _) => sym.dri} + ) + + ctx.staticSiteContext.foreach(_.memberLinkResolver = driFor) + topLevels ++= parser.parseRootTree(root.asInstanceOf[parser.qctx.reflect.Tree]) + } - ctx.staticSiteContext.foreach(_.memberLinkResolver = driFor) - topLevels ++= parser.parseRootTree(root.asInstanceOf[parser.qctx.reflect.Tree]) - end processCompilationUnit def result(): List[Member] = topLevels.clear() @@ -65,8 +117,14 @@ case class DokkaTastyInspector(parser: Parser)(using ctx: DocContext) extends Do }.toList /** Parses a single Tasty compilation unit. */ -case class TastyParser(qctx: Quotes, inspector: DokkaTastyInspector)(using val ctx: DocContext) - extends ScaladocSupport with BasicSupport with TypesSupport with ClassLikeSupport with SyntheticsSupport with PackageSupport with NameNormalizer: +case class TastyParser( + qctx: Quotes, + inspector: DokkaTastyInspector, +)( + isSkipped: qctx.reflect.Symbol => Boolean +)( + using val ctx: DocContext +) extends ScaladocSupport with BasicSupport with TypesSupport with ClassLikeSupport with SyntheticsSupport with PackageSupport with NameNormalizer: import qctx.reflect._ def processTree[T](tree: Tree)(op: => T): Option[T] = try Option(op) catch @@ -92,7 +150,7 @@ case class TastyParser(qctx: Quotes, inspector: DokkaTastyInspector)(using val c override def traverseTree(tree: Tree)(owner: Symbol): Unit = seen = tree :: seen - tree match { + if !isSkipped(tree.symbol) then tree match { case pck: PackageClause => docs += parsePackage(pck) super.traverseTree(tree)(owner)