scala.tools.nsc.ast.DocComments.scala Maven / Gradle / Ivy
The newest version!
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala.tools.nsc
package ast
import scala.annotation.tailrec
import symtab._
import util.DocStrings._
import scala.collection.mutable
import scala.tools.nsc.Reporting.WarningCategory
/*
* @author Martin Odersky
*/
trait DocComments { self: Global =>
val cookedDocComments = mutable.HashMap[Symbol, String]()
/** The raw doc comment map
*
* In IDE, background compilation runs get interrupted by
* reloading new sourcefiles. This is weak to avoid
* memleaks due to the doc of their cached symbols
* (e.g. in baseTypeSeq) between periodic doc reloads.
*/
val docComments = mutable.WeakHashMap[Symbol, DocComment]()
def clearDocComments(): Unit = {
cookedDocComments.clear()
docComments.clear()
defs.clear()
}
/** The raw doc comment of symbol `sym`, as it appears in the source text, "" if missing.
*/
def rawDocComment(sym: Symbol): String =
docComments get sym map (_.raw) getOrElse ""
/** The position of the raw doc comment of symbol `sym`, or NoPosition if missing
* If a symbol does not have a doc comment but some overridden version of it does,
* the position of the doc comment of the overridden version is returned instead.
*/
def docCommentPos(sym: Symbol): Position =
getDocComment(sym) map (_.pos) getOrElse NoPosition
/** A version which doesn't consider self types, as a temporary measure:
* an infinite loop has broken out between superComment and cookedDocComment
* since r23926.
*/
private def allInheritedOverriddenSymbols(sym: Symbol): List[Symbol] = {
val getter: Symbol = sym.getter
val symOrGetter = getter.orElse(sym)
if (!symOrGetter.owner.isClass) Nil
else symOrGetter.owner.ancestors map (symOrGetter overriddenSymbol _) filter (_ != NoSymbol)
}
def fillDocComment(sym: Symbol, comment: DocComment): Unit = {
docComments(sym) = comment
comment.defineVariables(sym)
}
def replaceInheritDocToInheritdoc(docStr: String):String = {
docStr.replaceAll("""\{@inheritDoc\p{Zs}*\}""", "@inheritdoc")
}
/** The raw doc comment of symbol `sym`, minus usecase and define sections, augmented by
* missing sections of an inherited doc comment.
* If a symbol does not have a doc comment but some overridden version of it does,
* the doc comment of the overridden version is copied instead.
*/
def cookedDocComment(sym: Symbol, docStr: String = ""): String = cookedDocComments.getOrElseUpdate(sym, {
val ownComment = replaceInheritDocToInheritdoc {
if (docStr.length == 0) docComments get sym map (_.template) getOrElse ""
else DocComment(docStr).template
}
superComment(sym) match {
case None =>
// scala/bug#8210 - The warning would be false negative when this symbol is a setter
if (ownComment.indexOf("@inheritdoc") != -1 && ! sym.isSetter)
runReporting.warning(sym.pos, s"The comment for ${sym} contains @inheritdoc, but no parent comment is available to inherit from.", WarningCategory.Scaladoc, sym)
ownComment.replace("@inheritdoc", "")
case Some(sc) =>
if (ownComment == "") sc
else expandInheritdoc(sc, merge(sc, ownComment, sym), sym)
}
})
/** The cooked doc comment of symbol `sym` after variable expansion, or "" if missing.
*
* @param sym The symbol for which doc comment is returned
* @param site The class for which doc comments are generated
* @throws ExpansionLimitExceeded when more than 10 successive expansions
* of the same string are done, which is
* interpreted as a recursive variable definition.
*/
def expandedDocComment(sym: Symbol, site: Symbol, docStr: String = ""): String = {
// when parsing a top level class or module, use the (module-)class itself to look up variable definitions
val site1 = if ((sym.isModule || sym.isClass) && site.hasPackageFlag) sym
else site
expandVariables(cookedDocComment(sym, docStr), sym, site1)
}
/** The list of use cases of doc comment of symbol `sym` seen as a member of class
* `site`. Each use case consists of a synthetic symbol (which is entered nowhere else),
* of an expanded doc comment string, and of its position.
*
* @param sym The symbol for which use cases are returned
* @param site The class for which doc comments are generated
* @throws ExpansionLimitExceeded when more than 10 successive expansions
* of the same string are done, which is
* interpreted as a recursive variable definition.
*/
def useCases(sym: Symbol, site: Symbol): List[(Symbol, String, Position)] = {
def getUseCases(dc: DocComment) = {
val fullSigComment = cookedDocComment(sym)
for (uc <- dc.useCases; defn <- uc.expandedDefs(sym, site)) yield {
// use cases comments go through a series of transformations:
// 1 - filling in missing sections from the full signature
// 2 - expanding explicit inheritance @inheritdoc tags
// 3 - expanding variables like $COLL
val useCaseCommentRaw = uc.comment.raw
val useCaseCommentMerged = merge(fullSigComment, useCaseCommentRaw, defn)
val useCaseCommentInheritdoc = expandInheritdoc(fullSigComment, useCaseCommentMerged, sym)
val useCaseCommentVariables = expandVariables(useCaseCommentInheritdoc, sym, site)
(defn, useCaseCommentVariables, uc.pos)
}
}
getDocComment(sym) map getUseCases getOrElse List()
}
private def getDocComment(sym: Symbol): Option[DocComment] =
mapFind(sym :: allInheritedOverriddenSymbols(sym))(docComments get _)
/** The cooked doc comment of an overridden symbol */
protected def superComment(sym: Symbol): Option[String] = {
allInheritedOverriddenSymbols(sym).iterator
.map(cookedDocComment(_))
.find(_ != "")
}
private def mapFind[A, B](xs: Iterable[A])(f: A => Option[B]): Option[B] =
xs collectFirst scala.Function.unlift(f)
private def isMovable(str: String, sec: (Int, Int)): Boolean =
startsWithTag(str, sec, "@param") ||
startsWithTag(str, sec, "@tparam") ||
startsWithTag(str, sec, "@return")
/** Merge elements of doccomment `src` into doc comment `dst` for symbol `sym`.
* In detail:
* 1. If `copyFirstPara` is true, copy first paragraph
* 2. For all parameters of `sym` if there is no @param section
* in `dst` for that parameter name, but there is one on `src`, copy that section.
* 3. If there is no @return section in `dst` but there is one in `src`, copy it.
*/
def merge(src: String, dst: String, sym: Symbol, copyFirstPara: Boolean = false): String = {
val srcSections = tagIndex(src)
val dstSections = tagIndex(dst)
val srcParams = paramDocs(src, "@param", srcSections)
val dstParams = paramDocs(dst, "@param", dstSections)
val srcTParams = paramDocs(src, "@tparam", srcSections)
val dstTParams = paramDocs(dst, "@tparam", dstSections)
val out = new StringBuilder
var copied = 0
var tocopy = startTag(dst, dstSections dropWhile (!isMovable(dst, _)))
if (copyFirstPara) {
val eop = // end of comment body (first para), which is delimited by blank line, or tag, or end of comment
(findNext(src, 0)(src.charAt(_) == '\n')) min startTag(src, srcSections)
out append src.substring(0, eop).trim
copied = 3
tocopy = 3
}
def mergeSection(srcSec: Option[(Int, Int)], dstSec: Option[(Int, Int)]) = dstSec match {
case Some((_, end)) =>
if (end > tocopy) tocopy = end
case None =>
srcSec match {
case Some((start1, end1)) => {
out append dst.substring(copied, tocopy).trim
out append "\n"
copied = tocopy
out append src.substring(start1, end1).trim
}
case None =>
}
}
for (params <- sym.paramss; param <- params)
mergeSection(srcParams get param.name.toString, dstParams get param.name.toString)
for (tparam <- sym.typeParams)
mergeSection(srcTParams get tparam.name.toString, dstTParams get tparam.name.toString)
mergeSection(returnDoc(src, srcSections), returnDoc(dst, dstSections))
mergeSection(groupDoc(src, srcSections), groupDoc(dst, dstSections))
if (out.length == 0) dst
else {
out append dst.substring(copied)
out.toString
}
}
/**
* Expand inheritdoc tags
* - for the main comment we transform the inheritdoc into the super variable,
* and the variable expansion can expand it further
* - for the param, tparam and throws sections we must replace comments on the spot
*
* This is done separately, for two reasons:
* 1. It takes longer to run compared to merge
* 2. The inheritdoc annotation should not be used very often, as building the comment from pieces severely
* impacts performance
*
* @param parent The source (or parent) comment
* @param child The child (overriding member or usecase) comment
* @param sym The child symbol
* @return The child comment with the inheritdoc sections expanded
*/
def expandInheritdoc(parent: String, child: String, sym: Symbol): String =
if (child.indexOf("@inheritdoc") == -1)
child
else {
val parentSections = tagIndex(parent)
val childSections = tagIndex(child)
val parentTagMap = sectionTagMap(parent, parentSections)
val parentNamedParams = Map() +
("@param" -> paramDocs(parent, "@param", parentSections)) +
("@tparam" -> paramDocs(parent, "@tparam", parentSections)) +
("@throws" -> paramDocs(parent, "@throws", parentSections))
val out = new StringBuilder
def replaceInheritdoc(childSection: String, parentSection: => String) =
if (childSection.indexOf("@inheritdoc") == -1)
childSection
else
childSection.replace("@inheritdoc", parentSection)
def getParentSection(section: (Int, Int)): String = {
def getSectionHeader = extractSectionTag(child, section) match {
case param@("@param"|"@tparam"|"@throws") => param + " " + extractSectionParam(child, section)
case other => other
}
def sectionString(param: String, paramMap: Map[String, (Int, Int)]): String =
paramMap.get(param) match {
case Some(paramSection) =>
val (start, end) = extractSectionText(parent, paramSection)
cleanupSectionText(parent.substring(start, end)) // Cleanup the section tag and parameter
case None =>
reporter.echo(sym.pos, s"""The "$getSectionHeader" annotation of the $sym comment contains @inheritdoc, but the corresponding section in the parent is not defined.""")
""
}
child.substring(section._1, section._1 + 7) match {
case param@("@param "|"@tparam"|"@throws") =>
sectionString(extractSectionParam(child, section), parentNamedParams(param.trim))
case _ =>
sectionString(extractSectionTag(child, section), parentTagMap)
}
}
def mainComment(str: String, sections: List[(Int, Int)]): String =
if (str.trim.length > 3)
str.trim.substring(3, startTag(str, sections))
else
""
// Append main comment
out.append("/**")
out.append(replaceInheritdoc(mainComment(child, childSections), mainComment(parent, parentSections)))
// Append sections
for (section <- childSections)
out.append(replaceInheritdoc(child.substring(section._1, section._2), getParentSection(section)))
out.append("*/")
out.toString
}
/** Maps symbols to the variable -> replacement maps that are defined
* in their doc comments
*/
private val defs = mutable.HashMap[Symbol, Map[String, String]]() withDefaultValue Map()
/** Lookup definition of variable.
*
* @param vble The variable for which a definition is searched
* @param site The class for which doc comments are generated
*/
@tailrec
final def lookupVariable(vble: String, site: Symbol): Option[String] = site match {
case NoSymbol => None
case _ =>
val searchList =
if (site.isModule) site :: site.info.baseClasses
else site.info.baseClasses
searchList collectFirst { case x if defs(x) contains vble => defs(x)(vble) } match {
case Some(str) if str startsWith "$" => lookupVariable(str.tail, site)
case s @ Some(_) => s
case None => lookupVariable(vble, site.owner)
}
}
/** Expand variable occurrences in string `str`, until a fix point is reached or
* an expandLimit is exceeded.
*
* @param initialStr The string to be expanded
* @param sym The symbol for which doc comments are generated
* @param site The class for which doc comments are generated
* @return Expanded string
*/
protected def expandVariables(initialStr: String, sym: Symbol, site: Symbol): String = {
val expandLimit = 10
@tailrec
def expandInternal(str: String, depth: Int): String = {
if (depth >= expandLimit)
throw new ExpansionLimitExceeded(str)
val out = new StringBuilder
var copied, idx = 0
// excluding variables written as \$foo so we can use them when
// necessary to document things like Symbol#decode
def isEscaped = idx > 0 && str.charAt(idx - 1) == '\\'
while (idx < str.length) {
if ((str charAt idx) != '$' || isEscaped)
idx += 1
else {
val vstart = idx
idx = skipVariable(str, idx + 1)
def replaceWith(repl: String): Unit = {
out append str.substring(copied, vstart)
out append repl
copied = idx
}
variableName(str.substring(vstart + 1, idx)) match {
case "super" =>
superComment(sym) foreach { sc =>
val superSections = tagIndex(sc)
replaceWith(sc.substring(3, startTag(sc, superSections)))
for (sec @ (start, end) <- superSections)
if (!isMovable(sc, sec)) out append sc.substring(start, end)
}
case "" => idx += 1
case vname =>
lookupVariable(vname, site) match {
case Some(replacement) => replaceWith(replacement)
case None =>
val pos = docCommentPos(sym)
val loc = if (pos.isDefined) pos.withPoint(pos.start + vstart + 1) else NoPosition
runReporting.warning(loc, s"Variable $vname undefined in comment for $sym in $site", WarningCategory.Scaladoc, sym)
}
}
}
}
if (out.length == 0) str
else {
out append str.substring(copied)
expandInternal(out.toString, depth + 1)
}
}
// We suppressed expanding \$ throughout the recursion, and now we
// need to replace \$ with $ so it looks as intended.
expandInternal(initialStr, 0).replace("""\$""", "$")
}
// !!! todo: inherit from Comment?
case class DocComment(raw: String, pos: Position = NoPosition, codePos: Position = NoPosition) {
/** Returns:
* template: the doc comment minus all @define and @usecase sections
* defines : all define sections (as strings)
* useCases: all usecase sections (as instances of class UseCase)
*/
lazy val (template, defines, useCases) = {
val sections = tagIndex(raw)
val defines = sections filter { startsWithTag(raw, _, "@define") }
val usecases = sections filter { startsWithTag(raw, _, "@usecase") }
val end = startTag(raw, (defines ::: usecases).sortBy(_._1))
(if (end == raw.length - 2) raw else raw.substring(0, end) + "*/",
defines map { case (start, end) => raw.substring(start, end) },
usecases map { case (start, end) => decomposeUseCase(start, end) })
}
private def decomposeUseCase(start: Int, end: Int): UseCase = {
val codeStart = skipWhitespace(raw, start + "@usecase".length)
val codeEnd = skipToEol(raw, codeStart)
val code = raw.substring(codeStart, codeEnd)
val codePos = subPos(codeStart, codeEnd)
val commentStart = skipLineLead(raw, codeEnd + 1) min end
val comment = "/** " + raw.substring(commentStart, end) + "*/"
val commentPos = subPos(commentStart, end)
runReporting.deprecationWarning(codePos, "The @usecase tag is deprecated, instead use the @example tag to document the usage of your API", "2.13.0", site = "", origin = "")
UseCase(DocComment(comment, commentPos, codePos), code, codePos)
}
private def subPos(start: Int, end: Int) =
if (pos == NoPosition) NoPosition
else {
val start1 = pos.start + start
val end1 = pos.start + end
pos withStart start1 withPoint start1 withEnd end1
}
def defineVariables(sym: Symbol) = {
val Trim = "(?s)^[\\s&&[^\n\r]]*(.*?)\\s*$".r
defs(sym) ++= defines.map {
str => {
val start = skipWhitespace(str, "@define".length)
val (key, value) = str.splitAt(skipVariable(str, start))
key.drop(start) -> value
}
} map {
case (key, Trim(value)) => variableName(key) -> value.replaceAll("\\s+\\*+$", "")
case x => throw new MatchError(x)
}
}
}
case class UseCase(comment: DocComment, body: String, pos: Position) {
var defined: List[Symbol] = List() // initialized by Typer
var aliases: List[Symbol] = List() // initialized by Typer
def expandedDefs(sym: Symbol, site: Symbol): List[Symbol] = {
def select(site: Type, name: Name, orElse: => Type): Type = {
val member = site.nonPrivateMember(name)
if (member.isTerm) singleType(site, member)
else if (member.isType) site.memberType(member)
else orElse
}
def getSite(name: Name): Type = {
def findIn(sites: List[Symbol]): Type = sites match {
case site1 :: sites1 => select(site1.thisType, name, findIn(sites1))
case _ => NoType
}
// Previously, searching was taking place *only* in the current package and in the root package
// now we're looking for it everywhere in the hierarchy, so we'll be able to link variable expansions like
// immutable.Seq in package immutable
//val (classes, pkgs) = site.ownerChain.span(!_.isPackageClass)
//val sites = (classes ::: List(pkgs.head, rootMirror.RootClass)))
//findIn(sites)
findIn(site.ownerChain ::: List(rootMirror.EmptyPackage))
}
def getType(str: String, variable: String): Type = {
def getParts(start: Int): List[String] = {
val end = skipIdent(str, start)
if (end == start) List()
else str.substring (start, end) :: {
if (end < str.length && (str charAt end) == '.') getParts(end + 1)
else List()
}
}
val parts = getParts(0)
if (parts.isEmpty) {
reporter.error(comment.codePos, "Incorrect variable expansion for " + variable + " in use case. Does the " +
"variable expand to wiki syntax when documenting " + site + "?")
return ErrorType
}
val partnames = (parts.init map newTermName) :+ newTypeName(parts.last)
val (start, rest) = parts match {
case "this" :: _ => (site.thisType, partnames.tail)
case _ :: "this" :: _ =>
site.ownerChain.find(_.name == partnames.head) match {
case Some(clazz) => (clazz.thisType, partnames drop 2)
case _ => (NoType, Nil)
}
case _ =>
(getSite(partnames.head), partnames.tail)
}
val result = rest.foldLeft(start)(select(_, _, NoType))
if (result == NoType)
runReporting.warning(
comment.codePos,
s"""Could not find the type $variable points to while expanding it for the usecase signature of $sym in $site. In this context, $variable = "$str".""",
WarningCategory.Scaladoc,
site)
result
}
/*
* work around the backticks issue suggested by Simon in
* https://groups.google.com/forum/?hl=en&fromgroups#!topic/scala-internals/z7s1CCRCz74
* ideally, we'd have a removeWikiSyntax method in the CommentFactory to completely eliminate the wiki markup
*/
def cleanupVariable(str: String) = {
val tstr = str.trim
if (tstr.length >= 2 && tstr.startsWith("`") && tstr.endsWith("`"))
tstr.substring(1, tstr.length - 1)
else
tstr
}
// the Boolean tells us whether we can normalize: if we found an actual type, then yes, we can normalize, else no,
// use the synthetic alias created for the variable
val aliasExpansions: List[(Type, Boolean)] =
for (alias <- aliases) yield
lookupVariable(alias.name.toString.substring(1), site) match {
case Some(repl) =>
val repl2 = cleanupVariable(repl)
val tpe = getType(repl2, alias.name.toString)
if (tpe != NoType) (tpe, true)
else {
val alias1 = alias.cloneSymbol(rootMirror.RootClass, alias.rawflags, newTypeName(repl2))
(typeRef(NoPrefix, alias1, Nil), false)
}
case None =>
(typeRef(NoPrefix, alias, Nil), false)
}
@tailrec
def subst(sym: Symbol, from: List[Symbol], to: List[(Type, Boolean)]): (Type, Boolean) =
if (from.isEmpty) (sym.tpe, false)
else if (from.head == sym) to.head
else subst(sym, from.tail, to.tail)
val substAliases = new TypeMap {
def apply(tp: Type) = mapOver(tp) match {
case tp1 @ TypeRef(_, sym, args) if sym.name.length > 1 && sym.name.startChar == '$' =>
subst(sym, aliases, aliasExpansions) match {
case (TypeRef(pre, sym, _), canNormalize) =>
val tpe = typeRef(pre, sym, args)
if (canNormalize) tpe.normalize else tpe
case _ => tp1
}
case tp1 => tp1
}
}
for (defn <- defined) yield {
defn.cloneSymbol(sym.owner, sym.flags | Flags.SYNTHETIC) modifyInfo (info =>
substAliases(info).asSeenFrom(site.thisType, sym.owner)
)
}
}
}
class ExpansionLimitExceeded(str: String) extends Exception(str)
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy