package scala.tools.nsc
package ast
import symtab._
import reporters.Reporter
import util.{Position, NoPosition}
import util.DocStrings._
import util.Chars._
import scala.collection.mutable.{HashMap, ListBuffer, StringBuilder}
trait DocComments { self: SymbolTable =>
def reporter: Reporter
val docComments = new HashMap[Symbol, DocComment]
def docComment(sym: Symbol, docStr: String, pos: Position = NoPosition) =
if ((sym ne null) && (sym ne NoSymbol))
docComments += (sym -> DocComment(docStr, pos))
def rawDocComment(sym: Symbol): String =
docComments get sym map (_.raw) getOrElse ""
def docCommentPos(sym: Symbol): Position =
getDocComment(sym) map (_.pos) getOrElse NoPosition
private def allInheritedOverriddenSymbols(sym: Symbol): List[Symbol] = {
if (!sym.owner.isClass) Nil
else sym.owner.ancestors map (sym overriddenSymbol _) filter (_ != NoSymbol)
}
def cookedDocComment(sym: Symbol, docStr: String = ""): String = {
val ownComment = if (docStr.length == 0) docComments get sym map (_.template) getOrElse ""
else DocComment(docStr).template
superComment(sym) match {
case None =>
ownComment
case Some(sc) =>
if (ownComment == "") sc
else merge(sc, ownComment, sym)
}
}
def expandedDocComment(sym: Symbol, site: Symbol, docStr: String = ""): String = {
val site1 = if ((sym.isModule || sym.isClass) && site.hasPackageFlag) sym
else site
expandVariables(cookedDocComment(sym, docStr), sym, site1)
}
def expandedDocComment(sym: Symbol): String = expandedDocComment(sym, sym.enclClass)
def useCases(sym: Symbol, site: Symbol): List[(Symbol, String, Position)] = {
def getUseCases(dc: DocComment) = {
for (uc <- dc.useCases; defn <- uc.expandedDefs(site)) yield
(defn,
expandVariables(merge(cookedDocComment(sym), uc.comment.raw, defn, copyFirstPara = true), sym, site),
uc.pos)
}
getDocComment(sym) map getUseCases getOrElse List()
}
def useCases(sym: Symbol): List[(Symbol, String, Position)] = useCases(sym, sym.enclClass)
def toJavaDoc(s: String): String = expandWiki(s)
private val wikiReplacements = List(
("""(\n\s*\*?)(\s*\n)""" .r, """$1 <p>$2"""),
("""<([^\w/])""" .r, """<$1"""),
("""([^\w/])>""" .r, """$1>"""),
("""\{\{\{(.*(?:\n.*)*)\}\}\}""".r, """<pre>$1</pre>"""),
("""`([^`]*)`""" .r, """<code>$1</code>"""),
("""__([^_]*)__""" .r, """<u>$1</u>"""),
("""''([^']*)''""" .r, """<i>$1</i>"""),
("""'''([^']*)'''""" .r, """<b>$1</b>"""),
("""\^([^^]*)\^""" .r, """<sup>$1</sup>"""),
(""",,([^,]*),,""" .r, """<sub>$1</sub>"""))
def expandWiki(str: String): String =
(str /: wikiReplacements) { (str1, regexRepl) => regexRepl._1 replaceAllIn(str1, regexRepl._2) }
private def getDocComment(sym: Symbol): Option[DocComment] =
mapFind(sym :: allInheritedOverriddenSymbols(sym))(docComments get _)
protected def superComment(sym: Symbol): Option[String] =
allInheritedOverriddenSymbols(sym).iterator map (x => cookedDocComment(x)) 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")
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 =
(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((start, 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))
if (out.length == 0) dst
else {
out append dst.substring(copied)
out.toString
}
}
private val defs = new HashMap[Symbol, Map[String, String]] {
override def default(key: Symbol) = Map()
}
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 res => res orElse lookupVariable(vble, site.owner)
}
}
protected def expandVariables(initialStr: String, sym: Symbol, site: Symbol): String = {
val expandLimit = 10
def expandInternal(str: String, depth: Int): String = {
if (depth >= expandLimit)
throw new ExpansionLimitExceeded(str)
val out = new StringBuilder
var copied, idx = 0
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) {
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 => reporter.warning(sym.pos, "Variable " + vname + " undefined in comment for " + sym)
}
}
}
}
if (out.length == 0) str
else {
out append str.substring(copied)
expandInternal(out.toString, depth + 1)
}
}
expandInternal(initialStr, 0).replaceAllLiterally("""\$""", "$")
}
case class DocComment(raw: String, pos: Position = NoPosition) {
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)
UseCase(DocComment(comment, commentPos), code, codePos)
}
private def subPos(start: Int, end: Int) =
if (pos == NoPosition) NoPosition
else {
val start1 = pos.start + start
val end1 = pos.end + 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 class UseCase(comment: DocComment, body: String, pos: Position) {
var defined: List[Symbol] = List()
var aliases: List[Symbol] = List()
def expandedDefs(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 List() => NoType
case site :: sites1 => select(site.thisType, name, findIn(sites1))
}
val (classes, pkgs) = site.ownerChain.span(!_.isPackageClass)
findIn(classes ::: List(pkgs.head, definitions.RootClass))
}
def getType(str: 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)
assert(parts.nonEmpty, "parts is empty '" + str + "' in site " + site)
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)
}
(start /: rest)(select(_, _, NoType))
}
val aliasExpansions: List[Type] =
for (alias <- aliases) yield
lookupVariable(alias.name.toString.substring(1), site) match {
case Some(repl) =>
val tpe = getType(repl.trim)
if (tpe != NoType) tpe
else {
val alias1 = alias.cloneSymbol(definitions.RootClass)
alias1.name = repl.toTypeName
typeRef(NoPrefix, alias1, Nil)
}
case None =>
typeRef(NoPrefix, alias, Nil)
}
def subst(sym: Symbol, from: List[Symbol], to: List[Type]): Type =
if (from.isEmpty) sym.tpe
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(pre, sym, args) if (sym.name.length > 1 && sym.name(0) == '$') =>
subst(sym, aliases, aliasExpansions) match {
case TypeRef(pre1, sym1, _) =>
typeRef(pre1, sym1, args)
case _ =>
tp1
}
case tp1 =>
tp1
}
}
for (defn <- defined) yield {
defn.cloneSymbol.setFlag(Flags.SYNTHETIC).setInfo(
substAliases(defn.info).asSeenFrom(site.thisType, defn.owner))
}
}
}
class ExpansionLimitExceeded(str: String) extends Exception
}