From 44ec110bf059a089f54c06469ff2a54275d0f05f Mon Sep 17 00:00:00 2001 From: Vlad Ureche Date: Sat, 16 Jun 2012 18:47:40 +0200 Subject: Scaladoc diff-firendly output Scaladoc can create raw content files that we can easily diff and spot any modifications. There is a cool project by Stefan Zeiger to export the scaladoc model in JSON, but with the language and scaladoc being so quick to evolve, it'll be a pain to properly maintain. In the long-run, the plan is to sample a couple of raw files on each build and email me the diff. If I spot anything that may be wrong I can fix it, revert the commit or at least file a bug. For now, .html.raw files are generated on-demand, using ant -Dscaladoc.raw.output="yes" Also added a script that will do the job of diff-ing. Review by @jsuereth. Conflicts: src/compiler/scala/tools/nsc/doc/Settings.scala --- build.xml | 17 +++++--- src/compiler/scala/tools/ant/Scaladoc.scala | 9 ++++ src/compiler/scala/tools/nsc/doc/Settings.scala | 5 +++ .../scala/tools/nsc/doc/html/HtmlPage.scala | 18 ++++---- src/compiler/scala/tools/nsc/doc/html/Page.scala | 18 +++++++- .../tools/nsc/doc/html/page/IndexScript.scala | 10 +---- tools/scaladoc-compare | 50 ++++++++++++++++++++++ 7 files changed, 102 insertions(+), 25 deletions(-) create mode 100755 tools/scaladoc-compare diff --git a/build.xml b/build.xml index 33ebbfe377..e6b23f268f 100644 --- a/build.xml +++ b/build.xml @@ -2064,6 +2064,9 @@ DOCUMENTATION + + + @@ -2091,7 +2094,7 @@ DOCUMENTATION classpathref="pack.classpath" addparams="${scalac.args.all}" docRootContent="${src.dir}/library/rootdoc.txt" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> @@ -2175,7 +2178,7 @@ DOCUMENTATION srcdir="${src.dir}/compiler" docRootContent="${src.dir}/compiler/rootdoc.txt" addparams="${scalac.args.all}" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> @@ -2197,7 +2200,7 @@ DOCUMENTATION classpathref="pack.classpath" srcdir="${src.dir}/jline/src/main/java" addparams="${scalac.args.all}" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> @@ -2221,7 +2224,7 @@ DOCUMENTATION classpathref="pack.classpath" srcdir="${src.dir}/scalap" addparams="${scalac.args.all}" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> @@ -2243,7 +2246,7 @@ DOCUMENTATION classpathref="pack.classpath" srcdir="${src.dir}/partest" addparams="${scalac.args.all}" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> @@ -2265,7 +2268,7 @@ DOCUMENTATION classpathref="pack.classpath" srcdir="${src.dir}/continuations/plugin" addparams="${scalac.args.all}" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> @@ -2287,7 +2290,7 @@ DOCUMENTATION classpathref="pack.classpath" srcdir="${src.dir}/actors-migration" addparams="${scalac.args.all}" - implicits="on" diagrams="on"> + implicits="on" diagrams="on" rawOutput="${scaladoc.raw.output}"> diff --git a/src/compiler/scala/tools/ant/Scaladoc.scala b/src/compiler/scala/tools/ant/Scaladoc.scala index daa08ef8a7..2cada92c1e 100644 --- a/src/compiler/scala/tools/ant/Scaladoc.scala +++ b/src/compiler/scala/tools/ant/Scaladoc.scala @@ -150,6 +150,9 @@ class Scaladoc extends ScalaMatchingTask { /** Instruct the scaladoc tool to use the binary given to create diagrams */ private var docDiagramsDotPath: Option[String] = None + /** Instruct the scaladoc to produce textual ouput from html pages, for easy diff-ing */ + private var docRawOutput: Boolean = false + /*============================================================================*\ ** Properties setters ** @@ -419,6 +422,11 @@ class Scaladoc extends ScalaMatchingTask { def setDiagramsDotPath(input: String) = docDiagramsDotPath = Some(input) + /** Set the `rawOutput` bit so Scaladoc also outputs text from each html file + * @param input One of the flags `yes/no` or `on/off`. Default if no/off. */ + def setRawOutput(input: String) = + docRawOutput = Flag.getBooleanValue(input, "rawOutput") + /*============================================================================*\ ** Properties getters ** \*============================================================================*/ @@ -616,6 +624,7 @@ class Scaladoc extends ScalaMatchingTask { docSettings.docImplicitsShowAll.value = docImplicitsShowAll docSettings.docDiagrams.value = docDiagrams docSettings.docDiagramsDebug.value = docDiagramsDebug + docSettings.docRawOutput.value = docRawOutput if(!docDiagramsDotPath.isEmpty) docSettings.docDiagramsDotPath.value = docDiagramsDotPath.get if (!docgenerator.isEmpty) docSettings.docgenerator.value = docgenerator.get diff --git a/src/compiler/scala/tools/nsc/doc/Settings.scala b/src/compiler/scala/tools/nsc/doc/Settings.scala index 4458889d55..d7b61bc129 100644 --- a/src/compiler/scala/tools/nsc/doc/Settings.scala +++ b/src/compiler/scala/tools/nsc/doc/Settings.scala @@ -120,6 +120,11 @@ class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { "dot" // by default, just pick up the system-wide dot ) + val docRawOutput = BooleanSetting ( + "-raw-output", + "For each html file, create another .html.raw file containing only the text. (can be used for quickly diffing two scaladoc outputs)" + ) + // Somewhere slightly before r18708 scaladoc stopped building unless the // self-type check was suppressed. I hijacked the slotted-for-removal-anyway // suppress-vt-warnings option and renamed it for this purpose. diff --git a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala index e3da8bddea..4c9215f923 100644 --- a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala +++ b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala @@ -13,7 +13,7 @@ import comment._ import xml.{XML, NodeSeq} import xml.dtd.{DocType, PublicID} import scala.collection._ -import java.nio.channels.Channels +import java.io.Writer /** An html page that is part of a Scaladoc site. * @author David Bernard @@ -52,17 +52,19 @@ abstract class HtmlPage extends Page { thisPage => { body } - val fos = createFileOutputStream(site) - val w = Channels.newWriter(fos.getChannel, site.encoding) - try { + + writeFile(site) { (w: Writer) => w.write("\n") w.write(doctype.toString + "\n") w.write(xml.Xhtml.toXhtml(html)) } - finally { - w.close() - fos.close() - } + + if (site.universe.settings.docRawOutput.value) + writeFile(site, ".raw") { + // we're only interested in the body, as this will go into the diff + _.write(body.text) + } + //XML.save(pageFile.getPath, html, site.encoding, xmlDecl = false, doctype = doctype) } diff --git a/src/compiler/scala/tools/nsc/doc/html/Page.scala b/src/compiler/scala/tools/nsc/doc/html/Page.scala index c5bf3e0e37..72b62dd482 100644 --- a/src/compiler/scala/tools/nsc/doc/html/Page.scala +++ b/src/compiler/scala/tools/nsc/doc/html/Page.scala @@ -8,6 +8,8 @@ package scala.tools.nsc.doc.html import scala.tools.nsc.doc.model._ import java.io.{FileOutputStream, File} import scala.reflect.NameTransformer +import java.nio.channels.Channels +import java.io.Writer abstract class Page { thisPage => @@ -20,8 +22,8 @@ abstract class Page { def absoluteLinkTo(path: List[String]) = path.reverse.mkString("/") - def createFileOutputStream(site: HtmlFactory) = { - val file = new File(site.siteRoot, absoluteLinkTo(thisPage.path)) + def createFileOutputStream(site: HtmlFactory, suffix: String = "") = { + val file = new File(site.siteRoot, absoluteLinkTo(thisPage.path) + suffix) val folder = file.getParentFile if (! folder.exists) { folder.mkdirs @@ -29,6 +31,18 @@ abstract class Page { new FileOutputStream(file.getPath) } + def writeFile(site: HtmlFactory, suffix: String = "")(fn: Writer => Unit) = { + val fos = createFileOutputStream(site, suffix) + val w = Channels.newWriter(fos.getChannel, site.encoding) + try { + fn(w) + } + finally { + w.close() + fos.close() + } + } + /** Writes this page as a file. The file's location is relative to the * generator's site root, and the encoding is also defined by the generator. * @param generator The generator that is writing this page. */ diff --git a/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala b/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala index 7edd4937c4..c5fe4da17a 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala @@ -15,14 +15,8 @@ class IndexScript(universe: doc.Universe, index: doc.Index) extends Page { def path = List("index.js") override def writeFor(site: HtmlFactory) { - val stream = createFileOutputStream(site) - val writer = Channels.newWriter(stream.getChannel, site.encoding) - try { - writer.write("Index.PACKAGES = " + packages.toString() + ";") - } - finally { - writer.close - stream.close + writeFile(site) { + _.write("Index.PACKAGES = " + packages.toString() + ";") } } diff --git a/tools/scaladoc-compare b/tools/scaladoc-compare new file mode 100755 index 0000000000..74fbfd1dd4 --- /dev/null +++ b/tools/scaladoc-compare @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Script to compare scaladoc raw files. For an explanation read the next echos. +# + +if [ $# -ne 2 ] +then + echo + echo "scaladoc-compare will compare the scaladoc-generated pages in two different locations and output the diff" + echo "it's main purpose is to track changes to scaladoc and prevent updates that break things." + echo + echo "This script is meant to be used with the scaladoc -raw-output option, as it compares .html.raw files " + echo "instead of markup-heavy .html files." + echo + echo "Script usage $0 " + echo " eg: $0 build/scaladoc/library build/scaladoc-prev/library | less" + echo + exit 1 +fi + +NEW_PATH=$1 +OLD_PATH=$2 + +FILES=`find $NEW_PATH -name '*.html.raw'` +if [ "$FILES" == "" ] +then + echo "No .html.raw files found in $NEW_PATH!" + exit 1 +fi + +for NEW_FILE in $FILES +do + OLD_FILE=${NEW_FILE/$NEW_PATH/$OLD_PATH} + if [ -f $OLD_FILE ] + then + #echo $NEW_FILE" => "$OLD_FILE + DIFF=`diff -q -w $NEW_FILE $OLD_FILE 2>&1` + if [ "$DIFF" != "" ] + then + # Redo the full diff + echo "$NEW_FILE:" + diff -w $NEW_FILE $OLD_FILE 2>&1 + echo -e "\n\n" + fi + else + echo -e "$NEW_FILE: No corresponding file (expecting $OLD_FILE)\n\n" + fi +done + +echo Done. -- cgit v1.2.3 From c11427c13e85ba6fb210c1f05724c21c8aeb4be3 Mon Sep 17 00:00:00 2001 From: Vlad Ureche Date: Mon, 25 Jun 2012 16:53:29 +0200 Subject: Reorganized scaladoc model Since the old model was "interruptible", it was prone to something similar to race conditions -- where the model was creating a template, that template creating was interrupted to creat another template, and so on until a cycle was hit -- then, the loop would be broken by returning the originally not-yet-finished template. Now everything happens in a depth-first order, starting from root, traversing packages and classes all the way to members. The previously interrupting operations are now grouped in two categories: - those that were meant to add entities, like inheriting a class from a template to the other (e.g. trait T { class C }; trait U extends T) => those were moved right after the core model creation - those that were meant to do lookups - like finding the companion object -- those were moved after the model creation and inheritance and are not allowed to create new documentable templates. Now, for the documentable templates we have: DocTemplateImpl - the main documentable template, it represents a Scala template (class, trait, object or package). It may only be created when modelFinished=false by methods in the modelCreation object NoDocTemplateMemberImpl - a non-documented (source not present) template that was inherited. May be used as a member, but does not get its own page NoDocTemplateImpl - a non-documented (source not present) template that may not be used as a member and does not get its own page For model users: you can use anything in the ModelFactory trait at will, but not from the modelCreation object -- that is reserved for the core model creation and using those functions may lead to duplicate templates, invalid links and other ugly problems. --- src/compiler/scala/tools/nsc/doc/DocFactory.scala | 4 +- src/compiler/scala/tools/nsc/doc/Settings.scala | 19 +- src/compiler/scala/tools/nsc/doc/html/Page.scala | 2 +- .../scala/tools/nsc/doc/html/page/Template.scala | 36 +- .../scala/tools/nsc/doc/model/Entity.scala | 53 +- .../scala/tools/nsc/doc/model/ModelFactory.scala | 567 +++++++++++++-------- .../doc/model/ModelFactoryImplicitSupport.scala | 46 +- .../scala/tools/nsc/doc/model/TreeFactory.scala | 2 +- .../nsc/doc/model/comment/CommentFactory.scala | 8 +- test/scaladoc/run/SI-5373.scala | 4 +- test/scaladoc/run/package-object.check | 3 +- test/scaladoc/run/package-object.scala | 3 +- 12 files changed, 466 insertions(+), 281 deletions(-) diff --git a/src/compiler/scala/tools/nsc/doc/DocFactory.scala b/src/compiler/scala/tools/nsc/doc/DocFactory.scala index e2e1ddf065..37e3b626e8 100644 --- a/src/compiler/scala/tools/nsc/doc/DocFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/DocFactory.scala @@ -83,8 +83,8 @@ class DocFactory(val reporter: Reporter, val settings: doc.Settings) { processor with model.ModelFactoryImplicitSupport with model.comment.CommentFactory with model.TreeFactory { - override def templateShouldDocument(sym: compiler.Symbol) = - extraTemplatesToDocument(sym) || super.templateShouldDocument(sym) + override def templateShouldDocument(sym: compiler.Symbol, inTpl: TemplateImpl) = + extraTemplatesToDocument(sym) || super.templateShouldDocument(sym, inTpl) } ) diff --git a/src/compiler/scala/tools/nsc/doc/Settings.scala b/src/compiler/scala/tools/nsc/doc/Settings.scala index d7b61bc129..1cc1f98fba 100644 --- a/src/compiler/scala/tools/nsc/doc/Settings.scala +++ b/src/compiler/scala/tools/nsc/doc/Settings.scala @@ -155,15 +155,16 @@ class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { * the function result should be a humanly-understandable description of the type class */ val knownTypeClasses: Map[String, String => String] = Map() + - (".scala.package.Numeric" -> ((tparam: String) => tparam + " is a numeric class, such as Int, Long, Float or Double")) + - (".scala.package.Integral" -> ((tparam: String) => tparam + " is an integral numeric class, such as Int or Long")) + - (".scala.package.Fractional" -> ((tparam: String) => tparam + " is a fractional numeric class, such as Float or Double")) + - (".scala.reflect.Manifest" -> ((tparam: String) => tparam + " is accompanied by a Manifest, which is a runtime representation of its type that survives erasure")) + - (".scala.reflect.ClassManifest" -> ((tparam: String) => tparam + " is accompanied by a ClassManifest, which is a runtime representation of its type that survives erasure")) + - (".scala.reflect.OptManifest" -> ((tparam: String) => tparam + " is accompanied by an OptManifest, which can be either a runtime representation of its type or the NoManifest, which means the runtime type is not available")) + - (".scala.reflect.ClassTag" -> ((tparam: String) => tparam + " is accompanied by a ClassTag, which is a runtime representation of its type that survives erasure")) + - (".scala.reflect.AbsTypeTag" -> ((tparam: String) => tparam + " is accompanied by an AbsTypeTag, which is a runtime representation of its type that survives erasure")) + - (".scala.reflect.TypeTag" -> ((tparam: String) => tparam + " is accompanied by a TypeTag, which is a runtime representation of its type that survives erasure")) + // TODO: Bring up to date and test these + ("scala.package.Numeric" -> ((tparam: String) => tparam + " is a numeric class, such as Int, Long, Float or Double")) + + ("scala.package.Integral" -> ((tparam: String) => tparam + " is an integral numeric class, such as Int or Long")) + + ("scala.package.Fractional" -> ((tparam: String) => tparam + " is a fractional numeric class, such as Float or Double")) + + ("scala.reflect.Manifest" -> ((tparam: String) => tparam + " is accompanied by a Manifest, which is a runtime representation of its type that survives erasure")) + + ("scala.reflect.ClassManifest" -> ((tparam: String) => tparam + " is accompanied by a ClassManifest, which is a runtime representation of its type that survives erasure")) + + ("scala.reflect.OptManifest" -> ((tparam: String) => tparam + " is accompanied by an OptManifest, which can be either a runtime representation of its type or the NoManifest, which means the runtime type is not available")) + + ("scala.reflect.ClassTag" -> ((tparam: String) => tparam + " is accompanied by a ClassTag, which is a runtime representation of its type that survives erasure")) + + ("scala.reflect.AbsTypeTag" -> ((tparam: String) => tparam + " is accompanied by an AbsTypeTag, which is a runtime representation of its type that survives erasure")) + + ("scala.reflect.TypeTag" -> ((tparam: String) => tparam + " is accompanied by a TypeTag, which is a runtime representation of its type that survives erasure")) /** * Set of classes to exclude from index and diagrams diff --git a/src/compiler/scala/tools/nsc/doc/html/Page.scala b/src/compiler/scala/tools/nsc/doc/html/Page.scala index 72b62dd482..40ae65a37a 100644 --- a/src/compiler/scala/tools/nsc/doc/html/Page.scala +++ b/src/compiler/scala/tools/nsc/doc/html/Page.scala @@ -58,7 +58,7 @@ abstract class Page { def templateToPath(tpl: TemplateEntity): List[String] = { def doName(tpl: TemplateEntity): String = - NameTransformer.encode(tpl.name) + (if (tpl.isObject) "$" else "") + (if (tpl.inPackageObject) "package$$" else "") + NameTransformer.encode(tpl.name) + (if (tpl.isObject) "$" else "") def downPacks(pack: Package): List[String] = if (pack.isRootPackage) Nil else (doName(pack) :: downPacks(pack.inTemplate)) def downInner(nme: String, tpl: TemplateEntity): (String, Package) = { diff --git a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala index 66189a6854..975ff8fb89 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala @@ -84,7 +84,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage { signature(tpl, true) } - { memberToCommentHtml(tpl, true) } + { memberToCommentHtml(tpl, tpl.inTemplate, true) }
@@ -96,7 +96,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage } { if (tpl.linearizationTemplates.isEmpty && tpl.conversions.isEmpty) NodeSeq.Empty else { - if (!tpl.linearization.isEmpty) + if (!tpl.linearizationTemplates.isEmpty)
Inherited
@@ -138,35 +138,35 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage { if (constructors.isEmpty) NodeSeq.Empty else

Instance Constructors

-
    { constructors map (memberToHtml(_)) }
+
    { constructors map (memberToHtml(_, tpl)) }
} { if (typeMembers.isEmpty) NodeSeq.Empty else

Type Members

-
    { typeMembers map (memberToHtml(_)) }
+
    { typeMembers map (memberToHtml(_, tpl)) }
} { if (absValueMembers.isEmpty) NodeSeq.Empty else

Abstract Value Members

-
    { absValueMembers map (memberToHtml(_)) }
+
    { absValueMembers map (memberToHtml(_, tpl)) }
} { if (concValueMembers.isEmpty) NodeSeq.Empty else

{ if (absValueMembers.isEmpty) "Value Members" else "Concrete Value Members" }

-
    { concValueMembers map (memberToHtml(_)) }
+
    { concValueMembers map (memberToHtml(_, tpl)) }
} { if (deprValueMembers.isEmpty) NodeSeq.Empty else

Deprecated Value Members

-
    { deprValueMembers map (memberToHtml(_)) }
+
    { deprValueMembers map (memberToHtml(_, tpl)) }
}
@@ -237,12 +237,12 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage tparamsToString(d.typeParams) + paramLists.mkString } - def memberToHtml(mbr: MemberEntity): NodeSeq = { + def memberToHtml(mbr: MemberEntity, inTpl: DocTemplateEntity): NodeSeq = { val defParamsString = mbr match { case d:MemberEntity with Def => defParamsToString(d) case _ => "" } - val memberComment = memberToCommentHtml(mbr, false) + val memberComment = memberToCommentHtml(mbr, inTpl, false)
  • @@ -251,24 +251,24 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage
  • } - def memberToCommentHtml(mbr: MemberEntity, isSelf: Boolean): NodeSeq = { + def memberToCommentHtml(mbr: MemberEntity, inTpl: DocTemplateEntity, isSelf: Boolean): NodeSeq = { mbr match { case dte: DocTemplateEntity if isSelf => // comment of class itself -
    { memberToCommentBodyHtml(mbr, isSelf = true) }
    +
    { memberToCommentBodyHtml(mbr, inTpl, isSelf = true) }
    case dte: DocTemplateEntity if mbr.comment.isDefined => // comment of inner, documented class (only short comment, full comment is on the class' own page) memberToInlineCommentHtml(mbr, isSelf) case _ => // comment of non-class member or non-documentented inner class - val commentBody = memberToCommentBodyHtml(mbr, isSelf = false) + val commentBody = memberToCommentBodyHtml(mbr, inTpl, isSelf = false) if (commentBody.isEmpty) NodeSeq.Empty else { val shortComment = memberToShortCommentHtml(mbr, isSelf) - val longComment = memberToUseCaseCommentHtml(mbr, isSelf) ++ memberToCommentBodyHtml(mbr, isSelf) + val longComment = memberToUseCaseCommentHtml(mbr, isSelf) ++ memberToCommentBodyHtml(mbr, inTpl, isSelf) val includedLongComment = if (shortComment.text.trim == longComment.text.trim) NodeSeq.Empty @@ -298,7 +298,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage def memberToInlineCommentHtml(mbr: MemberEntity, isSelf: Boolean): NodeSeq =

    { inlineToHtml(mbr.comment.get.short) }

    - def memberToCommentBodyHtml(mbr: MemberEntity, isSelf: Boolean, isReduced: Boolean = false): NodeSeq = { + def memberToCommentBodyHtml(mbr: MemberEntity, inTpl: DocTemplateEntity, isSelf: Boolean, isReduced: Boolean = false): NodeSeq = { val memberComment = if (mbr.comment.isEmpty) NodeSeq.Empty @@ -383,7 +383,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage }
    - This member is added by an implicit conversion from { typeToHtml(mbr.inTemplate.resultType, true) } to + This member is added by an implicit conversion from { typeToHtml(inTpl.resultType, true) } to { targetType } performed by method { conversionMethod } in { conversionOwner }. { constraintText }
    @@ -404,7 +404,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage val definitionClasses: Seq[scala.xml.Node] = { val inDefTpls = mbr.inDefinitionTemplates - if ((inDefTpls.tail.isEmpty && (inDefTpls.head == mbr.inTemplate)) || isReduced) NodeSeq.Empty + if ((inDefTpls.tail.isEmpty && (inDefTpls.head == inTpl)) || isReduced) NodeSeq.Empty else {
    Definition Classes
    { templatesToHtml(inDefTpls, xml.Text(" → ")) }
    @@ -650,7 +650,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage
    {nameHtml} else nameHtml }{ - def tparamsToHtml(mbr: Entity): NodeSeq = mbr match { + def tparamsToHtml(mbr: Any): NodeSeq = mbr match { case hk: HigherKinded => val tpss = hk.typeParams if (tpss.isEmpty) NodeSeq.Empty else { @@ -662,7 +662,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage } [{ tparams0(tpss) }] } - case _ => NodeSeq.Empty + case _ => NodeSeq.Empty } tparamsToHtml(mbr) }{ diff --git a/src/compiler/scala/tools/nsc/doc/model/Entity.scala b/src/compiler/scala/tools/nsc/doc/model/Entity.scala index 6488847049..8c6fa53fbc 100644 --- a/src/compiler/scala/tools/nsc/doc/model/Entity.scala +++ b/src/compiler/scala/tools/nsc/doc/model/Entity.scala @@ -24,6 +24,9 @@ import comment._ * - annotations. */ trait Entity { + /** Similar to symbols, so we can track entities */ + def id: Int + /** The name of the entity. Note that the name does not qualify this entity uniquely; use its `qualifiedName` * instead. */ def name : String @@ -48,6 +51,8 @@ trait Entity { /** The annotations attached to this entity, if any. */ def annotations: List[Annotation] + /** The kind of the entity */ + def kind: String } object Entity { @@ -86,9 +91,11 @@ trait TemplateEntity extends Entity { /** Whether this template is a case class. */ def isCaseClass: Boolean + /** Whether or not the template was defined in a package object */ + def inPackageObject: Boolean + /** The self-type of this template, if it differs from the template type. */ def selfType : Option[TypeEntity] - } @@ -177,7 +184,7 @@ object MemberEntity { } /** An entity that is parameterized by types */ -trait HigherKinded extends Entity { +trait HigherKinded { /** The type parameters of this entity. */ def typeParams: List[TypeParam] @@ -187,8 +194,14 @@ trait HigherKinded extends Entity { /** A template (class, trait, object or package) which is referenced in the universe, but for which no further * documentation is available. Only templates for which a source file is given are documented by Scaladoc. */ -trait NoDocTemplate extends TemplateEntity +trait NoDocTemplate extends TemplateEntity { + def kind = "" +} +/** TODO: Document */ +trait NoDocTemplateMemberEntity extends TemplateEntity with MemberEntity { + def kind = "" +} /** A template (class, trait, object or package) for which documentation is available. Only templates for which * a source file is given are documented by Scaladoc. */ @@ -209,9 +222,6 @@ trait DocTemplateEntity extends TemplateEntity with MemberEntity { /** The direct super-type of this template. */ def parentType: Option[TypeEntity] - @deprecated("Use `linearizationTemplates` and `linearizationTypes` instead", "2.9.0") - def linearization: List[(TemplateEntity, TypeEntity)] - /** All class, trait and object templates which are part of this template's linearization, in lineratization order. * This template's linearization contains all of its direct and indirect super-classes and super-traits. */ def linearizationTemplates: List[TemplateEntity] @@ -254,7 +264,9 @@ trait DocTemplateEntity extends TemplateEntity with MemberEntity { /** A trait template. */ -trait Trait extends DocTemplateEntity with HigherKinded +trait Trait extends DocTemplateEntity with HigherKinded { + def kind = "trait" +} /** A class template. */ @@ -270,11 +282,14 @@ trait Class extends Trait with HigherKinded { * parameters cannot be curried, the outer list has exactly one element. */ def valueParams: List[List[ValueParam]] + override def kind = "class" } /** An object template. */ -trait Object extends DocTemplateEntity +trait Object extends DocTemplateEntity { + def kind = "object" +} /** A package template. A package is in the universe if it is declared as a package object, or if it @@ -290,6 +305,8 @@ trait Package extends Object { /** All packages that are member of this package. */ def packages: List[Package] + + override def kind = "package" } @@ -323,6 +340,7 @@ trait Def extends NonTemplateMemberEntity with HigherKinded { * Each parameter block is a list of value parameters. */ def valueParams : List[List[ValueParam]] + def kind = "method" } @@ -337,11 +355,14 @@ trait Constructor extends NonTemplateMemberEntity { * element. */ def valueParams : List[List[ValueParam]] + def kind = "constructor" } /** A value (`val`), lazy val (`lazy val`) or variable (`var`) of a template. */ -trait Val extends NonTemplateMemberEntity +trait Val extends NonTemplateMemberEntity { + def kind = "[lazy] value/variable" +} /** An abstract type member of a template. */ @@ -353,6 +374,7 @@ trait AbstractType extends NonTemplateMemberEntity with HigherKinded { /** The upper bound for this abstract type, if it has been defined. */ def hi: Option[TypeEntity] + def kind = "abstract type" } @@ -362,18 +384,14 @@ trait AliasType extends NonTemplateMemberEntity with HigherKinded { /** The type aliased by this type alias. */ def alias: TypeEntity + def kind = "type alias" } /** A parameter to an entity. */ -trait ParameterEntity extends Entity { - - /** Whether this parameter is a type parameter. */ - def isTypeParam: Boolean - - /** Whether this parameter is a value parameter. */ - def isValueParam: Boolean +trait ParameterEntity { + def name: String } @@ -388,7 +406,6 @@ trait TypeParam extends ParameterEntity with HigherKinded { /** The upper bound for this type parameter, if it has been defined. */ def hi: Option[TypeEntity] - } @@ -403,7 +420,6 @@ trait ValueParam extends ParameterEntity { /** Whether this value parameter is implicit. */ def isImplicit: Boolean - } @@ -416,6 +432,7 @@ trait Annotation extends Entity { /** The arguments passed to the constructor of the annotation class. */ def arguments: List[ValueArgument] + def kind = "annotation" } /** A trait that signals the member results from an implicit conversion */ diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala index 3dd77d47da..9a8df1fd0e 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala @@ -21,10 +21,9 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { import global._ import definitions.{ ObjectClass, NothingClass, AnyClass, AnyValClass, AnyRefClass } - import rootMirror.{ RootPackage, EmptyPackage } + import rootMirror.{ RootPackage, RootClass, EmptyPackage } - private var droppedPackages = 0 - def templatesCount = templatesCache.size - droppedPackages + def templatesCount = docTemplatesCache.count(_._2.isDocTemplate) - droppedPackages.size private var modelFinished = false private var universe: Universe = null @@ -45,44 +44,49 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { private lazy val noSubclassCache = Set[Symbol](AnyClass, AnyRefClass, ObjectClass) - /** */ def makeModel: Option[Universe] = { val universe = new Universe { thisUniverse => thisFactory.universe = thisUniverse val settings = thisFactory.settings - private val rootPackageMaybe = makeRootPackage - val rootPackage = rootPackageMaybe.orNull + val rootPackage = modelCreation.createRootPackage } modelFinished = true + // complete the links between model entities, everthing that couldn't have been done before + universe.rootPackage.completeModel + Some(universe) filter (_.rootPackage != null) } - /** */ - protected val templatesCache = - new mutable.LinkedHashMap[Symbol, DocTemplateImpl] - - def findTemplate(query: String): Option[DocTemplateImpl] = { - if (!modelFinished) sys.error("cannot find template in unfinished universe") - templatesCache.values find { tpl => tpl.qualifiedName == query && !tpl.isObject } - } + // state: + var ids = 0 + private val droppedPackages = mutable.Set[PackageImpl]() + protected val docTemplatesCache = new mutable.LinkedHashMap[Symbol, DocTemplateImpl] + protected val noDocTemplatesCache = new mutable.LinkedHashMap[Symbol, NoDocTemplateImpl] + protected var typeCache = new mutable.LinkedHashMap[Type, TypeEntity] def optimize(str: String): String = if (str.length < 16) str.intern else str /* ============== IMPLEMENTATION PROVIDING ENTITY TYPES ============== */ - abstract class EntityImpl(val sym: Symbol, inTpl: => TemplateImpl) extends Entity { + abstract class EntityImpl(val sym: Symbol, val inTpl: TemplateImpl) extends Entity { + val id = { ids += 1; ids } val name = optimize(sym.nameString) + val universe = thisFactory.universe + + // Debugging: + // assert(id != 36, sym + " " + sym.getClass) + //println("Creating entity #" + id + " [" + kind + " " + qualifiedName + "] for sym " + sym.kindString + " " + sym.ownerChain.reverse.map(_.name).mkString(".")) + def inTemplate: TemplateImpl = inTpl def toRoot: List[EntityImpl] = this :: inTpl.toRoot def qualifiedName = name - val universe = thisFactory.universe def annotations = sym.annotations.map(makeAnnotation) } trait TemplateImpl extends EntityImpl with TemplateEntity { override def qualifiedName: String = - if (inTemplate.isRootPackage) name else optimize(inTemplate.qualifiedName + "." + name) + if (inTemplate == null || inTemplate.isRootPackage) name else optimize(inTemplate.qualifiedName + "." + name) def isPackage = sym.isPackage def isTrait = sym.isTrait def isClass = sym.isClass && !sym.isTrait @@ -90,15 +94,11 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def isCaseClass = sym.isCaseClass def isRootPackage = false def selfType = if (sym.thisSym eq sym) None else Some(makeType(sym.thisSym.typeOfThis, this)) + def inPackageObject: Boolean = sym.owner.isModuleClass && sym.owner.sourceModule.isPackageObject } - class NoDocTemplateImpl(sym: Symbol, inTpl: => TemplateImpl) extends EntityImpl(sym, inTpl) with TemplateImpl with NoDocTemplate { - def isDocTemplate = false - } - - abstract class MemberImpl(sym: Symbol, implConv: ImplicitConversionImpl = null, inTpl: => DocTemplateImpl) extends EntityImpl(sym, inTpl) with MemberEntity { - lazy val comment = - if (inTpl == null) None else thisFactory.comment(sym, inTpl) + abstract class MemberImpl(sym: Symbol, implConv: ImplicitConversionImpl, inTpl: DocTemplateImpl) extends EntityImpl(sym, inTpl) with MemberEntity { + lazy val comment = if (inTpl != null) thisFactory.comment(sym, inTpl) else None override def inTemplate = inTpl override def toRoot: List[MemberImpl] = this :: inTpl.toRoot def inDefinitionTemplates = this match { @@ -106,9 +106,9 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { mb.useCaseOf.get.inDefinitionTemplates case _ => if (inTpl == null) - makeRootPackage.toList + List(makeRootPackage) else - makeTemplate(sym.owner) :: (sym.allOverriddenSymbols map { inhSym => makeTemplate(inhSym.owner) }) + makeTemplate(sym.owner)::(sym.allOverriddenSymbols map { inhSym => makeTemplate(inhSym.owner) }) } def visibility = { if (sym.isPrivateLocal) PrivateInInstance() @@ -189,16 +189,40 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def byConversion = if (implConv ne null) Some(implConv) else None } + /** A template that is not documented at all. The class is instantiated during lookups, to indicate that the class + * exists, but should not be documented (either it's not included in the source or it's not visible) + */ + class NoDocTemplateImpl(sym: Symbol, inTpl: TemplateImpl) extends EntityImpl(sym, inTpl) with TemplateImpl with HigherKindedImpl with NoDocTemplate { + assert(modelFinished) + assert(!(noDocTemplatesCache isDefinedAt sym)) + noDocTemplatesCache += (sym -> this) + + def isDocTemplate = false + } + + /** An inherited template that was not documented in its original owner - example: + * in classpath: trait T { class C } -- T (and implicitly C) are not documented + * in the source: trait U extends T -- C appears in U as a NoDocTemplateMemberImpl -- that is, U has a member for it + * but C doesn't get its own page + */ + class NoDocTemplateMemberImpl(sym: Symbol, inTpl: DocTemplateImpl) extends MemberImpl(sym, null, inTpl) with TemplateImpl with HigherKindedImpl with NoDocTemplateMemberEntity { + assert(modelFinished) + + def isDocTemplate = false + lazy val definitionName = optimize(inDefinitionTemplates.head.qualifiedName + "." + name) + } + /** The instantiation of `TemplateImpl` triggers the creation of the following entities: * All ancestors of the template and all non-package members. */ - abstract class DocTemplateImpl(sym: Symbol, inTpl: => DocTemplateImpl) extends MemberImpl(sym, null, inTpl) with TemplateImpl with HigherKindedImpl with DocTemplateEntity { - //if (inTpl != null) println("mbr " + sym + " in " + (inTpl.toRoot map (_.sym)).mkString(" > ")) + abstract class DocTemplateImpl(sym: Symbol, inTpl: DocTemplateImpl) extends MemberImpl(sym, null, inTpl) with TemplateImpl with HigherKindedImpl with DocTemplateEntity { + assert(!modelFinished) + assert(!(docTemplatesCache isDefinedAt sym), sym) + docTemplatesCache += (sym -> this) + if (settings.verbose.value) inform("Creating doc template for " + sym) - templatesCache += (sym -> this) - lazy val definitionName = optimize(inDefinitionTemplates.head.qualifiedName + "." + name) override def toRoot: List[DocTemplateImpl] = this :: inTpl.toRoot def inSource = if (sym.sourceFile != null && ! sym.isSynthetic) @@ -244,7 +268,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } } - val linearization = linearizationFromSymbol(sym) + lazy val linearization = linearizationFromSymbol(sym) def linearizationTemplates = linearization map { _._1 } def linearizationTypes = linearization map { _._2 } @@ -258,45 +282,65 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } def subClasses = if (subClassesCache == null) Nil else subClassesCache.toList - val conversions = if (settings.docImplicits.value) makeImplicitConversions(sym, this) else Nil + val conversions: List[ImplicitConversionImpl] = + if (settings.docImplicits.value) makeImplicitConversions(sym, this) else Nil + + lazy val memberSyms = sym.info.members.filter(s => membersShouldDocument(s, this)) + + var memberSymsLazy = memberSyms.filter(t => templateShouldDocument(t, this) && !inOriginalOnwer(t, this)) + var memberSymsEager = memberSyms.filter(!memberSymsLazy.contains(_)) + + var members: List[MemberImpl] = (memberSymsEager.flatMap(makeMember(_, null, this))) ::: + (conversions.flatMap((_.members))) // also take in the members from implicit conversions + + def templates = members collect { case c: DocTemplateEntity => c } + def methods = members collect { case d: Def => d } + def values = members collect { case v: Val => v } + def abstractTypes = members collect { case t: AbstractType => t } + def aliasTypes = members collect { case t: AliasType => t } + + def completeModel: Unit = { + for (member <- members) + member match { + case d: DocTemplateImpl => d.completeModel + case _ => + } - lazy val memberSyms = - // Only this class's constructors are part of its members, inherited constructors are not. - sym.info.members.filter(s => localShouldDocument(s) && (!s.isConstructor || s.owner == sym) && !isPureBridge(sym) ) + members :::= memberSymsLazy.map(modelCreation.createLazyTemplateMember(_, inTpl)) - val members = (memberSyms.flatMap(makeMember(_, null, this))) ::: - (conversions.flatMap((_.members))) // also take in the members from implicit conversions + // compute linearization to register subclasses + linearization + } - val templates = members collect { case c: DocTemplateEntity => c } - val methods = members collect { case d: Def => d } - val values = members collect { case v: Val => v } - val abstractTypes = members collect { case t: AbstractType => t } - val aliasTypes = members collect { case t: AliasType => t } override def isTemplate = true + lazy val definitionName = optimize(inDefinitionTemplates.head.qualifiedName + "." + name) def isDocTemplate = true def companion = sym.companionSymbol match { case NoSymbol => None case comSym if !isEmptyJavaObject(comSym) && (comSym.isClass || comSym.isModule) => - Some(makeDocTemplate(comSym, inTpl)) + makeTemplate(comSym) match { + case d: DocTemplateImpl => Some(d) + case _ => None + } case _ => None } } - abstract class PackageImpl(sym: Symbol, inTpl: => PackageImpl) extends DocTemplateImpl(sym, inTpl) with Package { + abstract class PackageImpl(sym: Symbol, inTpl: PackageImpl) extends DocTemplateImpl(sym, inTpl) with Package { override def inTemplate = inTpl override def toRoot: List[PackageImpl] = this :: inTpl.toRoot - override val linearization = { + override lazy val linearization = { val symbol = sym.info.members.find { s => s.isPackageObject } getOrElse sym linearizationFromSymbol(symbol) } - val packages = members collect { case p: Package => p } + def packages = members collect { case p: PackageImpl if !(droppedPackages contains p) => p } } abstract class RootPackageImpl(sym: Symbol) extends PackageImpl(sym, null) with RootPackageEntity - abstract class NonTemplateMemberImpl(sym: Symbol, implConv: ImplicitConversionImpl, inTpl: => DocTemplateImpl) extends MemberImpl(sym, implConv, inTpl) with NonTemplateMemberEntity { + abstract class NonTemplateMemberImpl(sym: Symbol, implConv: ImplicitConversionImpl, inTpl: DocTemplateImpl) extends MemberImpl(sym, implConv, inTpl) with NonTemplateMemberEntity { override def qualifiedName = optimize(inTemplate.qualifiedName + "#" + name) lazy val definitionName = if (implConv == null) optimize(inDefinitionTemplates.head.qualifiedName + "#" + name) @@ -305,7 +349,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def isBridge = sym.isBridge } - abstract class NonTemplateParamMemberImpl(sym: Symbol, implConv: ImplicitConversionImpl, inTpl: => DocTemplateImpl) extends NonTemplateMemberImpl(sym, implConv, inTpl) { + abstract class NonTemplateParamMemberImpl(sym: Symbol, implConv: ImplicitConversionImpl, inTpl: DocTemplateImpl) extends NonTemplateMemberImpl(sym, implConv, inTpl) { def valueParams = { val info = if (implConv eq null) sym.info else implConv.toType memberInfo sym info.paramss map { ps => (ps.zipWithIndex) map { case (p, i) => @@ -314,26 +358,30 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } } - abstract class ParameterImpl(sym: Symbol, inTpl: => TemplateImpl) extends EntityImpl(sym, inTpl) with ParameterEntity { - override def inTemplate = inTpl + abstract class ParameterImpl(val sym: Symbol, val inTpl: TemplateImpl) extends ParameterEntity { + val name = optimize(sym.nameString) } - private trait TypeBoundsImpl extends EntityImpl { + private trait TypeBoundsImpl { + def sym: Symbol + def inTpl: TemplateImpl def lo = sym.info.bounds match { case TypeBounds(lo, hi) if lo.typeSymbol != NothingClass => - Some(makeTypeInTemplateContext(appliedType(lo, sym.info.typeParams map {_.tpe}), inTemplate, sym)) + Some(makeTypeInTemplateContext(appliedType(lo, sym.info.typeParams map {_.tpe}), inTpl, sym)) case _ => None } def hi = sym.info.bounds match { case TypeBounds(lo, hi) if hi.typeSymbol != AnyClass => - Some(makeTypeInTemplateContext(appliedType(hi, sym.info.typeParams map {_.tpe}), inTemplate, sym)) + Some(makeTypeInTemplateContext(appliedType(hi, sym.info.typeParams map {_.tpe}), inTpl, sym)) case _ => None } } - trait HigherKindedImpl extends EntityImpl with HigherKinded { + trait HigherKindedImpl extends HigherKinded { + def sym: Symbol + def inTpl: TemplateImpl def typeParams = - sym.typeParams map (makeTypeParam(_, inTemplate)) + sym.typeParams map (makeTypeParam(_, inTpl)) } /* ============== MAKER METHODS ============== */ @@ -352,145 +400,133 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { aSym } - def makeRootPackage: Option[PackageImpl] = - makePackage(RootPackage, null) + /** + * These are all model construction methods. Please do not use them directly, they are calling each other recursively + * starting from makeModel. On the other hand, makeTemplate, makeAnnotation, makeMember, makeType should only be used + * after the model was created (modelFinished=true) otherwise assertions will start failing. + */ + object modelCreation { - /** Creates a package entity for the given symbol or returns `None` if the symbol does not denote a package that - * contains at least one ''documentable'' class, trait or object. Creating a package entity */ - def makePackage(aSym: Symbol, inTpl: => PackageImpl): Option[PackageImpl] = { - val bSym = normalizeTemplate(aSym) - if (templatesCache isDefinedAt (bSym)) - Some(templatesCache(bSym) match {case p: PackageImpl => p}) - else { - val pack = - if (bSym == RootPackage) - new RootPackageImpl(bSym) { - override lazy val comment = - if(settings.docRootContent.isDefault) None - else { - import Streamable._ - Path(settings.docRootContent.value) match { - case f : File => { - val rootComment = closing(f.inputStream)(is => parse(slurp(is), "", NoPosition)) - Some(rootComment) - } - case _ => None - } - } - override val name = "root" - override def inTemplate = this - override def toRoot = this :: Nil - override def qualifiedName = "_root_" - override def inheritedFrom = Nil - override def isRootPackage = true - override lazy val memberSyms = - (bSym.info.members ++ EmptyPackage.info.members) filter { s => - s != EmptyPackage && s != RootPackage - } - } - else - new PackageImpl(bSym, inTpl) {} - if (pack.templates.isEmpty) { - droppedPackages += 1 - None - } - else Some(pack) + def createRootPackage: PackageImpl = docTemplatesCache.get(RootPackage) match { + case Some(root: PackageImpl) => root + case _ => modelCreation.createTemplate(RootPackage, null).asInstanceOf[PackageImpl] } - } - - /** */ - def makeTemplate(aSym: Symbol): TemplateImpl = { - val bSym = normalizeTemplate(aSym) - if (bSym == RootPackage) - makeRootPackage.get - else if (bSym.isPackage) - makeTemplate(bSym.owner) match { - case inPkg: PackageImpl => makePackage(bSym, inPkg) getOrElse (new NoDocTemplateImpl(bSym, inPkg)) - case inNoDocTpl: NoDocTemplateImpl => new NoDocTemplateImpl(bSym, inNoDocTpl) - case _ => throw new Error("'" + bSym + "' must be in a package") - } - else if (templateShouldDocument(bSym)) - makeTemplate(bSym.owner) match { - case inDTpl: DocTemplateImpl => makeDocTemplate(bSym, inDTpl) - case inNoDocTpl: NoDocTemplateImpl => new NoDocTemplateImpl(bSym, inNoDocTpl) - case _ => throw new Error("'" + bSym + "' must be in documentable template") - } - else - new NoDocTemplateImpl(bSym, makeTemplate(bSym.owner)) - } - - /** */ - def makeDocTemplate(aSym: Symbol, inTpl: => DocTemplateImpl): DocTemplateImpl = { - val bSym = normalizeTemplate(aSym) - val minimumInTpl = - if (bSym.owner != inTpl.sym) - makeTemplate(aSym.owner) match { - case inDTpl: DocTemplateImpl => inDTpl - case inNDTpl => throw new Error("'" + bSym + "' is owned by '" + inNDTpl + "' which is not documented") + /** + * Create a template, either a package, class, trait or object + */ + def createTemplate(aSym: Symbol, inTpl: DocTemplateImpl): DocTemplateImpl = { + // don't call this after the model finished! + assert(!modelFinished) + + def createRootPackageComment: Option[Comment] = + if(settings.docRootContent.isDefault) None + else { + import Streamable._ + Path(settings.docRootContent.value) match { + case f : File => { + val rootComment = closing(f.inputStream)(is => parse(slurp(is), "", NoPosition)) + Some(rootComment) + } + case _ => None + } } - else - inTpl - if (templatesCache isDefinedAt (bSym)) - templatesCache(bSym) - else if (bSym.isModule || (bSym.isAliasType && bSym.tpe.typeSymbol.isModule)) - new DocTemplateImpl(bSym, minimumInTpl) with Object - else if (bSym.isTrait || (bSym.isAliasType && bSym.tpe.typeSymbol.isTrait)) - new DocTemplateImpl(bSym, minimumInTpl) with Trait - else if (bSym.isClass || (bSym.isAliasType && bSym.tpe.typeSymbol.isClass)) - new DocTemplateImpl(bSym, minimumInTpl) with Class { - def valueParams = - // we don't want params on a class (non case class) signature - if (isCaseClass) List(sym.constrParamAccessors map (makeValueParam(_, this))) - else List.empty - val constructors = - members collect { case d: Constructor => d } - def primaryConstructor = constructors find { _.isPrimary } + + def createDocTemplate(bSym: Symbol, inTpl: DocTemplateImpl): DocTemplateImpl = { + if (bSym.isModule || (bSym.isAliasType && bSym.tpe.typeSymbol.isModule)) + new DocTemplateImpl(bSym, inTpl) with Object + else if (bSym.isTrait || (bSym.isAliasType && bSym.tpe.typeSymbol.isTrait)) + new DocTemplateImpl(bSym, inTpl) with Trait + else if (bSym.isClass || (bSym.isAliasType && bSym.tpe.typeSymbol.isClass)) + new DocTemplateImpl(bSym, inTpl) with Class { + def valueParams = + // we don't want params on a class (non case class) signature + if (isCaseClass) List(sym.constrParamAccessors map (makeValueParam(_, this))) + else List.empty + val constructors = + members collect { case d: Constructor => d } + def primaryConstructor = constructors find { _.isPrimary } + } + else + sys.error("'" + bSym + "' isn't a class, trait or object thus cannot be built as a documentable template") } - else - throw new Error("'" + bSym + "' that isn't a class, trait or object cannot be built as a documentable template") - } - /** */ - def makeAnnotation(annot: AnnotationInfo): Annotation = { - val aSym = annot.symbol - new EntityImpl(aSym, makeTemplate(aSym.owner)) with Annotation { - lazy val annotationClass = - makeTemplate(annot.symbol) - val arguments = { // lazy - def noParams = annot.args map { _ => None } - val params: List[Option[ValueParam]] = annotationClass match { - case aClass: Class => - (aClass.primaryConstructor map { _.valueParams.head }) match { - case Some(vps) => vps map { Some(_) } - case None => noParams + val bSym = normalizeTemplate(aSym) + if (docTemplatesCache isDefinedAt bSym) + return docTemplatesCache(bSym) + + /* Three cases of templates: + * (1) root package -- special cased for bootstrapping + * (2) package + * (3) class/object/trait + */ + if (bSym == RootPackage) // (1) + new RootPackageImpl(bSym) { + override lazy val comment = createRootPackageComment + override val name = "root" + override def inTemplate = this + override def toRoot = this :: Nil + override def qualifiedName = "_root_" + override def inheritedFrom = Nil + override def isRootPackage = true + override lazy val memberSyms = + (bSym.info.members ++ EmptyPackage.info.members) filter { s => + s != EmptyPackage && s != RootPackage } - case _ => noParams } - assert(params.length == annot.args.length) - (params zip annot.args) flatMap { case (param, arg) => - makeTree(arg) match { - case Some(tree) => - Some(new ValueArgument { - def parameter = param - def value = tree - }) - case None => None - } + else if (bSym.isPackage) // (2) + inTpl match { + case inPkg: PackageImpl => + val pack = new PackageImpl(bSym, inPkg) {} + if (pack.templates.isEmpty && pack.memberSymsLazy.isEmpty) + droppedPackages += pack + pack + case _ => + sys.error("'" + bSym + "' must be in a package") } + else { + // no class inheritance at this point + assert(inOriginalOnwer(bSym, inTpl)) + createDocTemplate(bSym, inTpl) } } + + /** + * After the model is completed, no more DocTemplateEntities are created. + * Therefore any symbol that still appears is: + * - NoDocTemplateMemberEntity (created here) + * - NoDocTemplateEntity (created in makeTemplate) + */ + def createLazyTemplateMember(aSym: Symbol, inTpl: DocTemplateImpl): MemberImpl = { + assert(modelFinished) + val bSym = normalizeTemplate(aSym) + + if (docTemplatesCache isDefinedAt bSym) + docTemplatesCache(bSym) + else + docTemplatesCache.get(bSym.owner) match { + case Some(inTpl) => + val mbrs = inTpl.members.collect({ case mbr: MemberImpl if mbr.sym == bSym => mbr }) + assert(mbrs.length == 1) + mbrs.head + case _ => + // move the class completely to the new location + new NoDocTemplateMemberImpl(aSym, inTpl) + } + } } - /** */ + /** Get the root package */ + def makeRootPackage: PackageImpl = docTemplatesCache(RootPackage).asInstanceOf[PackageImpl] + // TODO: Should be able to override the type - def makeMember(aSym: Symbol, implConv: ImplicitConversionImpl, inTpl: => DocTemplateImpl): List[MemberImpl] = { + def makeMember(aSym: Symbol, implConv: ImplicitConversionImpl, inTpl: DocTemplateImpl): List[MemberImpl] = { def makeMember0(bSym: Symbol, _useCaseOf: Option[MemberImpl]): Option[MemberImpl] = { if (bSym.isGetter && bSym.isLazy) Some(new NonTemplateMemberImpl(bSym, implConv, inTpl) with Val { override lazy val comment = // The analyser does not duplicate the lazy val's DocDef when it introduces its accessor. - thisFactory.comment(bSym.accessed, inTpl) // This hack should be removed after analyser is fixed. + thisFactory.comment(bSym.accessed, inTpl.asInstanceOf[DocTemplateImpl]) // This hack should be removed after analyser is fixed. override def isLazyVal = true override def useCaseOf = _useCaseOf }) @@ -538,10 +574,18 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def alias = makeTypeInTemplateContext(sym.tpe.dealias, inTpl, sym) override def useCaseOf = _useCaseOf }) - else if (bSym.isPackage) - inTpl match { case inPkg: PackageImpl => makePackage(bSym, inPkg) } - else if ((bSym.isClass || bSym.isModule || bSym == AnyRefClass) && templateShouldDocument(bSym)) - Some(makeDocTemplate(bSym, inTpl)) + else if (bSym.isPackage && !modelFinished) + inTpl match { + case inPkg: PackageImpl => modelCreation.createTemplate(bSym, inTpl) match { + case p: PackageImpl if droppedPackages contains p => None + case p: PackageImpl => Some(p) + case _ => sys.error("'" + bSym + "' must be a package") + } + case _ => + sys.error("'" + bSym + "' must be in a package") + } + else if (!modelFinished && templateShouldDocument(bSym, inTpl) && inOriginalOnwer(bSym, inTpl)) + Some(modelCreation.createTemplate(bSym, inTpl)) else None } @@ -561,14 +605,78 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { // Use cases replace the original definitions - SI-5054 allSyms flatMap { makeMember0(_, member) } } + } + + def findMember(aSym: Symbol, inTpl: DocTemplateImpl): Option[MemberImpl] = { + val tplSym = normalizeTemplate(aSym.owner) + inTpl.members.find(_.sym == aSym) + } + + def findTemplate(query: String): Option[DocTemplateImpl] = { + assert(modelFinished) + docTemplatesCache.values find { (tpl: TemplateImpl) => tpl.qualifiedName == query && !tpl.isObject } + } + + def findTemplateMaybe(aSym: Symbol): Option[DocTemplateImpl] = { + assert(modelFinished) + docTemplatesCache.get(normalizeTemplate(aSym)) + } + + def makeTemplate(aSym: Symbol): TemplateImpl = { + assert(modelFinished) + def makeNoDocTemplate(aSym: Symbol, inTpl: TemplateImpl): NoDocTemplateImpl = { + val bSym = normalizeTemplate(aSym) + noDocTemplatesCache.get(bSym) match { + case Some(noDocTpl) => noDocTpl + case None => new NoDocTemplateImpl(bSym, inTpl) + } + } + + findTemplateMaybe(aSym) match { + case Some(dtpl) => + dtpl + case None => + val bSym = normalizeTemplate(aSym) + makeNoDocTemplate(bSym, makeTemplate(bSym.owner)) + } + } + + + /** */ + def makeAnnotation(annot: AnnotationInfo): Annotation = { + val aSym = annot.symbol + new EntityImpl(aSym, makeTemplate(aSym.owner)) with Annotation { + lazy val annotationClass = + makeTemplate(annot.symbol) + val arguments = { // lazy + def noParams = annot.args map { _ => None } + val params: List[Option[ValueParam]] = annotationClass match { + case aClass: Class => + (aClass.primaryConstructor map { _.valueParams.head }) match { + case Some(vps) => vps map { Some(_) } + case None => noParams + } + case _ => noParams + } + assert(params.length == annot.args.length) + (params zip annot.args) flatMap { case (param, arg) => + makeTree(arg) match { + case Some(tree) => + Some(new ValueArgument { + def parameter = param + def value = tree + }) + case None => None + } + } + } + } } /** */ - def makeTypeParam(aSym: Symbol, inTpl: => TemplateImpl): TypeParam = + def makeTypeParam(aSym: Symbol, inTpl: TemplateImpl): TypeParam = new ParameterImpl(aSym, inTpl) with TypeBoundsImpl with HigherKindedImpl with TypeParam { - def isTypeParam = true - def isValueParam = false def variance: String = { if (sym hasFlag Flags.COVARIANT) "+" else if (sym hasFlag Flags.CONTRAVARIANT) "-" @@ -577,16 +685,15 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } /** */ - def makeValueParam(aSym: Symbol, inTpl: => DocTemplateImpl): ValueParam = { + def makeValueParam(aSym: Symbol, inTpl: DocTemplateImpl): ValueParam = { makeValueParam(aSym, inTpl, aSym.nameString) } + /** */ - def makeValueParam(aSym: Symbol, inTpl: => DocTemplateImpl, newName: String): ValueParam = + def makeValueParam(aSym: Symbol, inTpl: DocTemplateImpl, newName: String): ValueParam = new ParameterImpl(aSym, inTpl) with ValueParam { override val name = newName - def isTypeParam = false - def isValueParam = true def defaultValue = if (aSym.hasDefault) { // units.filter should return only one element @@ -601,12 +708,12 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } else None def resultType = - makeTypeInTemplateContext(sym.tpe, inTpl, sym) + makeTypeInTemplateContext(aSym.tpe, inTpl, aSym) def isImplicit = aSym.isImplicit } /** */ - def makeTypeInTemplateContext(aType: Type, inTpl: => TemplateImpl, dclSym: Symbol): TypeEntity = { + def makeTypeInTemplateContext(aType: Type, inTpl: TemplateImpl, dclSym: Symbol): TypeEntity = { def ownerTpl(sym: Symbol): Symbol = if (sym.isClass || sym.isModule || sym == NoSymbol) sym else ownerTpl(sym.owner) val tpe = @@ -620,10 +727,10 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } /** */ - def makeType(aType: Type, inTpl: => TemplateImpl): TypeEntity = { + def makeType(aType: Type, inTpl: TemplateImpl): TypeEntity = { def templatePackage = closestPackage(inTpl.sym) - new TypeEntity { + def createTypeEntity = new TypeEntity { private val nameBuffer = new StringBuilder private var refBuffer = new immutable.TreeMap[Int, (TemplateEntity, Int)] private def appendTypes0(types: List[Type], sep: String): Unit = types match { @@ -719,23 +826,81 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { val refEntity = refBuffer val name = optimize(nameBuffer.toString) } - } - def templateShouldDocument(aSym: Symbol): Boolean = { - // TODO: document sourceless entities (e.g., Any, etc), based on a new Setting to be added - (aSym.isPackageClass || (aSym.sourceFile != null)) && localShouldDocument(aSym) && - ( aSym.owner == NoSymbol || templateShouldDocument(aSym.owner) ) && !isEmptyJavaObject(aSym) + if (aType.isTrivial) + typeCache.get(aType) match { + case Some(typeEntity) => typeEntity + case None => + val typeEntity = createTypeEntity + typeCache += aType -> typeEntity + typeEntity + } + else + createTypeEntity } - def isEmptyJavaObject(aSym: Symbol): Boolean = { - def hasMembers = aSym.info.members.exists(s => localShouldDocument(s) && (!s.isConstructor || s.owner == aSym)) - aSym.isModule && aSym.isJavaDefined && !hasMembers - } + def normalizeOwner(aSym: Symbol): Symbol = + /* + * Okay, here's the explanation of what happens. The code: + * + * package foo { + * object `package` { + * class Bar + * } + * } + * + * will yield this Symbol structure: + * + * +---------------+ +--------------------------+ + * | package foo#1 ----(1)---> module class foo#2 | + * +---------------+ | +----------------------+ | +-------------------------+ + * | | package object foo#3 ------(1)---> module class package#4 | + * | +----------------------+ | | +---------------------+ | + * +--------------------------+ | | class package$Bar#5 | | + * | +---------------------+ | + * +-------------------------+ + * (1) sourceModule + * (2) you get out of owners with .owner + */ + normalizeTemplate(aSym) match { + case bSym if bSym.isPackageObject => + normalizeOwner(bSym.owner) + case bSym => + bSym + } - def localShouldDocument(aSym: Symbol): Boolean = { + def inOriginalOnwer(aSym: Symbol, inTpl: TemplateImpl): Boolean = + normalizeOwner(aSym.owner) == normalizeOwner(inTpl.sym) + + def templateShouldDocument(aSym: Symbol, inTpl: TemplateImpl): Boolean = + (aSym.isClass || aSym.isModule || aSym == AnyRefClass) && + localShouldDocument(aSym) && + !isEmptyJavaObject(aSym) && + // either it's inside the original owner or we can document it later: + (!inOriginalOnwer(aSym, inTpl) || (aSym.isPackageClass || (aSym.sourceFile != null))) + + def membersShouldDocument(sym: Symbol, inTpl: TemplateImpl) = + // pruning modules that shouldn't be documented + // Why Symbol.isInitialized? Well, because we need to avoid exploring all the space available to scaladoc + // from the classpath -- scaladoc is a hog, it will explore everything starting from the root package unless we + // somehow prune the tree. And isInitialized is a good heuristic for prunning -- if the package was not explored + // during typer and refchecks, it's not necessary for the current application and there's no need to explore it. + (!sym.isModule || sym.moduleClass.isInitialized) && + // documenting only public and protected members + localShouldDocument(sym) && + // Only this class's constructors are part of its members, inherited constructors are not. + (!sym.isConstructor || sym.owner == inTpl.sym) && + // If the @bridge annotation overrides a normal member, show it + !isPureBridge(sym) + + def isEmptyJavaObject(aSym: Symbol): Boolean = + aSym.isModule && aSym.isJavaDefined && + aSym.info.members.exists(s => localShouldDocument(s) && (!s.isConstructor || s.owner == aSym)) + + def localShouldDocument(aSym: Symbol): Boolean = !aSym.isPrivate && (aSym.isProtected || aSym.privateWithin == NoSymbol) && !aSym.isSynthetic - } /** Filter '@bridge' methods only if *they don't override non-bridge methods*. See SI-5373 for details */ def isPureBridge(sym: Symbol) = sym.isBridge && sym.allOverriddenSymbols.forall(_.isBridge) } + diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala index c3525037cd..2a0fcea0ea 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala @@ -58,6 +58,7 @@ trait ModelFactoryImplicitSupport { import global._ import global.analyzer._ import global.definitions._ + import rootMirror.{RootPackage, RootClass, EmptyPackage, EmptyPackageClass} import settings.hardcoded // debugging: @@ -96,13 +97,7 @@ trait ModelFactoryImplicitSupport { def targetType: TypeEntity = makeType(toType, inTpl) - def convertorOwner: TemplateEntity = - if (convSym != NoSymbol) - makeTemplate(convSym.owner) - else { - error("Scaladoc implicits: Implicit conversion from " + sym.tpe + " to " + toType + " done by " + convSym + " = NoSymbol!") - makeRootPackage.get // surely the root package was created :) - } + def convertorOwner: TemplateEntity = makeTemplate(convSym.owner) def convertorMethod: Either[MemberEntity, String] = { var convertor: MemberEntity = null @@ -122,11 +117,11 @@ trait ModelFactoryImplicitSupport { def conversionShortName = convSym.nameString - def conversionQualifiedName = convertorOwner.qualifiedName + "." + convSym.nameString + def conversionQualifiedName = makeQualifiedName(convSym) lazy val constraints: List[Constraint] = constrs - val members: List[MemberEntity] = { + val members: List[MemberImpl] = { // Obtain the members inherited by the implicit conversion var memberSyms = toType.members.filter(implicitShouldDocument(_)) val existingMembers = sym.info.members @@ -162,7 +157,7 @@ trait ModelFactoryImplicitSupport { * default Scala imports (Predef._ for example) and the companion object of the current class, if one exists. In the * future we might want to extend this to more complex scopes. */ - def makeImplicitConversions(sym: Symbol, inTpl: => DocTemplateImpl): List[ImplicitConversion] = + def makeImplicitConversions(sym: Symbol, inTpl: => DocTemplateImpl): List[ImplicitConversionImpl] = // Nothing and Null are somewhat special -- they can be transformed by any implicit conversion available in scope. // But we don't want that, so we'll simply refuse to find implicit conversions on for Nothing and Null if (!(sym.isClass || sym.isTrait || sym == AnyRefClass) || sym == NothingClass || sym == NullClass) Nil @@ -218,7 +213,7 @@ trait ModelFactoryImplicitSupport { * - we also need to transform implicit parameters in the view's signature into constraints, such that Numeric[T4] * appears as a constraint */ - def makeImplicitConversion(sym: Symbol, result: SearchResult, constrs: List[TypeConstraint], context: Context, inTpl: => DocTemplateImpl): List[ImplicitConversion] = + def makeImplicitConversion(sym: Symbol, result: SearchResult, constrs: List[TypeConstraint], context: Context, inTpl: => DocTemplateImpl): List[ImplicitConversionImpl] = if (result.tree == EmptyTree) Nil else { // `result` will contain the type of the view (= implicit conversion method) @@ -280,7 +275,7 @@ trait ModelFactoryImplicitSupport { types.flatMap((tpe:Type) => { // TODO: Before creating constraints, map typeVarToOriginOrWildcard on the implicitTypes val implType = typeVarToOriginOrWildcard(tpe) - val qualifiedName = implType.typeSymbol.ownerChain.reverse.map(_.nameString).mkString(".") + val qualifiedName = makeQualifiedName(implType.typeSymbol) var available: Option[Boolean] = None @@ -333,20 +328,20 @@ trait ModelFactoryImplicitSupport { case Some(explanation) => List(new KnownTypeClassConstraint { val typeParamName = targ.nameString - val typeExplanation = explanation - val typeClassEntity = makeTemplate(sym) - val implicitType: TypeEntity = makeType(implType, inTpl) + lazy val typeExplanation = explanation + lazy val typeClassEntity = makeTemplate(sym) + lazy val implicitType: TypeEntity = makeType(implType, inTpl) }) case None => List(new TypeClassConstraint { val typeParamName = targ.nameString - val typeClassEntity = makeTemplate(sym) - val implicitType: TypeEntity = makeType(implType, inTpl) + lazy val typeClassEntity = makeTemplate(sym) + lazy val implicitType: TypeEntity = makeType(implType, inTpl) }) } case _ => List(new ImplicitInScopeConstraint{ - val implicitType: TypeEntity = makeType(implType, inTpl) + lazy val implicitType: TypeEntity = makeType(implType, inTpl) }) } } @@ -372,23 +367,23 @@ trait ModelFactoryImplicitSupport { case (List(lo), List(up)) if (lo == up) => List(new EqualTypeParamConstraint { val typeParamName = tparam.nameString - val rhs = makeType(lo, inTpl) + lazy val rhs = makeType(lo, inTpl) }) case (List(lo), List(up)) => List(new BoundedTypeParamConstraint { val typeParamName = tparam.nameString - val lowerBound = makeType(lo, inTpl) - val upperBound = makeType(up, inTpl) + lazy val lowerBound = makeType(lo, inTpl) + lazy val upperBound = makeType(up, inTpl) }) case (List(lo), Nil) => List(new LowerBoundedTypeParamConstraint { val typeParamName = tparam.nameString - val lowerBound = makeType(lo, inTpl) + lazy val lowerBound = makeType(lo, inTpl) }) case (Nil, List(up)) => List(new UpperBoundedTypeParamConstraint { val typeParamName = tparam.nameString - val upperBound = makeType(up, inTpl) + lazy val upperBound = makeType(up, inTpl) }) case other => // this is likely an error on the lub/glb side @@ -399,6 +394,11 @@ trait ModelFactoryImplicitSupport { } } + def makeQualifiedName(sym: Symbol): String = { + val remove = Set[Symbol](RootPackage, RootClass, EmptyPackage, EmptyPackageClass) + sym.ownerChain.filterNot(remove.contains(_)).reverse.map(_.nameString).mkString(".") + } + /** * uniteConstraints takes a TypeConstraint instance and simplifies the constraints inside * diff --git a/src/compiler/scala/tools/nsc/doc/model/TreeFactory.scala b/src/compiler/scala/tools/nsc/doc/model/TreeFactory.scala index fe586c4996..bd7534ded4 100755 --- a/src/compiler/scala/tools/nsc/doc/model/TreeFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/TreeFactory.scala @@ -52,7 +52,7 @@ trait TreeFactory { thisTreeFactory: ModelFactory with TreeFactory => if (asym.isSetter) asym = asym.getter(asym.owner) makeTemplate(asym.owner) match { case docTmpl: DocTemplateImpl => - val mbrs: List[MemberImpl] = makeMember(asym, null, docTmpl) + val mbrs: Option[MemberImpl] = findMember(asym, docTmpl) mbrs foreach { mbr => refs += ((start, (mbr,end))) } case _ => } diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala index 996223b9f9..c749d30267 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala @@ -733,8 +733,8 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => nextChar() } - /** - * Eliminates the (common) leading spaces in all lines, based on the first line + /** + * Eliminates the (common) leading spaces in all lines, based on the first line * For indented pieces of code, it reduces the indent to the least whitespace prefix: * {{{ * indented example @@ -757,11 +757,11 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => while (index < code.length) { code(index) match { case ' ' => - if (wsArea) + if (wsArea) crtSkip += 1 case c => wsArea = (c == '\n') - maxSkip = if (firstLine || emptyLine) maxSkip else if (maxSkip <= crtSkip) maxSkip else crtSkip + maxSkip = if (firstLine || emptyLine) maxSkip else if (maxSkip <= crtSkip) maxSkip else crtSkip crtSkip = if (c == '\n') 0 else crtSkip firstLine = if (c == '\n') false else firstLine emptyLine = if (c == '\n') true else false diff --git a/test/scaladoc/run/SI-5373.scala b/test/scaladoc/run/SI-5373.scala index 0062abbb2a..65cf8baff5 100644 --- a/test/scaladoc/run/SI-5373.scala +++ b/test/scaladoc/run/SI-5373.scala @@ -12,12 +12,12 @@ object Test extends ScaladocModelTest { def foo = () } - trait B { + trait B extends A { @bridge() def foo = () } - class C extends A with B + class C extends B } """ diff --git a/test/scaladoc/run/package-object.check b/test/scaladoc/run/package-object.check index 4297847e73..01dbcc682f 100644 --- a/test/scaladoc/run/package-object.check +++ b/test/scaladoc/run/package-object.check @@ -1,2 +1,3 @@ -List((test.B,B), (test.A,A), (scala.AnyRef,AnyRef), (scala.Any,Any)) +List(test.B, test.A, scala.AnyRef, scala.Any) +List(B, A, AnyRef, Any) Done. diff --git a/test/scaladoc/run/package-object.scala b/test/scaladoc/run/package-object.scala index fd36a8df7b..5fb5a4ddf1 100644 --- a/test/scaladoc/run/package-object.scala +++ b/test/scaladoc/run/package-object.scala @@ -9,7 +9,8 @@ object Test extends ScaladocModelTest { import access._ val p = root._package("test") - println(p.linearization) + println(p.linearizationTemplates) + println(p.linearizationTypes) } } -- cgit v1.2.3 From fba65513d1195b162f958a5c25124958d6e7ea52 Mon Sep 17 00:00:00 2001 From: Vlad Ureche Date: Wed, 13 Jun 2012 16:35:58 +0200 Subject: Scaladoc class diagrams part 1 This commit contains model changes required for adding class diagrams to scaladoc. It also contains an improved implicit shadowing computation, which hides the shadowed implicitly inherited members from the main view and gives instructions on how to access them. This is joint work with Damien Obrist (@damienobrist) on supporting diagram generation in scaladoc, as part of Damien's semester project in the LAMP laborarory at EPFL. The full history is located at: https://github.com/damienobrist/scala/tree/feature/diagrams-dev Commit summary: - diagrams model - diagram settings (Settings.scala, ScalaDoc.scala) - diagram model object (Entity.scala, Diagram.scala) - model: tracking direct superclasses and subclasses, implicit conversions from and to (ModelFactory.scala) - diagram object computation (DiagramFactory.scala, DocFactory.scala) - capacity to filter diagrams (CommentFactory.scala, DiagramDirectiveParser.scala) - diagram statistics object (DiagramStats.scala) - delayed link evaluation (Body.scala, Comment.scala) - tests - improved implicits shadowing information - model shadowing computation (ModelFactoryImplicitSupport.scala, Entity.scala) - html generation for shadowing information (Template.scala) - tests Also fixes an issue reported by @dragos, where single-line comment expansion would lead to the comment disappearing. Review by @kzys, @pedrofurla. Adapted to the new model and fixed a couple of problems: - duplicate implicit conversions in StringAdd/StringFormat - incorrect implicit conversion signature (from X to X) Conflicts: src/compiler/scala/tools/nsc/doc/Settings.scala src/compiler/scala/tools/nsc/doc/html/page/Template.scala src/compiler/scala/tools/nsc/doc/model/Entity.scala src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala --- src/compiler/scala/tools/nsc/ScalaDoc.scala | 3 +- src/compiler/scala/tools/nsc/doc/DocFactory.scala | 6 +- src/compiler/scala/tools/nsc/doc/Settings.scala | 58 ++++- .../scala/tools/nsc/doc/html/HtmlPage.scala | 16 +- src/compiler/scala/tools/nsc/doc/html/Page.scala | 14 -- .../scala/tools/nsc/doc/html/page/Index.scala | 2 +- .../tools/nsc/doc/html/page/IndexScript.scala | 2 +- .../scala/tools/nsc/doc/html/page/Template.scala | 81 ++++++- .../nsc/doc/html/page/diagram/DiagramStats.scala | 58 +++++ .../tools/nsc/doc/html/resource/lib/template.css | 6 +- .../scala/tools/nsc/doc/model/Entity.scala | 80 +++++-- .../scala/tools/nsc/doc/model/ModelFactory.scala | 117 +++++++-- .../doc/model/ModelFactoryImplicitSupport.scala | 265 ++++++++++++++------- .../scala/tools/nsc/doc/model/comment/Body.scala | 2 +- .../tools/nsc/doc/model/comment/Comment.scala | 6 + .../nsc/doc/model/comment/CommentFactory.scala | 132 +++++----- .../tools/nsc/doc/model/diagram/Diagram.scala | 143 +++++++++++ .../doc/model/diagram/DiagramDirectiveParser.scala | 248 +++++++++++++++++++ .../nsc/doc/model/diagram/DiagramFactory.scala | 197 +++++++++++++++ .../scala/tools/partest/ScaladocModelTest.scala | 4 +- .../resources/implicits-ambiguating-res.scala | 72 ++++++ test/scaladoc/resources/implicits-base-res.scala | 16 +- .../resources/implicits-elimination-res.scala | 6 +- test/scaladoc/run/diagrams-base.check | 1 + test/scaladoc/run/diagrams-base.scala | 73 ++++++ test/scaladoc/run/diagrams-determinism.check | 1 + test/scaladoc/run/diagrams-determinism.scala | 67 ++++++ test/scaladoc/run/diagrams-filtering.check | 1 + test/scaladoc/run/diagrams-filtering.scala | 93 ++++++++ test/scaladoc/run/implicits-ambiguating.check | 1 + test/scaladoc/run/implicits-ambiguating.scala | 114 +++++++++ test/scaladoc/run/implicits-base.scala | 39 ++- test/scaladoc/run/implicits-elimination.check | 1 - test/scaladoc/run/implicits-elimination.scala | 23 -- test/scaladoc/run/implicits-shadowing.scala | 35 +-- test/scaladoc/run/implicits-var-exp.check | 1 + test/scaladoc/run/implicits-var-exp.scala | 43 ++++ test/scaladoc/scalacheck/CommentFactoryTest.scala | 6 +- 38 files changed, 1766 insertions(+), 267 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala create mode 100644 src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala create mode 100644 src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala create mode 100644 src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala create mode 100644 test/scaladoc/resources/implicits-ambiguating-res.scala create mode 100644 test/scaladoc/run/diagrams-base.check create mode 100644 test/scaladoc/run/diagrams-base.scala create mode 100644 test/scaladoc/run/diagrams-determinism.check create mode 100644 test/scaladoc/run/diagrams-determinism.scala create mode 100644 test/scaladoc/run/diagrams-filtering.check create mode 100644 test/scaladoc/run/diagrams-filtering.scala create mode 100644 test/scaladoc/run/implicits-ambiguating.check create mode 100644 test/scaladoc/run/implicits-ambiguating.scala delete mode 100644 test/scaladoc/run/implicits-elimination.check delete mode 100644 test/scaladoc/run/implicits-elimination.scala create mode 100644 test/scaladoc/run/implicits-var-exp.check create mode 100644 test/scaladoc/run/implicits-var-exp.scala diff --git a/src/compiler/scala/tools/nsc/ScalaDoc.scala b/src/compiler/scala/tools/nsc/ScalaDoc.scala index 5a4b4172c6..c6fdb4b891 100644 --- a/src/compiler/scala/tools/nsc/ScalaDoc.scala +++ b/src/compiler/scala/tools/nsc/ScalaDoc.scala @@ -20,7 +20,8 @@ class ScalaDoc { def process(args: Array[String]): Boolean = { var reporter: ConsoleReporter = null - val docSettings = new doc.Settings(msg => reporter.error(FakePos("scaladoc"), msg + "\n scaladoc -help gives more information")) + val docSettings = new doc.Settings(msg => reporter.error(FakePos("scaladoc"), msg + "\n scaladoc -help gives more information"), + msg => reporter.printMessage(msg)) reporter = new ConsoleReporter(docSettings) { // need to do this so that the Global instance doesn't trash all the // symbols just because there was an error diff --git a/src/compiler/scala/tools/nsc/doc/DocFactory.scala b/src/compiler/scala/tools/nsc/doc/DocFactory.scala index 37e3b626e8..3c92c3b4b6 100644 --- a/src/compiler/scala/tools/nsc/doc/DocFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/DocFactory.scala @@ -81,6 +81,7 @@ class DocFactory(val reporter: Reporter, val settings: doc.Settings) { processor new { override val global: compiler.type = compiler } with model.ModelFactory(compiler, settings) with model.ModelFactoryImplicitSupport + with model.diagram.DiagramFactory with model.comment.CommentFactory with model.TreeFactory { override def templateShouldDocument(sym: compiler.Symbol, inTpl: TemplateImpl) = @@ -90,11 +91,12 @@ class DocFactory(val reporter: Reporter, val settings: doc.Settings) { processor modelFactory.makeModel match { case Some(madeModel) => - if (settings.reportModel) + if (!settings.scaladocQuietRun) println("model contains " + modelFactory.templatesCount + " documentable templates") Some(madeModel) case None => - println("no documentable class found in compilation units") + if (!settings.scaladocQuietRun) + println("no documentable class found in compilation units") None } diff --git a/src/compiler/scala/tools/nsc/doc/Settings.scala b/src/compiler/scala/tools/nsc/doc/Settings.scala index 1cc1f98fba..31e49131f6 100644 --- a/src/compiler/scala/tools/nsc/doc/Settings.scala +++ b/src/compiler/scala/tools/nsc/doc/Settings.scala @@ -11,8 +11,9 @@ import java.lang.System import language.postfixOps /** An extended version of compiler settings, with additional Scaladoc-specific options. - * @param error A function that prints a string to the appropriate error stream. */ -class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { + * @param error A function that prints a string to the appropriate error stream + * @param print A function that prints the string, without any extra boilerplate of error */ +class Settings(error: String => Unit, val printMsg: String => Unit = println(_)) extends scala.tools.nsc.Settings(error) { /** A setting that defines in which format the documentation is output. ''Note:'' this setting is currently always * `html`. */ @@ -104,6 +105,12 @@ class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { "(for example conversions that require Numeric[String] to be in scope)" ) + val docImplicitsSoundShadowing = BooleanSetting ( + "-implicits-sound-shadowing", + "Use a sound implicit shadowing calculation. Note: this interacts badly with usecases, so " + + "only use it if you haven't defined usecase for implicitly inherited members." + ) + val docDiagrams = BooleanSetting ( "-diagrams", "Create inheritance diagrams for classes, traits and packages." @@ -116,10 +123,44 @@ class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { val docDiagramsDotPath = PathSetting ( "-diagrams-dot-path", - "The path to the dot executable used to generate the inheritance diagrams. Ex: /usr/bin/dot", + "The path to the dot executable used to generate the inheritance diagrams. Eg: /usr/bin/dot", "dot" // by default, just pick up the system-wide dot ) + /** The maxium nuber of normal classes to show in the diagram */ + val docDiagramsMaxNormalClasses = IntSetting( + "-diagrams-max-classes", + "The maximum number of superclasses or subclasses to show in a diagram", + 15, + None, + _ => None + ) + + /** The maxium nuber of implcit classes to show in the diagram */ + val docDiagramsMaxImplicitClasses = IntSetting( + "-diagrams-max-implicits", + "The maximum number of implicitly converted classes to show in a diagram", + 10, + None, + _ => None + ) + + val docDiagramsDotTimeout = IntSetting( + "-diagrams-dot-timeout", + "The timeout before the graphviz dot util is forecefully closed, in seconds (default: 10)", + 10, + None, + _ => None + ) + + val docDiagramsDotRestart = IntSetting( + "-diagrams-dot-restart", + "The number of times to restart a malfunctioning dot process before disabling diagrams (default: 5)", + 5, + None, + _ => None + ) + val docRawOutput = BooleanSetting ( "-raw-output", "For each html file, create another .html.raw file containing only the text. (can be used for quickly diffing two scaladoc outputs)" @@ -134,14 +175,16 @@ class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { def scaladocSpecific = Set[Settings#Setting]( docformat, doctitle, docfooter, docversion, docUncompilable, docsourceurl, docgenerator, docRootContent, useStupidTypes, docDiagrams, docDiagramsDebug, docDiagramsDotPath, - docImplicits, docImplicitsDebug, docImplicitsShowAll + docDiagramsDotTimeout, docDiagramsDotRestart, + docImplicits, docImplicitsDebug, docImplicitsShowAll, + docDiagramsMaxNormalClasses, docDiagramsMaxImplicitClasses ) val isScaladocSpecific: String => Boolean = scaladocSpecific map (_.name) override def isScaladoc = true - // unset by the testsuite, we don't need to count the entities in the model - var reportModel = true + // set by the testsuite, when checking test output + var scaladocQuietRun = false /** * This is the hardcoded area of Scaladoc. This is where "undesirable" stuff gets eliminated. I know it's not pretty, @@ -188,7 +231,8 @@ class Settings(error: String => Unit) extends scala.tools.nsc.Settings(error) { "scala.Predef.any2stringfmt", "scala.Predef.any2stringadd", "scala.Predef.any2ArrowAssoc", - "scala.Predef.any2Ensuring") + "scala.Predef.any2Ensuring", + "scala.collection.TraversableOnce.alternateImplicit") /** There's a reason all these are specialized by hand but documenting each of them is beyond the point */ val arraySkipConversions = List( diff --git a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala index 4c9215f923..4a1a8cf898 100644 --- a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala +++ b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala @@ -118,11 +118,25 @@ abstract class HtmlPage extends Page { thisPage => case Superscript(in) => { inlineToHtml(in) } case Subscript(in) => { inlineToHtml(in) } case Link(raw, title) => { inlineToHtml(title) } - case EntityLink(entity) => templateToHtml(entity) case Monospace(in) => { inlineToHtml(in) } case Text(text) => xml.Text(text) case Summary(in) => inlineToHtml(in) case HtmlTag(tag) => xml.Unparsed(tag) + case EntityLink(target, template) => template() match { + case Some(tpl) => + templateToHtml(tpl) + case None => + xml.Text(target) + } + } + + def typeToHtml(tpes: List[model.TypeEntity], hasLinks: Boolean): NodeSeq = tpes match { + case Nil => + sys.error("Internal Scaladoc error") + case List(tpe) => + typeToHtml(tpe, hasLinks) + case tpe :: rest => + typeToHtml(tpe, hasLinks) ++ scala.xml.Text(" with ") ++ typeToHtml(rest, hasLinks) } def typeToHtml(tpe: model.TypeEntity, hasLinks: Boolean): NodeSeq = { diff --git a/src/compiler/scala/tools/nsc/doc/html/Page.scala b/src/compiler/scala/tools/nsc/doc/html/Page.scala index 40ae65a37a..5e3ab87ccd 100644 --- a/src/compiler/scala/tools/nsc/doc/html/Page.scala +++ b/src/compiler/scala/tools/nsc/doc/html/Page.scala @@ -97,18 +97,4 @@ abstract class Page { } relativize(thisPage.path.reverse, destPath.reverse).mkString("/") } - - def isExcluded(dtpl: DocTemplateEntity) = { - val qname = dtpl.qualifiedName - ( ( qname.startsWith("scala.Tuple") || qname.startsWith("scala.Product") || - qname.startsWith("scala.Function") || qname.startsWith("scala.runtime.AbstractFunction") - ) && !( - qname == "scala.Tuple1" || qname == "scala.Tuple2" || - qname == "scala.Product" || qname == "scala.Product1" || qname == "scala.Product2" || - qname == "scala.Function" || qname == "scala.Function1" || qname == "scala.Function2" || - qname == "scala.runtime.AbstractFunction0" || qname == "scala.runtime.AbstractFunction1" || - qname == "scala.runtime.AbstractFunction2" - ) - ) - } } diff --git a/src/compiler/scala/tools/nsc/doc/html/page/Index.scala b/src/compiler/scala/tools/nsc/doc/html/page/Index.scala index 8ed13e0da2..0e894a03bf 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Index.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Index.scala @@ -61,7 +61,7 @@ class Index(universe: doc.Universe, index: doc.Index) extends HtmlPage { }
      { val tpls: Map[String, Seq[DocTemplateEntity]] = - (pack.templates filter (t => !t.isPackage && !isExcluded(t) )) groupBy (_.name) + (pack.templates filter (t => !t.isPackage && !universe.settings.hardcoded.isExcluded(t.qualifiedName) )) groupBy (_.name) val placeholderSeq: NodeSeq =
      diff --git a/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala b/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala index c5fe4da17a..2b68ac2937 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/IndexScript.scala @@ -62,7 +62,7 @@ class IndexScript(universe: doc.Universe, index: doc.Index) extends Page { def allPackagesWithTemplates = { Map(allPackages.map((key) => { - key -> key.templates.filter(t => !t.isPackage && !isExcluded(t)) + key -> key.templates.filter(t => !t.isPackage && !universe.settings.hardcoded.isExcluded(t.qualifiedName)) }) : _*) } } diff --git a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala index 975ff8fb89..5978489585 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala @@ -8,10 +8,11 @@ package doc package html package page -import model._ import scala.xml.{ NodeSeq, Text, UnprefixedAttribute } import language.postfixOps +import model._ + class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage { val path = @@ -41,9 +42,12 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage val (absValueMembers, nonAbsValueMembers) = valueMembers partition (_.isAbstract) - val (deprValueMembers, concValueMembers) = + val (deprValueMembers, nonDeprValueMembers) = nonAbsValueMembers partition (_.deprecation.isDefined) + val (concValueMembers, shadowedImplicitMembers) = + nonDeprValueMembers partition (!Template.isShadowedOrAmbiguousImplicit(_)) + val typeMembers = tpl.abstractTypes ++ tpl.aliasTypes ++ tpl.templates.filter(x => x.isTrait || x.isClass) sorted @@ -163,6 +167,13 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage
    } + { if (shadowedImplicitMembers.isEmpty) NodeSeq.Empty else +
    +

    Shadowed Implict Value Members

    +
      { shadowedImplicitMembers map (memberToHtml(_, tpl)) }
    +
    + } + { if (deprValueMembers.isEmpty) NodeSeq.Empty else

    Deprecated Value Members

    @@ -244,7 +255,8 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage } val memberComment = memberToCommentHtml(mbr, inTpl, false)
  • + data-isabs={ mbr.isAbstract.toString } + fullComment={ if(memberComment.filter(_.label=="div").isEmpty) "no" else "yes" }> { signature(mbr, false) } { memberComment } @@ -299,6 +311,7 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage

    { inlineToHtml(mbr.comment.get.short) }

    def memberToCommentBodyHtml(mbr: MemberEntity, inTpl: DocTemplateEntity, isSelf: Boolean, isReduced: Boolean = false): NodeSeq = { + val s = universe.settings val memberComment = if (mbr.comment.isEmpty) NodeSeq.Empty @@ -387,6 +400,35 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage { targetType } performed by method { conversionMethod } in { conversionOwner }. { constraintText } + } ++ { + if (Template.isShadowedOrAmbiguousImplicit(mbr)) { + // These are the members that are shadowing or ambiguating the current implicit + // see ImplicitMemberShadowing trait for more information + val shadowingSuggestion = { + val params = mbr match { + case d: Def => d.valueParams map (_ map (_ name) mkString("(", ", ", ")")) mkString + case _ => "" // no parameters + } +
    ++ xml.Text("To access this member you can use a ") ++ +
    type ascription ++ xml.Text(":") ++ +
    ++
    {"(" + Template.lowerFirstLetter(tpl.name) + ": " + conv.targetType.name + ")." + mbr.name + params }
    + } + + val shadowingWarning: NodeSeq = + if (Template.isShadowedImplicit(mbr)) + xml.Text("This implicitly inherited member is shadowed by one or more members in this " + + "class.") ++ shadowingSuggestion + else if (Template.isAmbiguousImplicit(mbr)) + xml.Text("This implicitly inherited member is ambiguous. One or more implicitly " + + "inherited members have similar signatures, so calling this member may produce an ambiguous " + + "implicit conversion compiler error.") ++ shadowingSuggestion + else NodeSeq.Empty + +
    Shadowing
    ++ +
    { shadowingWarning }
    + + } else NodeSeq.Empty } case _ => NodeSeq.Empty @@ -562,11 +604,11 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage } val subclasses = mbr match { - case dtpl: DocTemplateEntity if isSelf && !isReduced && dtpl.subClasses.nonEmpty => + case dtpl: DocTemplateEntity if isSelf && !isReduced && dtpl.allSubClasses.nonEmpty =>
    Known Subclasses
    { - templatesToHtml(dtpl.subClasses.sortBy(_.name), xml.Text(", ")) + templatesToHtml(dtpl.allSubClasses.sortBy(_.name), xml.Text(", ")) }
    case _ => NodeSeq.Empty @@ -605,13 +647,13 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage case PrivateInTemplate(owner) if (owner == mbr.inTemplate) => Some(Paragraph(CText("private"))) case PrivateInTemplate(owner) => - Some(Paragraph(Chain(List(CText("private["), EntityLink(owner), CText("]"))))) + Some(Paragraph(Chain(List(CText("private["), EntityLink(owner.qualifiedName, () => Some(owner)), CText("]"))))) case ProtectedInInstance() => Some(Paragraph(CText("protected[this]"))) case ProtectedInTemplate(owner) if (owner == mbr.inTemplate) => Some(Paragraph(CText("protected"))) case ProtectedInTemplate(owner) => - Some(Paragraph(Chain(List(CText("protected["), EntityLink(owner), CText("]"))))) + Some(Paragraph(Chain(List(CText("protected["), EntityLink(owner.qualifiedName, () => Some(owner)), CText("]"))))) case Public() => None } @@ -627,7 +669,15 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage { - val nameClass = if (mbr.byConversion.isDefined) "implicit" else "name" + val nameClass = + if (Template.isImplicit(mbr)) + if (Template.isShadowedOrAmbiguousImplicit(mbr)) + "implicit shadowed" + else + "implicit" + else + "name" + val nameHtml = { val value = if (mbr.isConstructor) tpl.name else mbr.name val span = if (mbr.deprecation.isDefined) @@ -699,8 +749,8 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage } }{ if (isReduced) NodeSeq.Empty else { mbr match { - case tpl: DocTemplateEntity if tpl.parentType.isDefined => - extends { typeToHtml(tpl.parentType.get, hasLinks) } + case tpl: DocTemplateEntity if !tpl.parentTypes.isEmpty => + extends { typeToHtml(tpl.parentTypes.map(_._2), hasLinks) } case tme: MemberEntity if (tme.isDef || tme.isVal || tme.isLazyVal || tme.isVar) => : { typeToHtml(tme.resultType, hasLinks) } @@ -870,5 +920,16 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage xml.Text(ub.typeParamName + " is a subclass of " + ub.upperBound.name + " (" + ub.typeParamName + " <: ") ++ typeToHtml(ub.upperBound, true) ++ xml.Text(")") } +} + +object Template { + + def isImplicit(mbr: MemberEntity) = mbr.byConversion.isDefined + def isShadowedImplicit(mbr: MemberEntity): Boolean = + mbr.byConversion.map(_.source.implicitsShadowing.get(mbr).map(_.isShadowed).getOrElse(false)).getOrElse(false) + def isAmbiguousImplicit(mbr: MemberEntity): Boolean = + mbr.byConversion.map(_.source.implicitsShadowing.get(mbr).map(_.isAmbiguous).getOrElse(false)).getOrElse(false) + def isShadowedOrAmbiguousImplicit(mbr: MemberEntity) = isShadowedImplicit(mbr) || isAmbiguousImplicit(mbr) + def lowerFirstLetter(s: String) = if (s.length >= 1) s.substring(0,1).toLowerCase() + s.substring(1) else s } diff --git a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala new file mode 100644 index 0000000000..16d859894f --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala @@ -0,0 +1,58 @@ +/** + * @author Vlad Ureche + */ +package scala.tools.nsc.doc +package html.page.diagram + +object DiagramStats { + + class TimeTracker(title: String) { + var totalTime: Long = 0l + var maxTime: Long = 0l + var instances: Int = 0 + + def addTime(ms: Long) = { + if (maxTime < ms) + maxTime = ms + totalTime += ms + instances += 1 + } + + def printStats(print: String => Unit) = { + if (instances == 0) + print(title + ": no stats gathered") + else { + print(" " + title) + print(" " + "=" * title.length) + print(" count: " + instances + " items") + print(" total time: " + totalTime + " ms") + print(" average time: " + (totalTime/instances) + " ms") + print(" maximum time: " + maxTime + " ms") + print("") + } + } + } + + private[this] val filterTrack = new TimeTracker("diagrams model filtering") + private[this] val modelTrack = new TimeTracker("diagrams model generation") + private[this] val dotGenTrack = new TimeTracker("dot diagram generation") + private[this] val dotRunTrack = new TimeTracker("dot process runnning") + private[this] val svgTrack = new TimeTracker("svg processing") + + def printStats(settings: Settings) = { + if (settings.docDiagramsDebug.value) { + settings.printMsg("\nDiagram generation running time breakdown:\n") + filterTrack.printStats(settings.printMsg) + modelTrack.printStats(settings.printMsg) + dotGenTrack.printStats(settings.printMsg) + dotRunTrack.printStats(settings.printMsg) + svgTrack.printStats(settings.printMsg) + } + } + + def addFilterTime(ms: Long) = filterTrack.addTime(ms) + def addModelTime(ms: Long) = modelTrack.addTime(ms) + def addDotGenerationTime(ms: Long) = dotGenTrack.addTime(ms) + def addDotRunningTime(ms: Long) = dotRunTrack.addTime(ms) + def addSvgTime(ms: Long) = svgTrack.addTime(ms) +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css index 5a1779bba5..b25f0d0b3f 100644 --- a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css @@ -333,6 +333,10 @@ div.members > ol > li:last-child { color: darkgreen; } +.signature .symbol .shadowed { + color: darkseagreen; +} + .signature .symbol .params > .implicit { font-style: italic; } @@ -802,4 +806,4 @@ div.fullcomment dl.paramcmts > dd { #mbrsel .showall span { color: #4C4C4C; font-weight: bold; -}*/ \ No newline at end of file +}*/ diff --git a/src/compiler/scala/tools/nsc/doc/model/Entity.scala b/src/compiler/scala/tools/nsc/doc/model/Entity.scala index 8c6fa53fbc..e1bbf28dc7 100644 --- a/src/compiler/scala/tools/nsc/doc/model/Entity.scala +++ b/src/compiler/scala/tools/nsc/doc/model/Entity.scala @@ -10,7 +10,7 @@ package model import scala.collection._ import comment._ - +import diagram._ /** An entity in a Scaladoc universe. Entities are declarations in the program and correspond to symbols in the * compiler. Entities model the following Scala concepts: @@ -96,6 +96,9 @@ trait TemplateEntity extends Entity { /** The self-type of this template, if it differs from the template type. */ def selfType : Option[TypeEntity] + + /** The type of this entity, with type members */ + def ownType: TypeEntity } @@ -174,6 +177,10 @@ trait MemberEntity extends Entity { /** Whether this member is abstract. */ def isAbstract: Boolean + /** If this symbol is a use case, the useCaseOf will contain the member it was derived from, containing the full + * signature and the complete parameter descriptions. */ + def useCaseOf: Option[MemberEntity] = None + /** If this member originates from an implicit conversion, we set the implicit information to the correct origin */ def byConversion: Option[ImplicitConversion] } @@ -219,8 +226,10 @@ trait DocTemplateEntity extends TemplateEntity with MemberEntity { * only if the `docsourceurl` setting has been set. */ def sourceUrl: Option[java.net.URL] - /** The direct super-type of this template. */ - def parentType: Option[TypeEntity] + /** The direct super-type of this template + e.g: {{{class A extends B[C[Int]] with D[E]}}} will have two direct parents: class B and D + NOTE: we are dropping the refinement here! */ + def parentTypes: List[(TemplateEntity, TypeEntity)] /** All class, trait and object templates which are part of this template's linearization, in lineratization order. * This template's linearization contains all of its direct and indirect super-classes and super-traits. */ @@ -230,9 +239,13 @@ trait DocTemplateEntity extends TemplateEntity with MemberEntity { * This template's linearization contains all of its direct and indirect super-types. */ def linearizationTypes: List[TypeEntity] - /**All class, trait and object templates for which this template is a direct or indirect super-class or super-trait. - * Only templates for which documentation is available in the universe (`DocTemplateEntity`) are listed. */ - def subClasses: List[DocTemplateEntity] + /** All class, trait and object templates for which this template is a direct or indirect super-class or super-trait. + * Only templates for which documentation is available in the universe (`DocTemplateEntity`) are listed. */ + def allSubClasses: List[DocTemplateEntity] + + /** All class, trait and object templates for which this template is a *direct* super-class or super-trait. + * Only templates for which documentation is available in the universe (`DocTemplateEntity`) are listed. */ + def directSubClasses: List[DocTemplateEntity] /** All members of this template. If this template is a package, only templates for which documentation is available * in the universe (`DocTemplateEntity`) are listed. */ @@ -260,6 +273,22 @@ trait DocTemplateEntity extends TemplateEntity with MemberEntity { /** The implicit conversions this template (class or trait, objects and packages are not affected) */ def conversions: List[ImplicitConversion] + + /** The shadowing information for the implicitly added members */ + def implicitsShadowing: Map[MemberEntity, ImplicitMemberShadowing] + + /** Classes that can be implcitly converted to this class */ + def incomingImplicitlyConvertedClasses: List[DocTemplateEntity] + + /** Classes to which this class can be implicitly converted to + NOTE: Some classes might not be included in the scaladoc run so they will be NoDocTemplateEntities */ + def outgoingImplicitlyConvertedClasses: List[(TemplateEntity, TypeEntity)] + + /** If this template takes place in inheritance and implicit conversion relations, it will be shown in this diagram */ + def inheritanceDiagram: Option[Diagram] + + /** If this template contains other templates, such as classes and traits, they will be shown in this diagram */ + def contentDiagram: Option[Diagram] } @@ -322,10 +351,6 @@ trait NonTemplateMemberEntity extends MemberEntity { * It corresponds to a real member, and provides a simplified, yet compatible signature for that member. */ def isUseCase: Boolean - /** If this symbol is a use case, the useCaseOf will contain the member it was derived from, containing the full - * signature and the complete parameter descriptions. */ - def useCaseOf: Option[MemberEntity] - /** Whether this member is a bridge member. A bridge member does only exist for binary compatibility reasons * and should not appear in ScalaDoc. */ def isBridge: Boolean @@ -444,6 +469,15 @@ trait ImplicitConversion { /** The result type after the conversion */ def targetType: TypeEntity + /** The result type after the conversion + * Note: not all targetTypes have a corresponding template. Examples include conversions resulting in refinement + * types. Need to check it's not option! + */ + def targetTemplate: Option[TemplateEntity] + + /** The components of the implicit conversion type parents */ + def targetTypeComponents: List[(TemplateEntity, TypeEntity)] + /** The entity for the method that performed the conversion, if it's documented (or just its name, otherwise) */ def convertorMethod: Either[MemberEntity, String] @@ -463,12 +497,30 @@ trait ImplicitConversion { def members: List[MemberEntity] } -/** A trait that encapsulates a constraint necessary for implicit conversion */ -trait Constraint { - // /** The implicit conversion during which this constraint appears */ - // def conversion: ImplicitConversion +/** Shadowing captures the information that the member is shadowed by some other members + * There are two cases of implicitly added member shadowing: + * 1) shadowing from a original class member (the class already has that member) + * in this case, it won't be possible to call the member directly, the type checker will fail attempting to adapt + * the call arguments (or if they fit it will call the original class' method) + * 2) shadowing from other possible implicit conversions () + * this will result in an ambiguous implicit converion error + */ +trait ImplicitMemberShadowing { + /** The members that shadow the current entry use .inTemplate to get to the template name */ + def shadowingMembers: List[MemberEntity] + + /** The members that ambiguate this implicit conversion + Note: for ambiguatingMembers you have the following invariant: + assert(ambiguatingMembers.foreach(_.byConversion.isDefined) */ + def ambiguatingMembers: List[MemberEntity] + + def isShadowed: Boolean = !shadowingMembers.isEmpty + def isAmbiguous: Boolean = !ambiguatingMembers.isEmpty } +/** A trait that encapsulates a constraint necessary for implicit conversion */ +trait Constraint + /** A constraint involving a type parameter which must be in scope */ trait ImplicitInScopeConstraint extends Constraint { /** The type of the implicit value required */ diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala index 9a8df1fd0e..8eb6e358b9 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala @@ -6,6 +6,8 @@ package model import comment._ +import diagram._ + import scala.collection._ import scala.util.matching.Regex @@ -17,7 +19,7 @@ import model.{ RootPackage => RootPackageEntity } /** This trait extracts all required information for documentation from compilation units */ class ModelFactory(val global: Global, val settings: doc.Settings) { - thisFactory: ModelFactory with ModelFactoryImplicitSupport with CommentFactory with TreeFactory => + thisFactory: ModelFactory with ModelFactoryImplicitSupport with DiagramFactory with CommentFactory with TreeFactory => import global._ import definitions.{ ObjectClass, NothingClass, AnyClass, AnyValClass, AnyRefClass } @@ -25,7 +27,8 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def templatesCount = docTemplatesCache.count(_._2.isDocTemplate) - droppedPackages.size - private var modelFinished = false + private var _modelFinished = false + def modelFinished: Boolean = _modelFinished private var universe: Universe = null private def dbg(msg: String) = if (sys.props contains "scala.scaladoc.debug") println(msg) @@ -50,7 +53,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { val settings = thisFactory.settings val rootPackage = modelCreation.createRootPackage } - modelFinished = true + _modelFinished = true // complete the links between model entities, everthing that couldn't have been done before universe.rootPackage.completeModel @@ -93,6 +96,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def isObject = sym.isModule && !sym.isPackage def isCaseClass = sym.isCaseClass def isRootPackage = false + def ownType = makeType(sym.tpe, this) def selfType = if (sym.thisSym eq sym) None else Some(makeType(sym.thisSym.typeOfThis, this)) def inPackageObject: Boolean = sym.owner.isModuleClass && sym.owner.sourceModule.isPackageObject } @@ -250,14 +254,26 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } else None } - def parentType = { - if (sym.isPackage || sym == AnyClass) None else { + + def parentTemplates = + if (sym.isPackage || sym == AnyClass) + List() + else + sym.tpe.parents.flatMap { tpe: Type => + val tSym = tpe.typeSymbol + if (tSym != NoSymbol) + List(makeTemplate(tSym)) + else + List() + } filter (_.isInstanceOf[DocTemplateEntity]) + + def parentTypes = + if (sym.isPackage || sym == AnyClass) List() else { val tps = sym.tpe.parents map { _.asSeenFrom(sym.thisType, sym) } - Some(makeType(RefinedType(tps, EmptyScope), inTpl)) + makeParentTypes(RefinedType(tps, EmptyScope), inTpl) } - } - protected def linearizationFromSymbol(symbol: Symbol) = { + protected def linearizationFromSymbol(symbol: Symbol): List[(TemplateEntity, TypeEntity)] = { symbol.ancestors map { ancestor => val typeEntity = makeType(symbol.info.baseType(ancestor), this) val tmplEntity = makeTemplate(ancestor) match { @@ -272,6 +288,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def linearizationTemplates = linearization map { _._1 } def linearizationTypes = linearization map { _._2 } + /* Subclass cache */ private lazy val subClassesCache = ( if (noSubclassCache(sym)) null else mutable.ListBuffer[DocTemplateEntity]() @@ -280,18 +297,40 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { if (subClassesCache != null) subClassesCache += sc } - def subClasses = if (subClassesCache == null) Nil else subClassesCache.toList + def allSubClasses = if (subClassesCache == null) Nil else subClassesCache.toList + def directSubClasses = allSubClasses.filter(_.parentTypes.map(_._1).contains(this)) + + /* Implcitly convertible class cache */ + private var implicitlyConvertibleClassesCache: mutable.ListBuffer[DocTemplateEntity] = null + def registerImplicitlyConvertibleClass(sc: DocTemplateEntity): Unit = { + if (implicitlyConvertibleClassesCache == null) + implicitlyConvertibleClassesCache = mutable.ListBuffer[DocTemplateEntity]() + implicitlyConvertibleClassesCache += sc + } + + def incomingImplicitlyConvertedClasses: List[DocTemplateEntity] = + if (implicitlyConvertibleClassesCache == null) + List() + else + implicitlyConvertibleClassesCache.toList + // the implicit conversions are generated eagerly, but the members generated by implicit conversions are added + // lazily, on completeModel val conversions: List[ImplicitConversionImpl] = if (settings.docImplicits.value) makeImplicitConversions(sym, this) else Nil + // members as given by the compiler lazy val memberSyms = sym.info.members.filter(s => membersShouldDocument(s, this)) + // the inherited templates (classes, traits or objects) var memberSymsLazy = memberSyms.filter(t => templateShouldDocument(t, this) && !inOriginalOnwer(t, this)) + // the direct members (methods, values, vars, types and directly contained templates) var memberSymsEager = memberSyms.filter(!memberSymsLazy.contains(_)) + // the members generated by the symbols in memberSymsEager + val ownMembers = (memberSyms.flatMap(makeMember(_, null, this))) - var members: List[MemberImpl] = (memberSymsEager.flatMap(makeMember(_, null, this))) ::: - (conversions.flatMap((_.members))) // also take in the members from implicit conversions + // all the members that are documentented PLUS the members inherited by implicit conversions + var members: List[MemberImpl] = ownMembers def templates = members collect { case c: DocTemplateEntity => c } def methods = members collect { case d: Def => d } @@ -299,7 +338,12 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def abstractTypes = members collect { case t: AbstractType => t } def aliasTypes = members collect { case t: AliasType => t } + /** + * This is the final point in the core model creation: no DocTemplates are created after the model has finished, but + * inherited templates and implicit members are added to the members at this point. + */ def completeModel: Unit = { + // DFS completion for (member <- members) member match { case d: DocTemplateImpl => d.completeModel @@ -310,8 +354,30 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { // compute linearization to register subclasses linearization + outgoingImplicitlyConvertedClasses + + // the members generated by the symbols in memberSymsEager PLUS the members from the usecases + val allMembers = ownMembers ::: ownMembers.flatMap(_.useCaseOf.map(_.asInstanceOf[MemberImpl])).distinct + implicitsShadowing = makeShadowingTable(allMembers, conversions, this) + // finally, add the members generated by implicit conversions + members :::= conversions.flatMap(_.memberImpls) } + var implicitsShadowing = Map[MemberEntity, ImplicitMemberShadowing]() + + lazy val outgoingImplicitlyConvertedClasses: List[(TemplateEntity, TypeEntity)] = conversions flatMap (conv => + if (!implicitExcluded(conv.conversionQualifiedName)) + conv.targetTypeComponents map { + case pair@(template, tpe) => + template match { + case d: DocTemplateImpl => d.registerImplicitlyConvertibleClass(this) + case _ => // nothing + } + pair + } + else List() + ) + override def isTemplate = true lazy val definitionName = optimize(inDefinitionTemplates.head.qualifiedName + "." + name) def isDocTemplate = true @@ -324,6 +390,10 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } case _ => None } + + // We make the diagram a lazy val, since we're not sure we'll include the diagrams in the page + lazy val inheritanceDiagram = makeInheritanceDiagram(this) + lazy val contentDiagram = makeContentDiagram(this) } abstract class PackageImpl(sym: Symbol, inTpl: PackageImpl) extends DocTemplateImpl(sym, inTpl) with Package { @@ -383,7 +453,6 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def typeParams = sym.typeParams map (makeTypeParam(_, inTpl)) } - /* ============== MAKER METHODS ============== */ /** */ @@ -540,10 +609,10 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { if (bSym == definitions.Object_synchronized) { val cSymInfo = (bSym.info: @unchecked) match { case PolyType(ts, MethodType(List(bp), mt)) => - val cp = bp.cloneSymbol.setInfo(definitions.byNameType(bp.info)) + val cp = bp.cloneSymbol.setPos(bp.pos).setInfo(definitions.byNameType(bp.info)) PolyType(ts, MethodType(List(cp), mt)) } - bSym.cloneSymbol.setInfo(cSymInfo) + bSym.cloneSymbol.setPos(bSym.pos).setInfo(cSymInfo) } else bSym } @@ -726,6 +795,20 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { makeType(tpe, inTpl) } + /** Get the types of the parents of the current class, ignoring the refinements */ + def makeParentTypes(aType: Type, inTpl: => TemplateImpl): List[(TemplateEntity, TypeEntity)] = aType match { + case RefinedType(parents, defs) => + val ignoreParents = Set[Symbol](AnyClass, ObjectClass) + val filtParents = parents filterNot (x => ignoreParents(x.typeSymbol)) + filtParents.map(parent => { + val templateEntity = makeTemplate(parent.typeSymbol) + val typeEntity = makeType(parent, inTpl) + (templateEntity, typeEntity) + }) + case _ => + List((makeTemplate(aType.typeSymbol), makeType(aType, inTpl))) + } + /** */ def makeType(aType: Type, inTpl: TemplateImpl): TypeEntity = { def templatePackage = closestPackage(inTpl.sym) @@ -902,5 +985,11 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { /** Filter '@bridge' methods only if *they don't override non-bridge methods*. See SI-5373 for details */ def isPureBridge(sym: Symbol) = sym.isBridge && sym.allOverriddenSymbols.forall(_.isBridge) + + // the classes that are excluded from the index should also be excluded from the diagrams + def classExcluded(clazz: TemplateEntity): Boolean = settings.hardcoded.isExcluded(clazz.qualifiedName) + + // the implicit conversions that are excluded from the pages should not appear in the diagram + def implicitExcluded(convertorMethod: String): Boolean = settings.hardcoded.commonConversionTargets.contains(convertorMethod) } diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala index 2a0fcea0ea..e7f4a9c79b 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala @@ -64,8 +64,8 @@ trait ModelFactoryImplicitSupport { // debugging: val DEBUG: Boolean = settings.docImplicitsDebug.value val ERROR: Boolean = true // currently we show all errors - @inline final def debug(msg: => String) = if (DEBUG) println(msg) - @inline final def error(msg: => String) = if (ERROR) println(msg) + @inline final def debug(msg: => String) = if (DEBUG) settings.printMsg(msg) + @inline final def error(msg: => String) = if (ERROR) settings.printMsg(msg) /** This is a flag that indicates whether to eliminate implicits that cannot be satisfied within the current scope. * For example, if an implicit conversion requires that there is a Numeric[T] in scope: @@ -80,74 +80,8 @@ trait ModelFactoryImplicitSupport { * - not be generated at all, since there's no Numeric[String] in scope (if ran without -implicits-show-all) * - generated with a *weird* constraint, Numeric[String] as the user might add it by hand (if flag is enabled) */ - val implicitsShowAll: Boolean = settings.docImplicitsShowAll.value class ImplicitNotFound(tpe: Type) extends Exception("No implicit of type " + tpe + " found in scope.") - /* ============== IMPLEMENTATION PROVIDING ENTITY TYPES ============== */ - - class ImplicitConversionImpl( - val sym: Symbol, - val convSym: Symbol, - val toType: Type, - val constrs: List[Constraint], - inTpl: => DocTemplateImpl) - extends ImplicitConversion { - - def source: DocTemplateEntity = inTpl - - def targetType: TypeEntity = makeType(toType, inTpl) - - def convertorOwner: TemplateEntity = makeTemplate(convSym.owner) - - def convertorMethod: Either[MemberEntity, String] = { - var convertor: MemberEntity = null - - convertorOwner match { - case doc: DocTemplateImpl => - val convertors = members.collect { case m: MemberImpl if m.sym == convSym => m } - if (convertors.length == 1) - convertor = convertors.head - case _ => - } - if (convertor ne null) - Left(convertor) - else - Right(convSym.nameString) - } - - def conversionShortName = convSym.nameString - - def conversionQualifiedName = makeQualifiedName(convSym) - - lazy val constraints: List[Constraint] = constrs - - val members: List[MemberImpl] = { - // Obtain the members inherited by the implicit conversion - var memberSyms = toType.members.filter(implicitShouldDocument(_)) - val existingMembers = sym.info.members - - // Debugging part :) - debug(sym.nameString + "\n" + "=" * sym.nameString.length()) - debug(" * conversion " + convSym + " from " + sym.tpe + " to " + toType) - - // Members inherited by implicit conversions cannot override actual members - memberSyms = memberSyms.filterNot((sym1: Symbol) => - existingMembers.exists(sym2 => sym1.name == sym2.name && - !isDistinguishableFrom(toType.memberInfo(sym1), sym.info.memberInfo(sym2)))) - - debug(" -> full type: " + toType) - if (constraints.length != 0) { - debug(" -> constraints: ") - constraints foreach { constr => debug(" - " + constr) } - } - debug(" -> members:") - memberSyms foreach (sym => debug(" - "+ sym.decodedName +" : " + sym.info)) - debug("") - - memberSyms.flatMap((makeMember(_, this, inTpl))) - } - } - /* ============== MAKER METHODS ============== */ /** @@ -166,16 +100,17 @@ trait ModelFactoryImplicitSupport { val results = global.analyzer.allViewsFrom(sym.tpe, context, sym.typeParams) var conversions = results.flatMap(result => makeImplicitConversion(sym, result._1, result._2, context, inTpl)) - conversions = conversions.filterNot(_.members.isEmpty) + // also keep empty conversions, so they appear in diagrams + // conversions = conversions.filter(!_.members.isEmpty) // Filter out specialized conversions from array if (sym == ArrayClass) - conversions = conversions.filterNot((conv: ImplicitConversion) => + conversions = conversions.filterNot((conv: ImplicitConversionImpl) => hardcoded.arraySkipConversions.contains(conv.conversionQualifiedName)) // Filter out non-sensical conversions from value types if (isPrimitiveValueType(sym.tpe)) - conversions = conversions.filter((ic: ImplicitConversion) => + conversions = conversions.filter((ic: ImplicitConversionImpl) => hardcoded.valueClassFilter(sym.nameString, ic.conversionQualifiedName)) // Put the class-specific conversions in front @@ -314,7 +249,7 @@ trait ModelFactoryImplicitSupport { available match { case Some(true) => Nil - case Some(false) if (!implicitsShowAll) => + case Some(false) if (!settings.docImplicitsShowAll.value) => // if -implicits-show-all is not set, we get rid of impossible conversions (such as Numeric[String]) throw new ImplicitNotFound(implType) case _ => @@ -399,6 +334,171 @@ trait ModelFactoryImplicitSupport { sym.ownerChain.filterNot(remove.contains(_)).reverse.map(_.nameString).mkString(".") } + /* ============== IMPLEMENTATION PROVIDING ENTITY TYPES ============== */ + + class ImplicitConversionImpl( + val sym: Symbol, + val convSym: Symbol, + val toType: Type, + val constrs: List[Constraint], + inTpl: => DocTemplateImpl) + extends ImplicitConversion { + + def source: DocTemplateEntity = inTpl + + def targetType: TypeEntity = makeType(toType, inTpl) + + def convertorOwner: TemplateEntity = + if (convSym != NoSymbol) + makeTemplate(convSym.owner) + else { + error("Scaladoc implicits: Implicit conversion from " + sym.tpe + " to " + toType + " done by " + convSym + " = NoSymbol!") + makeRootPackage + } + + def targetTemplate: Option[TemplateEntity] = toType match { + // @Vlad: I'm being extra conservative in template creation -- I don't want to create templates for complex types + // such as refinement types because the template can't represent the type corectly (a template corresponds to a + // package, class, trait or object) + case t: TypeRef => Some(makeTemplate(t.sym)) + case RefinedType(parents, decls) => None + case _ => error("Scaladoc implicits: Could not create template for: " + toType + " of type " + toType.getClass); None + } + + def targetTypeComponents: List[(TemplateEntity, TypeEntity)] = makeParentTypes(toType, inTpl) + + def convertorMethod: Either[MemberEntity, String] = { + var convertor: MemberEntity = null + + convertorOwner match { + case doc: DocTemplateImpl => + val convertors = members.collect { case m: MemberImpl if m.sym == convSym => m } + if (convertors.length == 1) + convertor = convertors.head + case _ => + } + if (convertor ne null) + Left(convertor) + else + Right(convSym.nameString) + } + + def conversionShortName = convSym.nameString + + def conversionQualifiedName = makeQualifiedName(convSym) + + lazy val constraints: List[Constraint] = constrs + + lazy val memberImpls: List[MemberImpl] = { + // Obtain the members inherited by the implicit conversion + val memberSyms = toType.members.filter(implicitShouldDocument(_)) + val existingSyms = sym.info.members + + // Debugging part :) + debug(sym.nameString + "\n" + "=" * sym.nameString.length()) + debug(" * conversion " + convSym + " from " + sym.tpe + " to " + toType) + + debug(" -> full type: " + toType) + if (constraints.length != 0) { + debug(" -> constraints: ") + constraints foreach { constr => debug(" - " + constr) } + } + debug(" -> members:") + memberSyms foreach (sym => debug(" - "+ sym.decodedName +" : " + sym.info)) + debug("") + + memberSyms.flatMap({ aSym => + makeTemplate(aSym.owner) match { + case d: DocTemplateImpl => + // we can't just pick up nodes from the previous template, although that would be very convenient: + // they need the byConversion field to be attached to themselves -- this is design decision I should + // revisit soon + // + // d.ownMembers.collect({ + // // it's either a member or has a couple of usecases it's hidden behind + // case m: MemberImpl if m.sym == aSym => + // m // the member itself + // case m: MemberImpl if m.useCaseOf.isDefined && m.useCaseOf.get.asInstanceOf[MemberImpl].sym == aSym => + // m.useCaseOf.get.asInstanceOf[MemberImpl] // the usecase + // }) + makeMember(aSym, this, d) + case _ => + // should only happen if the code for this template is not part of the scaladoc run => + // members won't have any comments + makeMember(aSym, this, inTpl) + } + }) + } + + lazy val members: List[MemberEntity] = memberImpls + } + + /* ========================= HELPER METHODS ========================== */ + /** + * Computes the shadowing table for all the members in the implicit conversions + * @param mbrs All template's members, including usecases and full signature members + * @param convs All the conversions the template takes part in + * @param inTpl the ususal :) + */ + def makeShadowingTable(mbrs: List[MemberImpl], + convs: List[ImplicitConversionImpl], + inTpl: DocTemplateImpl): Map[MemberEntity, ImplicitMemberShadowing] = { + assert(modelFinished) + + var shadowingTable = Map[MemberEntity, ImplicitMemberShadowing]() + + for (conv <- convs) { + val otherConvs = convs.filterNot(_ == conv) + + for (member <- conv.memberImpls) { + // for each member in our list + val sym1 = member.sym + val tpe1 = conv.toType.memberInfo(sym1) + + // check if it's shadowed by a member in the original class + var shadowedBySyms: List[Symbol] = List() + for (mbr <- mbrs) { + val sym2 = mbr.sym + if (sym1.name == sym2.name) { + val shadowed = !settings.docImplicitsSoundShadowing.value || { + val tpe2 = inTpl.sym.info.memberInfo(sym2) + !isDistinguishableFrom(tpe1, tpe2) + } + if (shadowed) + shadowedBySyms ::= sym2 + } + } + + val shadowedByMembers = mbrs.filter((mb: MemberImpl) => shadowedBySyms.contains(mb.sym)) + + // check if it's shadowed by another member + var ambiguousByMembers: List[MemberEntity] = List() + for (conv <- otherConvs) + for (member2 <- conv.memberImpls) { + val sym2 = member2.sym + if (sym1.name == sym2.name) { + val tpe2 = conv.toType.memberInfo(sym2) + // Ambiguity should be an equivalence relation + val ambiguated = !isDistinguishableFrom(tpe1, tpe2) || !isDistinguishableFrom(tpe2, tpe1) + if (ambiguated) + ambiguousByMembers ::= member2 + } + } + + // we finally have the shadowing info + val shadowing = new ImplicitMemberShadowing { + def shadowingMembers: List[MemberEntity] = shadowedByMembers + def ambiguatingMembers: List[MemberEntity] = ambiguousByMembers + } + + shadowingTable += (member -> shadowing) + } + } + + shadowingTable + } + + /** * uniteConstraints takes a TypeConstraint instance and simplifies the constraints inside * @@ -493,8 +593,8 @@ trait ModelFactoryImplicitSupport { // - common methods (in Any, AnyRef, Object) as they are automatically removed // - private and protected members (not accessible following an implicit conversion) // - members starting with _ (usually reserved for internal stuff) - localShouldDocument(aSym) && (!aSym.isConstructor) && (aSym.owner != ObjectClass) && - (aSym.owner != AnyClass) && (aSym.owner != AnyRefClass) && + localShouldDocument(aSym) && (!aSym.isConstructor) && (aSym.owner != AnyValClass) && + (aSym.owner != AnyClass) && (aSym.owner != ObjectClass) && (!aSym.isProtected) && (!aSym.isPrivate) && (!aSym.name.startsWith("_")) && (aSym.isMethod || aSym.isGetter || aSym.isSetter) && (aSym.nameString != "getClass") @@ -506,15 +606,18 @@ trait ModelFactoryImplicitSupport { * The trick here is that the resultType does not matter - the condition for removal it that paramss have the same * structure (A => B => C may not override (A, B) => C) and that all the types involved are * of the implcit conversion's member are subtypes of the parent members' parameters */ - def isDistinguishableFrom(t1: Type, t2: Type): Boolean = + def isDistinguishableFrom(t1: Type, t2: Type): Boolean = { + // Vlad: I tried using matches but it's not exactly what we need: + // (p: AnyRef)AnyRef matches ((t: String)AnyRef returns false -- but we want that to be true + // !(t1 matches t2) if (t1.paramss.map(_.length) == t2.paramss.map(_.length)) { for ((t1p, t2p) <- t1.paramss.flatten zip t2.paramss.flatten) - if (!isSubType(t1 memberInfo t1p, t2 memberInfo t2p)) - return true // if on the corresponding parameter you give a type that is in t1 but not in t2 - // example: - // def foo(a: Either[Int, Double]): Int = 3 - // def foo(b: Left[T1]): Int = 6 - // a.foo(Right(4.5d)) prints out 3 :) + if (!isSubType(t1 memberInfo t1p, t2 memberInfo t2p)) + return true // if on the corresponding parameter you give a type that is in t1 but not in t2 + // def foo(a: Either[Int, Double]): Int = 3 + // def foo(b: Left[T1]): Int = 6 + // a.foo(Right(4.5d)) prints out 3 :) false } else true // the member structure is different foo(3, 5) vs foo(3)(5) + } } \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala b/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala index ef4047cebf..ecc3273903 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala @@ -67,8 +67,8 @@ final case class Bold(text: Inline) extends Inline final case class Underline(text: Inline) extends Inline final case class Superscript(text: Inline) extends Inline final case class Subscript(text: Inline) extends Inline +final case class EntityLink(target: String, template: () => Option[TemplateEntity]) extends Inline final case class Link(target: String, title: Inline) extends Inline -final case class EntityLink(target: TemplateEntity) extends Inline final case class Monospace(text: Inline) extends Inline final case class Text(text: String) extends Inline final case class HtmlTag(data: String) extends Inline { diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala b/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala index 914275dd8d..7b70683db5 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala @@ -108,6 +108,12 @@ abstract class Comment { /** A description for the primary constructor */ def constructor: Option[Body] + /** A set of diagram directives for the inheritance diagram */ + def inheritDiagram: List[String] + + /** A set of diagram directives for the content diagram */ + def contentDiagram: List[String] + override def toString = body.toString + "\n" + (authors map ("@author " + _.toString)).mkString("\n") + diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala index c749d30267..a46be37d60 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala @@ -97,37 +97,41 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => /* Creates comments with necessary arguments */ def createComment ( - body0: Option[Body] = None, - authors0: List[Body] = List.empty, - see0: List[Body] = List.empty, - result0: Option[Body] = None, - throws0: Map[String,Body] = Map.empty, - valueParams0: Map[String,Body] = Map.empty, - typeParams0: Map[String,Body] = Map.empty, - version0: Option[Body] = None, - since0: Option[Body] = None, - todo0: List[Body] = List.empty, - deprecated0: Option[Body] = None, - note0: List[Body] = List.empty, - example0: List[Body] = List.empty, - constructor0: Option[Body] = None, - source0: Option[String] = None + body0: Option[Body] = None, + authors0: List[Body] = List.empty, + see0: List[Body] = List.empty, + result0: Option[Body] = None, + throws0: Map[String,Body] = Map.empty, + valueParams0: Map[String,Body] = Map.empty, + typeParams0: Map[String,Body] = Map.empty, + version0: Option[Body] = None, + since0: Option[Body] = None, + todo0: List[Body] = List.empty, + deprecated0: Option[Body] = None, + note0: List[Body] = List.empty, + example0: List[Body] = List.empty, + constructor0: Option[Body] = None, + source0: Option[String] = None, + inheritDiagram0: List[String] = List.empty, + contentDiagram0: List[String] = List.empty ) : Comment = new Comment{ - val body = if(body0 isDefined) body0.get else Body(Seq.empty) - val authors = authors0 - val see = see0 - val result = result0 - val throws = throws0 - val valueParams = valueParams0 - val typeParams = typeParams0 - val version = version0 - val since = since0 - val todo = todo0 - val deprecated = deprecated0 - val note = note0 - val example = example0 - val constructor = constructor0 - val source = source0 + val body = if(body0 isDefined) body0.get else Body(Seq.empty) + val authors = authors0 + val see = see0 + val result = result0 + val throws = throws0 + val valueParams = valueParams0 + val typeParams = typeParams0 + val version = version0 + val since = since0 + val todo = todo0 + val deprecated = deprecated0 + val note = note0 + val example = example0 + val constructor = constructor0 + val source = source0 + val inheritDiagram = inheritDiagram0 + val contentDiagram = contentDiagram0 } protected val endOfText = '\u0003' @@ -186,6 +190,10 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => protected val safeTagMarker = '\u000E' + /** A Scaladoc tag not linked to a symbol and not followed by text */ + protected val SingleTag = + new Regex("""\s*@(\S+)\s*""") + /** A Scaladoc tag not linked to a symbol. Returns the name of the tag, and the rest of the line. */ protected val SimpleTag = new Regex("""\s*@(\S+)\s+(.*)""") @@ -306,6 +314,11 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => val value = body :: tags.getOrElse(key, Nil) parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + case SingleTag(name) :: ls if (!inCodeBlock) => + val key = SimpleTagKey(name) + val value = "" :: tags.getOrElse(key, Nil) + parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + case line :: ls if (lastTagKey.isDefined) => val key = lastTagKey.get val value = @@ -321,9 +334,24 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => parse0(docBody, tags, lastTagKey, ls, inCodeBlock) case Nil => + // Take the {inheritance, content} diagram keys aside, as it doesn't need any parsing + val inheritDiagramTag = SimpleTagKey("inheritanceDiagram") + val contentDiagramTag = SimpleTagKey("contentDiagram") + + val inheritDiagramText: List[String] = tags.get(inheritDiagramTag) match { + case Some(list) => list + case None => List.empty + } + + val contentDiagramText: List[String] = tags.get(contentDiagramTag) match { + case Some(list) => list + case None => List.empty + } + + val tagsWithoutDiagram = tags.filterNot(pair => pair._1 == inheritDiagramTag || pair._1 == contentDiagramTag) val bodyTags: mutable.Map[TagKey, List[Body]] = - mutable.Map(tags mapValues {tag => tag map (parseWiki(_, pos))} toSeq: _*) + mutable.Map(tagsWithoutDiagram mapValues {tag => tag map (parseWiki(_, pos))} toSeq: _*) def oneTag(key: SimpleTagKey): Option[Body] = ((bodyTags remove key): @unchecked) match { @@ -356,21 +384,23 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => } val com = createComment ( - body0 = Some(parseWiki(docBody.toString, pos)), - authors0 = allTags(SimpleTagKey("author")), - see0 = allTags(SimpleTagKey("see")), - result0 = oneTag(SimpleTagKey("return")), - throws0 = allSymsOneTag(SimpleTagKey("throws")), - valueParams0 = allSymsOneTag(SimpleTagKey("param")), - typeParams0 = allSymsOneTag(SimpleTagKey("tparam")), - version0 = oneTag(SimpleTagKey("version")), - since0 = oneTag(SimpleTagKey("since")), - todo0 = allTags(SimpleTagKey("todo")), - deprecated0 = oneTag(SimpleTagKey("deprecated")), - note0 = allTags(SimpleTagKey("note")), - example0 = allTags(SimpleTagKey("example")), - constructor0 = oneTag(SimpleTagKey("constructor")), - source0 = Some(clean(src).mkString("\n")) + body0 = Some(parseWiki(docBody.toString, pos)), + authors0 = allTags(SimpleTagKey("author")), + see0 = allTags(SimpleTagKey("see")), + result0 = oneTag(SimpleTagKey("return")), + throws0 = allSymsOneTag(SimpleTagKey("throws")), + valueParams0 = allSymsOneTag(SimpleTagKey("param")), + typeParams0 = allSymsOneTag(SimpleTagKey("tparam")), + version0 = oneTag(SimpleTagKey("version")), + since0 = oneTag(SimpleTagKey("since")), + todo0 = allTags(SimpleTagKey("todo")), + deprecated0 = oneTag(SimpleTagKey("deprecated")), + note0 = allTags(SimpleTagKey("note")), + example0 = allTags(SimpleTagKey("example")), + constructor0 = oneTag(SimpleTagKey("constructor")), + source0 = Some(clean(src).mkString("\n")), + inheritDiagram0 = inheritDiagramText, + contentDiagram0 = contentDiagramText ) for ((key, _) <- bodyTags) @@ -686,13 +716,6 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => ) } - def entityLink(query: String): Inline = findTemplate(query) match { - case Some(tpl) => - EntityLink(tpl) - case None => - Text(query) - } - def link(): Inline = { val SchemeUri = """([^:]+:.*)""".r jump("[[") @@ -717,7 +740,8 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => if (!qualName.contains(".") && !definitions.packageExists(qualName)) reportError(pos, "entity link to " + qualName + " should be a fully qualified name") - entityLink(qualName) + // move the template resolution as late as possible + EntityLink(qualName, () => findTemplate(qualName)) } } diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala new file mode 100644 index 0000000000..28a8c7d37d --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala @@ -0,0 +1,143 @@ +package scala.tools.nsc.doc +package model +package diagram + +import model._ + +/** + * The diagram base classes + * + * @author Damien Obrist + * @author Vlad Ureche + */ +abstract class Diagram { + def nodes: List[Node] + def edges: List[(Node, List[Node])] + def isPackageDiagram = false + def isClassDiagram = false + def depthInfo: DepthInfo +} + +case class PackageDiagram(nodes:List[/*Class*/Node], edges:List[(Node, List[Node])]) extends Diagram { + override def isPackageDiagram = true + lazy val depthInfo = new PackageDiagramDepth(this) +} + +/** A class diagram */ +case class ClassDiagram(thisNode: ThisNode, + superClasses: List[/*Class*/Node], + subClasses: List[/*Class*/Node], + incomingImplicits: List[ImplicitNode], + outgoingImplicits: List[ImplicitNode]) extends Diagram { + def nodes = thisNode :: superClasses ::: subClasses ::: incomingImplicits ::: outgoingImplicits + def edges = (thisNode -> (superClasses ::: outgoingImplicits)) :: + (subClasses ::: incomingImplicits).map(_ -> List(thisNode)) + + override def isClassDiagram = true + lazy val depthInfo = new DepthInfo { + def maxDepth = 3 + def nodeDepth(node: Node) = + if (node == thisNode) 1 + else if (superClasses.contains(node)) 0 + else if (subClasses.contains(node)) 2 + else if (incomingImplicits.contains(node) || outgoingImplicits.contains(node)) 1 + else -1 + } +} + +trait DepthInfo { + /** Gives the maximum depth */ + def maxDepth: Int + /** Gives the depth of any node in the diagram or -1 if the node is not in the diagram */ + def nodeDepth(node: Node): Int +} + +abstract class Node { + def name = tpe.name + def tpe: TypeEntity + def tpl: Option[TemplateEntity] + /** shortcut to get a DocTemplateEntity */ + def doctpl: Option[DocTemplateEntity] = tpl match { + case Some(tpl) => tpl match { + case d: DocTemplateEntity => Some(d) + case _ => None + } + case _ => None + } + /* shortcuts to find the node type without matching */ + def isThisNode = false + def isNormalNode = false + def isClassNode = if (tpl.isDefined) (tpl.get.isClass || tpl.get.qualifiedName == "scala.AnyRef") else false + def isTraitNode = if (tpl.isDefined) tpl.get.isTrait else false + def isObjectNode= if (tpl.isDefined) tpl.get.isObject else false + def isOtherNode = !(isClassNode || isTraitNode || isObjectNode) + def isImplicitNode = false + def isOutsideNode = false +} + +// different matchers, allowing you to use the pattern matcher against any node +// NOTE: A ThisNode or ImplicitNode can at the same time be ClassNode/TraitNode/OtherNode, not exactly according to +// case class specification -- thus a complete match would be: +// node match { +// case ThisNode(tpe, _) => /* case for this node, you can still use .isClass, .isTrait and .isOther */ +// case ImplicitNode(tpe, _) => /* case for an implicit node, you can still use .isClass, .isTrait and .isOther */ +// case _ => node match { +// case ClassNode(tpe, _) => /* case for a non-this, non-implicit Class node */ +// case TraitNode(tpe, _) => /* case for a non-this, non-implicit Trait node */ +// case OtherNode(tpe, _) => /* case for a non-this, non-implicit Other node */ +// } +// } +object Node { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEntity])] = Some((n.tpe, n.tpl)) } +object ClassNode { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEntity])] = if (n.isClassNode) Some((n.tpe, n.tpl)) else None } +object TraitNode { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEntity])] = if (n.isTraitNode) Some((n.tpe, n.tpl)) else None } +object ObjectNode { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEntity])] = if (n.isObjectNode) Some((n.tpe, n.tpl)) else None } +object OutsideNode { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEntity])] = if (n.isOutsideNode) Some((n.tpe, n.tpl)) else None } +object OtherNode { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEntity])] = if (n.isOtherNode) Some((n.tpe, n.tpl)) else None } + + + +/** The node for the current class */ +case class ThisNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isThisNode = true } + +/** The usual node */ +case class NormalNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isNormalNode = true } + +/** A class or trait the thisnode can be converted to by an implicit conversion + * TODO: I think it makes more sense to use the tpe links to templates instead of the TemplateEntity for implicit nodes + * since some implicit conversions convert the class to complex types that cannot be represented as a single tmeplate + */ +case class ImplicitNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isImplicitNode = true } + +/** An outside node is shown in packages when a class from a different package makes it to the package diagram due to + * its relation to a class in the package (and @contentDiagram showInheritedNodes annotation) */ +case class OutsideNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isOutsideNode = true } + + +// Computing and offering node depth information +class PackageDiagramDepth(pack: PackageDiagram) extends DepthInfo { + private[this] var _maxDepth = 0 + private[this] var _nodeDepth = Map[Node, Int]() + private[this] var seedNodes = Set[Node]() + private[this] val invertedEdges: Map[Node, List[Node]] = + pack.edges.flatMap({case (node: Node, outgoing: List[Node]) => outgoing.map((_, node))}).groupBy(_._1).map({case (k, values) => (k, values.map(_._2))}).withDefaultValue(Nil) + private[this] val directEdges: Map[Node, List[Node]] = pack.edges.toMap.withDefaultValue(Nil) + + // seed base nodes, to minimize noise - they can't all have parents, else there would only be cycles + seedNodes ++= pack.nodes.filter(directEdges(_).isEmpty) + + while (!seedNodes.isEmpty) { + var newSeedNodes = Set[Node]() + for (node <- seedNodes) { + val depth = 1 + (-1 :: directEdges(node).map(_nodeDepth.getOrElse(_, -1))).max + if (depth != _nodeDepth.getOrElse(node, -1)) { + _nodeDepth += (node -> depth) + newSeedNodes ++= invertedEdges(node) + if (depth > _maxDepth) _maxDepth = depth + } + } + seedNodes = newSeedNodes + } + + val maxDepth = _maxDepth + def nodeDepth(node: Node) = _nodeDepth.getOrElse(node, -1) +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala new file mode 100644 index 0000000000..beaa045df4 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala @@ -0,0 +1,248 @@ +package scala.tools.nsc.doc +package model +package diagram + +import model._ +import comment.CommentFactory +import java.util.regex.{Pattern, Matcher} +import scala.util.matching.Regex + +// statistics +import html.page.diagram.DiagramStats + +/** + * This trait takes care of parsing @{inheritance, content}Diagram annotations + * + * @author Damien Obrist + * @author Vlad Ureche + */ +trait DiagramDirectiveParser { + this: ModelFactory with DiagramFactory with CommentFactory with TreeFactory => + + ///// DIAGRAM FILTERS ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * The DiagramFilter trait directs the diagram engine about the way the diagram should be displayed + * + * Vlad: There's an explanation I owe to people using diagrams and not finding a way to hide a specific class from + * all diagrams at once. So why did I choose to allow you to only control the diagrams at class level? So, the + * reason is you would break the separate scaladoc compilation: + * If you have an "@diagram hideMyClass" annotation in class A and you run scaladoc on it along with its subclass B + * A will not appear in B's diagram. But if you scaladoc only on B, A's comment will not be parsed and the + * instructions to hide class A from all diagrams will not be available. Thus I prefer to force you to control the + * diagrams of each class locally. The problem does not appear with scalac, as scalac stores all its necessary + * information (like scala signatures) serialized in the .class file. But we couldn't store doc comments in the class + * file, could we? (Turns out we could, but that's another story) + * + * Any flaming for this decision should go to scala-internals@googlegroups.com + */ + trait DiagramFilter { + /** A flag to hide the diagram completely */ + def hideDiagram: Boolean + /** Hide incoming implicit conversions (for type hierarchy diagrams) */ + def hideIncomingImplicits: Boolean + /** Hide outgoing implicit conversions (for type hierarchy diagrams) */ + def hideOutgoingImplicits: Boolean + /** Hide superclasses (for type hierarchy diagrams) */ + def hideSuperclasses: Boolean + /** Hide subclasses (for type hierarchy diagrams) */ + def hideSubclasses: Boolean + /** Show related classes from other objects/traits/packages (for content diagrams) */ + def showInheritedNodes: Boolean + /** Hide a node from the diagram */ + def hideNode(clazz: TemplateEntity): Boolean + /** Hide an edge from the diagram */ + def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean + } + + /** Main entry point into this trait: generate the filter for inheritance diagrams */ + def makeInheritanceDiagramFilter(template: DocTemplateImpl): DiagramFilter = { + val defaultFilter = if (template.isClass || template.isTrait) FullDiagram else NoDiagramAtAll + if (template.comment.isDefined) + makeDiagramFilter(template, template.comment.get.inheritDiagram, defaultFilter, true) + else + defaultFilter + } + + /** Main entry point into this trait: generate the filter for content diagrams */ + def makeContentDiagramFilter(template: DocTemplateImpl): DiagramFilter = { + val defaultFilter = if (template.isPackage || template.isObject) FullDiagram else NoDiagramAtAll + if (template.comment.isDefined) + makeDiagramFilter(template, template.comment.get.contentDiagram, defaultFilter, false) + else + defaultFilter + } + + protected var tFilter = 0l + protected var tModel = 0l + + /** Show the entire diagram, no filtering */ + case object FullDiagram extends DiagramFilter { + val hideDiagram: Boolean = false + val hideIncomingImplicits: Boolean = false + val hideOutgoingImplicits: Boolean = false + val hideSuperclasses: Boolean = false + val hideSubclasses: Boolean = false + val showInheritedNodes: Boolean = false + def hideNode(clazz: TemplateEntity): Boolean = false + def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean = false + } + + /** Hide the diagram completely, no need for special filtering */ + case object NoDiagramAtAll extends DiagramFilter { + val hideDiagram: Boolean = true + val hideIncomingImplicits: Boolean = true + val hideOutgoingImplicits: Boolean = true + val hideSuperclasses: Boolean = true + val hideSubclasses: Boolean = true + val showInheritedNodes: Boolean = false + def hideNode(clazz: TemplateEntity): Boolean = true + def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean = true + } + + /** The AnnotationDiagramFilter trait directs the diagram engine according to an annotation + * TODO: Should document the annotation, for now see parseDiagramAnnotation in ModelFactory.scala */ + case class AnnotationDiagramFilter(hideDiagram: Boolean, + hideIncomingImplicits: Boolean, + hideOutgoingImplicits: Boolean, + hideSuperclasses: Boolean, + hideSubclasses: Boolean, + showInheritedNodes: Boolean, + hideNodesFilter: List[Pattern], + hideEdgesFilter: List[(Pattern, Pattern)]) extends DiagramFilter { + + def hideNode(clazz: TemplateEntity): Boolean = { + val qualifiedName = clazz.qualifiedName + for (hideFilter <- hideNodesFilter) + if (hideFilter.matcher(qualifiedName).matches) { + // println(hideFilter + ".matcher(" + qualifiedName + ").matches = " + hideFilter.matcher(qualifiedName).matches) + return true + } + false + } + + def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean = { + val clazz1Name = clazz1.qualifiedName + val clazz2Name = clazz2.qualifiedName + for ((clazz1Filter, clazz2Filter) <- hideEdgesFilter) { + if (clazz1Filter.matcher(clazz1Name).matches && + clazz2Filter.matcher(clazz2Name).matches) { + // println(clazz1Filter + ".matcher(" + clazz1Name + ").matches = " + clazz1Filter.matcher(clazz1Name).matches) + // println(clazz2Filter + ".matcher(" + clazz2Name + ").matches = " + clazz2Filter.matcher(clazz2Name).matches) + return true + } + } + false + } + } + + // TODO: This could certainly be improved -- right now the only regex is *, but there's no way to match a single identifier + private val NodeSpecRegex = "\\\"[A-Za-z\\*][A-Za-z\\.\\*]*\\\"" + private val NodeSpecPattern = Pattern.compile(NodeSpecRegex) + private val EdgeSpecRegex = "\\(" + NodeSpecRegex + "\\s*\\->\\s*" + NodeSpecRegex + "\\)" + private val EdgeSpecPattern = Pattern.compile(NodeSpecRegex) + // And the composed regexes: + private val HideNodesRegex = new Regex("^hideNodes(\\s*" + NodeSpecRegex + ")+$") + private val HideEdgesRegex = new Regex("^hideEdges(\\s*" + EdgeSpecRegex + ")+$") + + private def makeDiagramFilter(template: DocTemplateImpl, + directives: List[String], + defaultFilter: DiagramFilter, + isInheritanceDiagram: Boolean): DiagramFilter = directives match { + + // if there are no specific diagram directives, return the default filter (either FullDiagram or NoDiagramAtAll) + case Nil => + defaultFilter + + // compute the exact filters. By including the annotation, the diagram is autmatically added + case _ => + tFilter -= System.currentTimeMillis + var hideDiagram0: Boolean = false + var hideIncomingImplicits0: Boolean = false + var hideOutgoingImplicits0: Boolean = false + var hideSuperclasses0: Boolean = false + var hideSubclasses0: Boolean = false + var showInheritedNodes0: Boolean = false + var hideNodesFilter0: List[Pattern] = Nil + var hideEdgesFilter0: List[(Pattern, Pattern)] = Nil + + def warning(message: String) = { + // we need the position from the package object (well, ideally its comment, but yeah ...) + val sym = if (template.sym.isPackage) template.sym.info.member(global.nme.PACKAGE) else template.sym + assert((sym != global.NoSymbol) || (sym == global.definitions.RootPackage)) + global.reporter.warning(sym.pos, message) + } + + def preparePattern(className: String) = + "^" + className.stripPrefix("\"").stripSuffix("\"").replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$" + + // separate entries: + val entries = directives.foldRight("")(_ + " " + _).split(",").map(_.trim) + for (entry <- entries) + entry match { + case "hideDiagram" => + hideDiagram0 = true + case "hideIncomingImplicits" if isInheritanceDiagram => + hideIncomingImplicits0 = true + case "hideOutgoingImplicits" if isInheritanceDiagram => + hideOutgoingImplicits0 = true + case "hideSuperclasses" if isInheritanceDiagram => + hideSuperclasses0 = true + case "hideSubclasses" if isInheritanceDiagram => + hideSubclasses0 = true + case "showInheritedNodes" if !isInheritanceDiagram => + showInheritedNodes0 = true + case HideNodesRegex(last) => + val matcher = NodeSpecPattern.matcher(entry) + while (matcher.find()) { + val classPattern = Pattern.compile(preparePattern(matcher.group())) + hideNodesFilter0 ::= classPattern + } + case HideEdgesRegex(last) => + val matcher = NodeSpecPattern.matcher(entry) + while (matcher.find()) { + val class1Pattern = Pattern.compile(preparePattern(matcher.group())) + assert(matcher.find()) // it's got to be there, just matched it! + val class2Pattern = Pattern.compile(preparePattern(matcher.group())) + hideEdgesFilter0 ::= ((class1Pattern, class2Pattern)) + } + case "" => + // don't need to do anything about it + case _ => + warning("Could not understand diagram annotation in " + template.kind + " " + template.qualifiedName + + ": unmatched entry \"" + entry + "\".\n" + + " This could be because:\n" + + " - you forgot to separate entries by commas\n" + + " - you used a tag that is not allowed in the current context (like @contentDiagram hideSuperclasses)\n"+ + " - you did not use one of the allowed tags (see docs.scala-lang.org for scaladoc annotations)") + } + val result = + if (hideDiagram0) + NoDiagramAtAll + else if ((hideNodesFilter0.isEmpty) && + (hideEdgesFilter0.isEmpty) && + (hideIncomingImplicits0 == false) && + (hideOutgoingImplicits0 == false) && + (hideSuperclasses0 == false) && + (hideSubclasses0 == false) && + (showInheritedNodes0 == false) && + (hideDiagram0 == false)) + FullDiagram + else + AnnotationDiagramFilter( + hideDiagram = hideDiagram0, + hideIncomingImplicits = hideIncomingImplicits0, + hideOutgoingImplicits = hideOutgoingImplicits0, + hideSuperclasses = hideSuperclasses0, + hideSubclasses = hideSubclasses0, + showInheritedNodes = showInheritedNodes0, + hideNodesFilter = hideNodesFilter0, + hideEdgesFilter = hideEdgesFilter0) + + if (settings.docDiagramsDebug.value && result != NoDiagramAtAll && result != FullDiagram) + settings.printMsg(template.kind + " " + template.qualifiedName + " filter: " + result) + tFilter += System.currentTimeMillis + + result + } +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala new file mode 100644 index 0000000000..4ae5e7a5cb --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala @@ -0,0 +1,197 @@ +package scala.tools.nsc.doc +package model +package diagram + +import model._ +import comment.CommentFactory +import collection.mutable + +// statistics +import html.page.diagram.DiagramStats + +/** + * This trait takes care of generating the diagram for classes and packages + * + * @author Damien Obrist + * @author Vlad Ureche + */ +trait DiagramFactory extends DiagramDirectiveParser { + this: ModelFactory with DiagramFactory with CommentFactory with TreeFactory => + + /** Create the inheritance diagram for this template */ + def makeInheritanceDiagram(tpl: DocTemplateImpl): Option[Diagram] = { + + tFilter = 0 + tModel = -System.currentTimeMillis + + // the diagram filter + val diagramFilter = makeInheritanceDiagramFilter(tpl) + + val result = + if (diagramFilter == NoDiagramAtAll) + None + else { + // the main node + val thisNode = ThisNode(tpl.ownType, Some(tpl)) + + // superclasses + var superclasses = List[Node]() + tpl.parentTypes.collect { case p: (TemplateEntity, TypeEntity) if !classExcluded(p._1) => p } foreach { + t: (TemplateEntity, TypeEntity) => + val n = NormalNode(t._2, Some(t._1)) + superclasses ::= n + } + val filteredSuperclasses = if (diagramFilter.hideSuperclasses) Nil else superclasses + + // incoming implcit conversions + lazy val incomingImplicitNodes = tpl.incomingImplicitlyConvertedClasses.map(tpl => ImplicitNode(tpl.ownType, Some(tpl))) + val filteredIncomingImplicits = if (diagramFilter.hideIncomingImplicits) Nil else incomingImplicitNodes + + // subclasses + val subclasses = tpl.directSubClasses.flatMap { + case d: TemplateEntity if !classExcluded(d) => List(NormalNode(d.ownType, Some(d))) + case _ => Nil + } + val filteredSubclasses = if (diagramFilter.hideSubclasses) Nil else subclasses + + // outgoing implicit coversions + lazy val implicitNodes = tpl.outgoingImplicitlyConvertedClasses.map(pair => ImplicitNode(pair._2, Some(pair._1))) + val filteredImplicitOutgoingNodes = if (diagramFilter.hideOutgoingImplicits) Nil else implicitNodes + + // final diagram filter + filterDiagram(ClassDiagram(thisNode, filteredSuperclasses.reverse, filteredSubclasses.reverse, filteredIncomingImplicits, filteredImplicitOutgoingNodes), diagramFilter) + } + + tModel += System.currentTimeMillis + DiagramStats.addFilterTime(tFilter) + DiagramStats.addModelTime(tModel-tFilter) + + result + } + + /** Create the content diagram for this template */ + def makeContentDiagram(pack: DocTemplateImpl): Option[Diagram] = { + + tFilter = 0 + tModel = -System.currentTimeMillis + + // the diagram filter + val diagramFilter = makeContentDiagramFilter(pack) + + val result = + if (diagramFilter == NoDiagramAtAll) + None + else { + var mapNodes = Map[DocTemplateEntity, Node]() + var nodesShown = Set[DocTemplateEntity]() + var edgesAll = List[(DocTemplateEntity, List[DocTemplateEntity])]() + + // classes is the entire set of classes and traits in the package, they are the superset of nodes in the diagram + // we collect classes, traits and objects without a companion, which are usually used as values(e.g. scala.None) + val dnodes = pack.members collect { + case d: DocTemplateEntity if d.isClass || d.isTrait || (d.isObject && !d.companion.isDefined) && + ((d.inTemplate == pack) || diagramFilter.showInheritedNodes) => d + } + + // for each node, add its subclasses + for (node <- dnodes if !classExcluded(node)) { + val superClasses = node.parentTypes.collect { + case (tpl: DocTemplateEntity, tpe) if tpl.inTemplate == pack && !classExcluded(tpl) => tpl + case (tpl: DocTemplateEntity, tpe) if tpl.inTemplate != pack && !classExcluded(tpl) && diagramFilter.showInheritedNodes && (pack.members contains tpl) => tpl + } + + if (!superClasses.isEmpty) { + nodesShown += node + nodesShown ++= superClasses + } + + edgesAll ::= node -> superClasses + mapNodes += node -> (if (node.inTemplate == pack) NormalNode(node.ownType, Some(node)) else OutsideNode(node.ownType, Some(node))) + } + + if (nodesShown.isEmpty) + None + else { + val nodes = dnodes.filter(nodesShown.contains(_)).map(mapNodes(_)) + val edges = edgesAll.map(pair => (mapNodes(pair._1), pair._2.map(mapNodes(_)))).filterNot(pair => pair._2.isEmpty) + filterDiagram(PackageDiagram(nodes, edges), diagramFilter) + } + } + + tModel += System.currentTimeMillis + DiagramStats.addFilterTime(tFilter) + DiagramStats.addModelTime(tModel-tFilter) + + result + } + + /** Diagram filtering logic */ + private def filterDiagram(diagram: Diagram, diagramFilter: DiagramFilter): Option[Diagram] = { + tFilter -= System.currentTimeMillis + + val result = + if (diagramFilter == FullDiagram) + Some(diagram) + else if (diagramFilter == NoDiagramAtAll) + None + else { + // Final diagram, with the filtered nodes and edges + diagram match { + case ClassDiagram(thisNode, _, _, _, _) if diagramFilter.hideNode(thisNode.tpl.get) => + None + + case ClassDiagram(thisNode, superClasses, subClasses, incomingImplicits, outgoingImplicits) => + + def hideIncoming(node: Node): Boolean = + if (node.tpl.isDefined) diagramFilter.hideNode(node.tpl.get) || diagramFilter.hideEdge(node.tpl.get, thisNode.tpl.get) + else false // hopefully we won't need to fallback here + + def hideOutgoing(node: Node): Boolean = + if (node.tpl.isDefined) diagramFilter.hideNode(node.tpl.get) || diagramFilter.hideEdge(thisNode.tpl.get, node.tpl.get) + else false // hopefully we won't need to fallback here + + // println(thisNode) + // println(superClasses.map(cl => "super: " + cl + " " + hideOutgoing(cl)).mkString("\n")) + // println(subClasses.map(cl => "sub: " + cl + " " + hideIncoming(cl)).mkString("\n")) + Some(ClassDiagram(thisNode, + superClasses.filterNot(hideOutgoing(_)), + subClasses.filterNot(hideIncoming(_)), + incomingImplicits.filterNot(hideIncoming(_)), + outgoingImplicits.filterNot(hideOutgoing(_)))) + + case PackageDiagram(nodes0, edges0) => + // Filter out all edges that: + // (1) are sources of hidden classes + // (2) are manually hidden by the user + // (3) are destinations of hidden classes + val edges: List[(Node, List[Node])] = + diagram.edges.flatMap({ + case (source@Node(_, Some(tpl1)), dests) if !diagramFilter.hideNode(tpl1) => + val dests2 = dests.collect({ case node@Node(_, Some(tpl2)) if (!(diagramFilter.hideEdge(tpl1, tpl2) || diagramFilter.hideNode(tpl2))) => node }) + if (dests2 != Nil) + List((source, dests2)) + else + Nil + case _ => Nil + }) + + // Only show the the non-isolated nodes + // TODO: Decide if we really want to hide package members, I'm not sure that's a good idea (!!!) + // TODO: Does .distinct cause any stability issues? + val sourceNodes = edges.map(_._1) + val sinkNodes = edges.map(_._2).flatten + val nodes = (sourceNodes ::: sinkNodes).distinct + Some(PackageDiagram(nodes, edges)) + } + } + + tFilter += System.currentTimeMillis + + // eliminate all empty diagrams + if (result.isDefined && result.get.edges.forall(_._2.isEmpty)) + None + else + result + } + +} diff --git a/src/partest/scala/tools/partest/ScaladocModelTest.scala b/src/partest/scala/tools/partest/ScaladocModelTest.scala index 142f2baea5..de5354d4a0 100644 --- a/src/partest/scala/tools/partest/ScaladocModelTest.scala +++ b/src/partest/scala/tools/partest/ScaladocModelTest.scala @@ -81,9 +81,9 @@ abstract class ScaladocModelTest extends DirectTest { private[this] var settings: Settings = null // create a new scaladoc compiler - def newDocFactory: DocFactory = { + private[this] def newDocFactory: DocFactory = { settings = new Settings(_ => ()) - settings.reportModel = false // yaay, no more "model contains X documentable templates"! + settings.scaladocQuietRun = true // yaay, no more "model contains X documentable templates"! val args = extraSettings + " " + scaladocSettings val command = new ScalaDoc.Command((CommandLineParser tokenize (args)), settings) val docFact = new DocFactory(new ConsoleReporter(settings), settings) diff --git a/test/scaladoc/resources/implicits-ambiguating-res.scala b/test/scaladoc/resources/implicits-ambiguating-res.scala new file mode 100644 index 0000000000..6ed51366cb --- /dev/null +++ b/test/scaladoc/resources/implicits-ambiguating-res.scala @@ -0,0 +1,72 @@ +/** + * Test scaladoc implicits distinguishing -- supress all members by implicit conversion that are shadowed by the + * class' own members + * + * {{{ + * scala> class A { def foo(t: String) = 4 } + * defined class A + * + * scala> class B { def foo(t: Any) = 5 } + * defined class B + * + * scala> implicit def AtoB(a:A) = new B + * AtoB: (a: A)B + * + * scala> val a = new A + * a: A = A@28f553e3 + * + * scala> a.foo("T") + * res1: Int = 4 + * + * scala> a.foo(4) + * res2: Int = 5 + * }}} + */ +package scala.test.scaladoc.implicits.ambiguating +import language.implicitConversions // according to SIP18 + +/** - conv1-5 should be ambiguous + * - conv6-7 should not be ambiguous + * - conv8 should be ambiguous + * - conv9 should be ambiguous + * - conv10 and conv11 should not be ambiguous */ +class A[T] +/** conv1-9 should be the same, conv10 should be ambiguous, conv11 should be okay */ +class B extends A[Int] +/** conv1-9 should be the same, conv10 and conv11 should not be ambiguous */ +class C extends A[Double] + /** conv1-9 should be the same, conv10 should not be ambiguous while conv11 should be ambiguous */ +class D extends A[AnyRef] + +class X[T] { + def conv1: AnyRef = ??? + def conv2: T = ??? + def conv3(l: Int): AnyRef = ??? + def conv4(l: AnyRef): AnyRef = ??? + def conv5(l: AnyRef): String = ??? + def conv6(l: String)(m: String): AnyRef = ??? + def conv7(l: AnyRef)(m: AnyRef): AnyRef = ??? + def conv8(l: AnyRef): AnyRef = ??? + def conv9(l: String): AnyRef = ??? + def conv10(l: T): T = ??? + def conv11(l: T): T = ??? +} + +class Z[T] { + def conv1: AnyRef = ??? + def conv2: T = ??? + def conv3(p: Int): AnyRef = ??? + def conv4(p: AnyRef): String = ??? + def conv5(p: AnyRef): AnyRef = ??? + def conv6(p: String, q: String): AnyRef = ??? + def conv7(p: AnyRef, q: AnyRef): AnyRef = ??? + def conv8(p: String): AnyRef = ??? + def conv9(p: AnyRef): AnyRef = ??? + def conv10(p: Int): T = ??? + def conv11(p: String): T = ??? +} + +object A { + implicit def AtoX[T](a: A[T]) = new X[T] + implicit def AtoZ[T](a: A[T]) = new Z[T] +} diff --git a/test/scaladoc/resources/implicits-base-res.scala b/test/scaladoc/resources/implicits-base-res.scala index 65d7bdf67c..d6c0332c10 100644 --- a/test/scaladoc/resources/implicits-base-res.scala +++ b/test/scaladoc/resources/implicits-base-res.scala @@ -16,8 +16,9 @@ trait MyNumeric[R] * def convToManifestA(x: T) // pimpA7: with 2 constraints: T: Manifest and T <: Double * def convToMyNumericA(x: T) // pimpA6: with a constraint that there is x: MyNumeric[T] implicit in scope * def convToNumericA(x: T) // pimpA1: with a constraint that there is x: Numeric[T] implicit in scope - * def convToPimpedA(x: Bar[Foo[T]]) // pimpA5: no constraints - * def convToPimpedA(x: S) // pimpA4: with 3 constraints: T = Foo[Bar[S]], S: Foo and S: Bar + * def convToPimpedA(x: Bar[Foo[T]]) // pimpA5: no constraints, SHADOWED + * def convToPimpedA(x: S) // pimpA4: with 3 constraints: T = Foo[Bar[S]], S: Foo and S: Bar, SHADOWED + * def convToPimpedA(x: T) // pimpA0: with no constraints, SHADOWED * def convToTraversableOps(x: T) // pimpA7: with 2 constraints: T: Manifest and T <: Double * // should not be abstract! * }}} @@ -52,9 +53,10 @@ object A { * def convToManifestA(x: Double) // pimpA7: no constraints * def convToMyNumericA(x: Double) // pimpA6: (if showAll is set) with a constraint that there is x: MyNumeric[Double] implicit in scope * def convToNumericA(x: Double) // pimpA1: no constraintsd - * def convToPimpedA(x: Bar[Foo[Double]]) // pimpA5: no constraints + * def convToPimpedA(x: Bar[Foo[Double]]) // pimpA5: no constraints, SHADOWED + * def convToPimpedA(x: Double) // pimpA0: no constraints, SHADOWED * def convToTraversableOps(x: Double) // pimpA7: no constraints - * // should not be abstract! + * // should not be abstract! * }}} */ class B extends A[Double] @@ -68,7 +70,8 @@ object B extends A * def convToIntA(x: Int) // pimpA2: no constraints * def convToMyNumericA(x: Int) // pimpA6: (if showAll is set) with a constraint that there is x: MyNumeric[Int] implicit in scope * def convToNumericA(x: Int) // pimpA1: no constraints - * def convToPimpedA(x: Bar[Foo[Int]]) // pimpA5: no constraints + * def convToPimpedA(x: Int) // pimpA0: no constraints, SHADOWED + * def convToPimpedA(x: Bar[Foo[Int]]) // pimpA5: no constraints, SHADOWED * }}} */ class C extends A[Int] @@ -81,7 +84,8 @@ object C extends A * {{{ * def convToMyNumericA(x: String) // pimpA6: (if showAll is set) with a constraint that there is x: MyNumeric[String] implicit in scope * def convToNumericA(x: String) // pimpA1: (if showAll is set) with a constraint that there is x: Numeric[String] implicit in scope - * def convToPimpedA(x: Bar[Foo[String]]) // pimpA5: no constraints + * def convToPimpedA(x: Bar[Foo[String]]) // pimpA5: no constraints, SHADOWED + * def convToPimpedA(x: String) // pimpA0: no constraints, SHADOWED * }}} */ class D extends A[String] diff --git a/test/scaladoc/resources/implicits-elimination-res.scala b/test/scaladoc/resources/implicits-elimination-res.scala index b23667440c..5f7135c9e8 100644 --- a/test/scaladoc/resources/implicits-elimination-res.scala +++ b/test/scaladoc/resources/implicits-elimination-res.scala @@ -2,13 +2,13 @@ * Testing scaladoc implicits elimination */ package scala.test.scaladoc.implicits.elimination { - + import language.implicitConversions // according to SIP18 /** No conversion, as B doesn't bring any member */ class A class B { class C; trait V; type T; } - object A { - implicit def toB(a: A): B = null + object A { + implicit def toB(a: A): B = null } } diff --git a/test/scaladoc/run/diagrams-base.check b/test/scaladoc/run/diagrams-base.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/diagrams-base.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/diagrams-base.scala b/test/scaladoc/run/diagrams-base.scala new file mode 100644 index 0000000000..38bed06502 --- /dev/null +++ b/test/scaladoc/run/diagrams-base.scala @@ -0,0 +1,73 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + override def code = """ + package scala.test.scaladoc.diagrams + + import language.implicitConversions + + trait A + trait B + trait C + class E extends A with B with C + object E { implicit def eToT(e: E) = new T } + + class F extends E + class G extends E + private class H extends E /* since it's private, it won't go into the diagram */ + class T { def t = true } + + class X + object X { implicit def xToE(x: X) = new E} + class Y extends X + class Z + object Z { implicit def zToE(z: Z) = new E} + """ + + // diagrams must be started. In case there's an error with dot, it should not report anything + def scaladocSettings = "-diagrams -implicits" + + def testModel(rootPackage: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("diagrams") + val E = base._class("E") + val diag = E.inheritanceDiagram.get + + // there must be a single this node + assert(diag.nodes.filter(_.isThisNode).length == 1) + + // 1. check class E diagram + assert(diag.isClassDiagram) + + val (incoming, outgoing) = diag.edges.partition(!_._1.isThisNode) + assert(incoming.length == 5) + assert(outgoing.head._2.length == 4) + + val (outgoingSuperclass, outgoingImplicit) = outgoing.head._2.partition(_.isNormalNode) + assert(outgoingSuperclass.length == 3) + assert(outgoingImplicit.length == 1) + + val (incomingSubclass, incomingImplicit) = incoming.partition(_._1.isNormalNode) + assert(incomingSubclass.length == 2) + assert(incomingImplicit.length == 3) + + val classDiag = diag.asInstanceOf[ClassDiagram] + assert(classDiag.incomingImplicits.length == 3) + assert(classDiag.outgoingImplicits.length == 1) + + // 2. check package diagram + // NOTE: Z should be eliminated because it's isolated + val packDiag = base.contentDiagram.get + assert(packDiag.isPackageDiagram) + assert(packDiag.nodes.length == 8) // check singular object removal + assert(packDiag.edges.length == 4) + assert(packDiag.edges.foldLeft(0)(_ + _._2.length) == 6) + + // TODO: Should check numbering + } +} \ No newline at end of file diff --git a/test/scaladoc/run/diagrams-determinism.check b/test/scaladoc/run/diagrams-determinism.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/diagrams-determinism.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/diagrams-determinism.scala b/test/scaladoc/run/diagrams-determinism.scala new file mode 100644 index 0000000000..6c8db05d78 --- /dev/null +++ b/test/scaladoc/run/diagrams-determinism.scala @@ -0,0 +1,67 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + override def code = """ + package scala.test.scaladoc.diagrams + + trait A + trait B extends A + trait C extends B + trait D extends C with A + trait E extends C with A with D + """ + + // diagrams must be started. In case there's an error with dot, it should not report anything + def scaladocSettings = "-diagrams -implicits" + + def testModel(rootPackage: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + def diagramString(rootPackage: Package) = { + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("diagrams") + val A = base._trait("A") + val B = base._trait("B") + val C = base._trait("C") + val D = base._trait("D") + val E = base._trait("E") + + base.contentDiagram.get.toString + "\n" + + A.inheritanceDiagram.get.toString + "\n" + + B.inheritanceDiagram.get.toString + "\n" + + C.inheritanceDiagram.get.toString + "\n" + + D.inheritanceDiagram.get.toString + "\n" + + E.inheritanceDiagram.get.toString + } + + // 1. check that several runs produce the same output + val run0 = diagramString(rootPackage) + val run1 = diagramString(model.getOrElse({sys.error("Scaladoc Model Test ERROR: No universe generated!")}).rootPackage) + val run2 = diagramString(model.getOrElse({sys.error("Scaladoc Model Test ERROR: No universe generated!")}).rootPackage) + val run3 = diagramString(model.getOrElse({sys.error("Scaladoc Model Test ERROR: No universe generated!")}).rootPackage) + + // any variance in the order of the diagram elements should crash the following tests: + assert(run0 == run1) + assert(run1 == run2) + assert(run2 == run3) + + // 2. check the order in the diagram: this node, subclasses, and then implicit conversions + def assertRightOrder(diagram: Diagram) = { + for ((node, subclasses) <- diagram.edges) + assert(subclasses == subclasses.filter(_.isThisNode) ::: + subclasses.filter(_.isNormalNode) ::: + subclasses.filter(_.isImplicitNode)) + } + + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("diagrams") + assertRightOrder(base.contentDiagram.get) + assertRightOrder(base._trait("A").inheritanceDiagram.get) + assertRightOrder(base._trait("B").inheritanceDiagram.get) + assertRightOrder(base._trait("C").inheritanceDiagram.get) + assertRightOrder(base._trait("D").inheritanceDiagram.get) + assertRightOrder(base._trait("E").inheritanceDiagram.get) + } +} \ No newline at end of file diff --git a/test/scaladoc/run/diagrams-filtering.check b/test/scaladoc/run/diagrams-filtering.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/diagrams-filtering.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/diagrams-filtering.scala b/test/scaladoc/run/diagrams-filtering.scala new file mode 100644 index 0000000000..dfde5cac52 --- /dev/null +++ b/test/scaladoc/run/diagrams-filtering.scala @@ -0,0 +1,93 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + override def code = """ + package scala.test.scaladoc + + /** @contentDiagram hideNodes "scala.test.*.A" "java.*", hideEdges ("*G" -> "*E") */ + package object diagrams { + def foo = 4 + } + + package diagrams { + import language.implicitConversions + + /** @inheritanceDiagram hideIncomingImplicits, hideNodes "*E" */ + trait A + trait AA extends A + trait B + trait AAA extends B + + /** @inheritanceDiagram hideDiagram */ + trait C + trait AAAA extends C + + /** @inheritanceDiagram hideEdges("*E" -> "*A") */ + class E extends A with B with C + class F extends E + /** @inheritanceDiagram hideNodes "*G" "G" */ + class G extends E + private class H extends E /* since it's private, it won't go into the diagram */ + class T { def t = true } + object E { + implicit def eToT(e: E) = new T + implicit def eToA(e: E) = new A { } + } + } + """ + + // diagrams must be started. In case there's an error with dot, it should not report anything + def scaladocSettings = "-diagrams -implicits" + + def testModel(rootPackage: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + // base package + // Assert we have 7 nodes and 6 edges + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("diagrams") + val packDiag = base.contentDiagram.get + assert(packDiag.nodes.length == 6) + assert(packDiag.edges.map(_._2.length).sum == 5) + + // trait A + // Assert we have just 2 nodes and 1 edge + val A = base._trait("A") + val ADiag = A.inheritanceDiagram.get + assert(ADiag.nodes.length == 2) + assert(ADiag.edges.map(_._2.length).sum == 1) + + // trait C + val C = base._trait("C") + assert(!C.inheritanceDiagram.isDefined) + + // trait G + val G = base._trait("G") + assert(!G.inheritanceDiagram.isDefined) + + // trait E + val E = base._class("E") + val EDiag = E.inheritanceDiagram.get + + // there must be a single this node + assert(EDiag.nodes.filter(_.isThisNode).length == 1) + + // 1. check class E diagram + val (incoming, outgoing) = EDiag.edges.partition(!_._1.isThisNode) + assert(incoming.length == 2) // F and G + assert(outgoing.head._2.length == 3) // B, C and T + + val (outgoingSuperclass, outgoingImplicit) = outgoing.head._2.partition(_.isNormalNode) + assert(outgoingSuperclass.length == 2) // B and C + assert(outgoingImplicit.length == 1) // T + + val (incomingSubclass, incomingImplicit) = incoming.partition(_._1.isNormalNode) + assert(incomingSubclass.length == 2) // F and G + assert(incomingImplicit.length == 0) + + assert(EDiag.nodes.length == 6) // E, B and C, F and G and the implicit conversion to T + } +} \ No newline at end of file diff --git a/test/scaladoc/run/implicits-ambiguating.check b/test/scaladoc/run/implicits-ambiguating.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/implicits-ambiguating.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/implicits-ambiguating.scala b/test/scaladoc/run/implicits-ambiguating.scala new file mode 100644 index 0000000000..1420593b74 --- /dev/null +++ b/test/scaladoc/run/implicits-ambiguating.scala @@ -0,0 +1,114 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + // test a file instead of a piece of code + override def resourceFile = "implicits-ambiguating-res.scala" + + // start implicits + def scaladocSettings = "-implicits" + + def testModel(root: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + def isAmbiguous(mbr: MemberEntity): Boolean = + mbr.byConversion.map(_.source.implicitsShadowing.get(mbr).map(_.isAmbiguous).getOrElse(false)).getOrElse(false) + + // SEE THE test/resources/implicits-chaining-res.scala FOR THE EXPLANATION OF WHAT'S CHECKED HERE: + val base = root._package("scala")._package("test")._package("scaladoc")._package("implicits")._object("ambiguating") + var conv1: ImplicitConversion = null + var conv2: ImplicitConversion = null + +//// class A /////////////////////////////////////////////////////////////////////////////////////////////////////////// + + val A = base._class("A") + + conv1 = A._conversion(base._object("A").qualifiedName + ".AtoX") + conv2 = A._conversion(base._object("A").qualifiedName + ".AtoZ") + assert(conv1.members.length == 11) + assert(conv2.members.length == 11) + assert(conv1.constraints.length == 0) + assert(conv2.constraints.length == 0) + + /** - conv1-5 should be ambiguous + * - conv6-7 should not be ambiguous + * - conv8 should be ambiguous + * - conv9 should be ambiguous + * - conv10 and conv11 should not be ambiguous */ + def check1to9(cls: String): Unit = { + for (conv <- (1 to 5).map("conv" + _)) { + assert(isAmbiguous(conv1._member(conv)), cls + " - AtoX." + conv + " is ambiguous") + assert(isAmbiguous(conv2._member(conv)), cls + " - AtoZ." + conv + " is ambiguous") + } + for (conv <- (6 to 7).map("conv" + _)) { + assert(!isAmbiguous(conv1._member(conv)), cls + " - AtoX." + conv + " is not ambiguous") + assert(!isAmbiguous(conv2._member(conv)), cls + " - AtoZ." + conv + " is not ambiguous") + } + assert(isAmbiguous(conv1._member("conv8")), cls + " - AtoX.conv8 is ambiguous") + assert(isAmbiguous(conv2._member("conv8")), cls + " - AtoZ.conv8 is ambiguous") + assert(isAmbiguous(conv1._member("conv9")), cls + " - AtoX.conv9 is ambiguous") + assert(isAmbiguous(conv2._member("conv9")), cls + " - AtoZ.conv9 is ambiguous") + } + check1to9("A") + assert(!isAmbiguous(conv1._member("conv10")), "A - AtoX.conv10 is not ambiguous") + assert(!isAmbiguous(conv2._member("conv10")), "A - AtoZ.conv10 is not ambiguous") + assert(!isAmbiguous(conv1._member("conv11")), "A - AtoX.conv11 is not ambiguous") + assert(!isAmbiguous(conv2._member("conv11")), "A - AtoZ.conv11 is not ambiguous") + +//// class B /////////////////////////////////////////////////////////////////////////////////////////////////////////// + + val B = base._class("B") + + conv1 = B._conversion(base._object("A").qualifiedName + ".AtoX") + conv2 = B._conversion(base._object("A").qualifiedName + ".AtoZ") + assert(conv1.members.length == 11) + assert(conv2.members.length == 11) + assert(conv1.constraints.length == 0) + assert(conv2.constraints.length == 0) + + /** conv1-9 should be the same, conv10 should be ambiguous, conv11 should be okay */ + check1to9("B") + assert(isAmbiguous(conv1._member("conv10")), "B - AtoX.conv10 is ambiguous") + assert(isAmbiguous(conv2._member("conv10")), "B - AtoZ.conv10 is ambiguous") + assert(!isAmbiguous(conv1._member("conv11")), "B - AtoX.conv11 is not ambiguous") + assert(!isAmbiguous(conv2._member("conv11")), "B - AtoZ.conv11 is not ambiguous") + +//// class C /////////////////////////////////////////////////////////////////////////////////////////////////////////// + + val C = base._class("C") + + conv1 = C._conversion(base._object("A").qualifiedName + ".AtoX") + conv2 = C._conversion(base._object("A").qualifiedName + ".AtoZ") + assert(conv1.members.length == 11) + assert(conv2.members.length == 11) + assert(conv1.constraints.length == 0) + assert(conv2.constraints.length == 0) + + /** conv1-9 should be the same, conv10 and conv11 should not be ambiguous */ + check1to9("C") + assert(!isAmbiguous(conv1._member("conv10")), "C - AtoX.conv10 is not ambiguous") + assert(!isAmbiguous(conv2._member("conv10")), "C - AtoZ.conv10 is not ambiguous") + assert(!isAmbiguous(conv1._member("conv11")), "C - AtoX.conv11 is not ambiguous") + assert(!isAmbiguous(conv2._member("conv11")), "C - AtoZ.conv11 is not ambiguous") + +//// class D /////////////////////////////////////////////////////////////////////////////////////////////////////////// + + val D = base._class("D") + + conv1 = D._conversion(base._object("A").qualifiedName + ".AtoX") + conv2 = D._conversion(base._object("A").qualifiedName + ".AtoZ") + assert(conv1.members.length == 11) + assert(conv2.members.length == 11) + assert(conv1.constraints.length == 0) + assert(conv2.constraints.length == 0) + + /** conv1-9 should be the same, conv10 should not be ambiguous while conv11 should be ambiguous */ + check1to9("D") + assert(!isAmbiguous(conv1._member("conv10")), "D - AtoX.conv10 is not ambiguous") + assert(!isAmbiguous(conv2._member("conv10")), "D - AtoZ.conv10 is not ambiguous") + assert(isAmbiguous(conv1._member("conv11")), "D - AtoX.conv11 is ambiguous") + assert(isAmbiguous(conv2._member("conv11")), "D - AtoZ.conv11 is ambiguous") + } +} \ No newline at end of file diff --git a/test/scaladoc/run/implicits-base.scala b/test/scaladoc/run/implicits-base.scala index 06d017ed70..3d57306f5d 100644 --- a/test/scaladoc/run/implicits-base.scala +++ b/test/scaladoc/run/implicits-base.scala @@ -14,6 +14,9 @@ object Test extends ScaladocModelTest { // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) import access._ + def isShadowed(mbr: MemberEntity): Boolean = + mbr.byConversion.map(_.source.implicitsShadowing.get(mbr).map(_.isShadowed).getOrElse(false)).getOrElse(false) + // SEE THE test/resources/implicits-base-res.scala FOR THE EXPLANATION OF WHAT'S CHECKED HERE: val base = root._package("scala")._package("test")._package("scaladoc")._package("implicits")._package("base") var conv: ImplicitConversion = null @@ -22,8 +25,12 @@ object Test extends ScaladocModelTest { val A = base._class("A") - // the method pimped on by pimpA0 should be shadowed by the method in class A - assert(A._conversions(A.qualifiedName + ".pimpA0").isEmpty) + // def convToPimpedA(x: T) // pimpA0: with no constraints, SHADOWED + conv = A._conversion(A.qualifiedName + ".pimpA0") + assert(conv.members.length == 1) + assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) + assert(conv._member("convToPimpedA").resultType.name == "T") // def convToNumericA: T // pimpA1: with a constraint that there is x: Numeric[T] implicit in scope conv = A._conversion(A.qualifiedName + ".pimpA1") @@ -53,6 +60,7 @@ object Test extends ScaladocModelTest { conv = A._conversion(A.qualifiedName + ".pimpA5") assert(conv.members.length == 1) assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) assert(conv._member("convToPimpedA").resultType.name == "Bar[Foo[T]]") // def convToMyNumericA: T // pimpA6: with a constraint that there is x: MyNumeric[T] implicit in scope @@ -76,10 +84,16 @@ object Test extends ScaladocModelTest { val B = base._class("B") // these conversions should not affect B - assert(B._conversions(A.qualifiedName + ".pimpA0").isEmpty) assert(B._conversions(A.qualifiedName + ".pimpA2").isEmpty) assert(B._conversions(A.qualifiedName + ".pimpA4").isEmpty) + // def convToPimpedA(x: Double) // pimpA0: no constraints, SHADOWED + conv = B._conversion(A.qualifiedName + ".pimpA0") + assert(conv.members.length == 1) + assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) + assert(conv._member("convToPimpedA").resultType.name == "Double") + // def convToNumericA: Double // pimpA1: no constraintsd conv = B._conversion(A.qualifiedName + ".pimpA1") assert(conv.members.length == 1) @@ -96,6 +110,7 @@ object Test extends ScaladocModelTest { conv = B._conversion(A.qualifiedName + ".pimpA5") assert(conv.members.length == 1) assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) assert(conv._member("convToPimpedA").resultType.name == "Bar[Foo[Double]]") // def convToMyNumericA: Double // pimpA6: (if showAll is set) with a constraint that there is x: MyNumeric[Double] implicit in scope @@ -119,11 +134,17 @@ object Test extends ScaladocModelTest { val C = base._class("C") // these conversions should not affect C - assert(C._conversions(A.qualifiedName + ".pimpA0").isEmpty) assert(C._conversions(A.qualifiedName + ".pimpA3").isEmpty) assert(C._conversions(A.qualifiedName + ".pimpA4").isEmpty) assert(C._conversions(A.qualifiedName + ".pimpA7").isEmpty) + // def convToPimpedA(x: Int) // pimpA0: no constraints, SHADOWED + conv = C._conversion(A.qualifiedName + ".pimpA0") + assert(conv.members.length == 1) + assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) + assert(conv._member("convToPimpedA").resultType.name == "Int") + // def convToNumericA: Int // pimpA1: no constraints conv = C._conversion(A.qualifiedName + ".pimpA1") assert(conv.members.length == 1) @@ -140,6 +161,7 @@ object Test extends ScaladocModelTest { conv = C._conversion(A.qualifiedName + ".pimpA5") assert(conv.members.length == 1) assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) assert(conv._member("convToPimpedA").resultType.name == "Bar[Foo[Int]]") // def convToMyNumericA: Int // pimpA6: (if showAll is set) with a constraint that there is x: MyNumeric[Int] implicit in scope @@ -153,12 +175,18 @@ object Test extends ScaladocModelTest { val D = base._class("D") // these conversions should not affect D - assert(D._conversions(A.qualifiedName + ".pimpA0").isEmpty) assert(D._conversions(A.qualifiedName + ".pimpA2").isEmpty) assert(D._conversions(A.qualifiedName + ".pimpA3").isEmpty) assert(D._conversions(A.qualifiedName + ".pimpA4").isEmpty) assert(D._conversions(A.qualifiedName + ".pimpA7").isEmpty) + // def convToPimpedA(x: String) // pimpA0: no constraints, SHADOWED + conv = D._conversion(A.qualifiedName + ".pimpA0") + assert(conv.members.length == 1) + assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) + assert(conv._member("convToPimpedA").resultType.name == "String") + // def convToNumericA: String // pimpA1: (if showAll is set) with a constraint that there is x: Numeric[String] implicit in scope conv = D._conversion(A.qualifiedName + ".pimpA1") assert(conv.members.length == 1) @@ -169,6 +197,7 @@ object Test extends ScaladocModelTest { conv = D._conversion(A.qualifiedName + ".pimpA5") assert(conv.members.length == 1) assert(conv.constraints.length == 0) + assert(isShadowed(conv._member("convToPimpedA"))) assert(conv._member("convToPimpedA").resultType.name == "Bar[Foo[String]]") // def convToMyNumericA: String // pimpA6: (if showAll is set) with a constraint that there is x: MyNumeric[String] implicit in scope diff --git a/test/scaladoc/run/implicits-elimination.check b/test/scaladoc/run/implicits-elimination.check deleted file mode 100644 index 619c56180b..0000000000 --- a/test/scaladoc/run/implicits-elimination.check +++ /dev/null @@ -1 +0,0 @@ -Done. diff --git a/test/scaladoc/run/implicits-elimination.scala b/test/scaladoc/run/implicits-elimination.scala deleted file mode 100644 index ed37b9cd90..0000000000 --- a/test/scaladoc/run/implicits-elimination.scala +++ /dev/null @@ -1,23 +0,0 @@ -import scala.tools.nsc.doc.model._ -import scala.tools.partest.ScaladocModelTest -import language._ - -object Test extends ScaladocModelTest { - - // test a file instead of a piece of code - override def resourceFile = "implicits-elimination-res.scala" - - // start implicits - def scaladocSettings = "-implicits" - - def testModel(root: Package) = { - // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) - import access._ - - // SEE THE test/resources/implicits-elimination-res.scala FOR THE EXPLANATION OF WHAT'S CHECKED HERE: - val base = root._package("scala")._package("test")._package("scaladoc")._package("implicits")._package("elimination") - val A = base._class("A") - - assert(A._conversions(A.qualifiedName + ".toB").isEmpty) - } -} diff --git a/test/scaladoc/run/implicits-shadowing.scala b/test/scaladoc/run/implicits-shadowing.scala index 7835223d21..2827d31122 100644 --- a/test/scaladoc/run/implicits-shadowing.scala +++ b/test/scaladoc/run/implicits-shadowing.scala @@ -13,6 +13,9 @@ object Test extends ScaladocModelTest { // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) import access._ + def isShadowed(mbr: MemberEntity): Boolean = + mbr.byConversion.map(_.source.implicitsShadowing.get(mbr).map(_.isShadowed).getOrElse(false)).getOrElse(false) + // SEE THE test/resources/implicits-chaining-res.scala FOR THE EXPLANATION OF WHAT'S CHECKED HERE: val base = root._package("scala")._package("test")._package("scaladoc")._package("implicits")._object("shadowing") var conv: ImplicitConversion = null @@ -22,12 +25,8 @@ object Test extends ScaladocModelTest { val A = base._class("A") conv = A._conversion(base._object("A").qualifiedName + ".AtoZ") - assert(conv.members.length == 5) - conv._member("conv5") - conv._member("conv8") - conv._member("conv9") - conv._member("conv10") - conv._member("conv11") + assert(conv.members.length == 11) + assert(conv.members.forall(isShadowed(_))) assert(conv.constraints.length == 0) //// class B /////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -35,11 +34,8 @@ object Test extends ScaladocModelTest { val B = base._class("B") conv = B._conversion(base._object("A").qualifiedName + ".AtoZ") - assert(conv.members.length == 4) - conv._member("conv5") - conv._member("conv8") - conv._member("conv9") - conv._member("conv11") + assert(conv.members.length == 11) + assert(conv.members.forall(isShadowed(_))) assert(conv.constraints.length == 0) //// class C /////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -47,12 +43,8 @@ object Test extends ScaladocModelTest { val C = base._class("C") conv = C._conversion(base._object("A").qualifiedName + ".AtoZ") - assert(conv.members.length == 5) - conv._member("conv5") - conv._member("conv8") - conv._member("conv9") - conv._member("conv10") - conv._member("conv11") + assert(conv.members.length == 11) + assert(conv.members.forall(isShadowed(_))) assert(conv.constraints.length == 0) //// class D /////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -60,11 +52,8 @@ object Test extends ScaladocModelTest { val D = base._class("D") conv = D._conversion(base._object("A").qualifiedName + ".AtoZ") - assert(conv.members.length == 4) - conv._member("conv5") - conv._member("conv8") - conv._member("conv9") - conv._member("conv10") + assert(conv.members.length == 11) + assert(conv.members.forall(isShadowed(_))) assert(conv.constraints.length == 0) } -} \ No newline at end of file +} diff --git a/test/scaladoc/run/implicits-var-exp.check b/test/scaladoc/run/implicits-var-exp.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/implicits-var-exp.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/implicits-var-exp.scala b/test/scaladoc/run/implicits-var-exp.scala new file mode 100644 index 0000000000..16569fe3c2 --- /dev/null +++ b/test/scaladoc/run/implicits-var-exp.scala @@ -0,0 +1,43 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + override def code = """ + package scala.test.scaladoc.variable.expansion { + /** + * Blah blah blah + */ + class A + + object A { + import language.implicitConversions + implicit def aToB(a: A) = new B + } + + /** + * @define coll collection + */ + class B { + /** + * foo returns a $coll + */ + def foo: Nothing = ??? + } + } + """ + + // diagrams must be started. In case there's an error with dot, it should not report anything + def scaladocSettings = "-implicits" + + def testModel(rootPackage: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("variable")._package("expansion") + val foo = base._class("A")._method("foo") + + assert(foo.comment.get.body.toString.contains("foo returns a collection"), "\"" + foo.comment.get.body.toString + "\".contains(\"foo returns a collection\")") + } +} \ No newline at end of file diff --git a/test/scaladoc/scalacheck/CommentFactoryTest.scala b/test/scaladoc/scalacheck/CommentFactoryTest.scala index 68ca68efdd..b576ba5544 100644 --- a/test/scaladoc/scalacheck/CommentFactoryTest.scala +++ b/test/scaladoc/scalacheck/CommentFactoryTest.scala @@ -5,10 +5,12 @@ import scala.tools.nsc.Global import scala.tools.nsc.doc import scala.tools.nsc.doc.model._ import scala.tools.nsc.doc.model.comment._ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ class Factory(val g: Global, val s: doc.Settings) extends doc.model.ModelFactory(g, s) { - thisFactory: Factory with ModelFactoryImplicitSupport with CommentFactory with doc.model.TreeFactory => + thisFactory: Factory with ModelFactoryImplicitSupport with DiagramFactory with CommentFactory with doc.model.TreeFactory => def strip(c: Comment): Option[Inline] = { c.body match { @@ -29,7 +31,7 @@ object Test extends Properties("CommentFactory") { val settings = new doc.Settings((str: String) => {}) val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings) val g = new Global(settings, reporter) - (new Factory(g, settings) with ModelFactoryImplicitSupport with CommentFactory with doc.model.TreeFactory) + (new Factory(g, settings) with ModelFactoryImplicitSupport with DiagramFactory with CommentFactory with doc.model.TreeFactory) } def parse(src: String, dst: Inline) = { -- cgit v1.2.3 From c85b4a495619ca69a5d0c0b69fc30233fbea3dff Mon Sep 17 00:00:00 2001 From: Damien Date: Wed, 13 Jun 2012 21:41:06 +0200 Subject: Scaladoc class diagrams part 2 This commit contains the svg diagram generation using the graphviz package, the template changes, the css styling and javascript code that enables displaying and interacting with the diagrams. The full history is located at: https://github.com/damienobrist/scala/tree/feature/diagrams-dev The diagrams are included as svg markup inside the html code. This enables interacting with the image beyond what would be possible with a static image (highlighting, scaling, tooltips, links to nodes, etc). The svg generation has four main phases: model => dot, dot => svg (using the graphviz package), svg postprocessing, inclusion in the html page. This commit also fixes SI-5212 - links to individual pages automatically load the left navigation panel of the website. Commit summary: - diagram generation - model => dot (DotDiagramGenerator.scala, DiagramGenerator.scala) - dot => svg (DotRunner.scala) - svg post-processing (DotDiagramGenerator.scala) - svg inclusion in the html (Template.scala) - diagram interaction - css, js and image files Review by @heathermiller, @kzys. Also fixed the memory leak that was causing the testsuite to timeout. --- .../scala/tools/nsc/doc/html/HtmlFactory.scala | 16 +- .../scala/tools/nsc/doc/html/page/Template.scala | 52 ++- .../doc/html/page/diagram/DiagramGenerator.scala | 53 +++ .../html/page/diagram/DotDiagramGenerator.scala | 470 +++++++++++++++++++++ .../nsc/doc/html/page/diagram/DotRunner.scala | 227 ++++++++++ .../nsc/doc/html/resource/lib/class_diagram.png | Bin 0 -> 3910 bytes .../tools/nsc/doc/html/resource/lib/diagrams.css | 135 ++++++ .../tools/nsc/doc/html/resource/lib/diagrams.js | 324 ++++++++++++++ .../nsc/doc/html/resource/lib/modernizr.custom.js | 4 + .../nsc/doc/html/resource/lib/object_diagram.png | Bin 0 -> 3903 bytes .../tools/nsc/doc/html/resource/lib/raphael-min.js | 10 + .../tools/nsc/doc/html/resource/lib/template.css | 12 +- .../tools/nsc/doc/html/resource/lib/template.js | 18 +- .../nsc/doc/html/resource/lib/trait_diagram.png | Bin 0 -> 3882 bytes 14 files changed, 1297 insertions(+), 24 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala create mode 100644 src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala create mode 100644 src/compiler/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/class_diagram.png create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.css create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.js create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/modernizr.custom.js create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/object_diagram.png create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/raphael-min.js create mode 100644 src/compiler/scala/tools/nsc/doc/html/resource/lib/trait_diagram.png diff --git a/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala b/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala index 914824d523..51c5793d46 100644 --- a/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala @@ -11,6 +11,9 @@ import model._ import java.io.{ File => JFile } import io.{ Streamable, Directory } import scala.collection._ +import page.diagram._ + +import html.page.diagram.DiagramGenerator /** A class that can generate Scaladoc sites to some fixed root folder. * @author David Bernard @@ -29,21 +32,27 @@ class HtmlFactory(val universe: doc.Universe, index: doc.Index) { "jquery.js", "jquery.layout.js", "scheduler.js", + "diagrams.js", "template.js", "tools.tooltip.js", + "modernizr.custom.js", "index.css", "ref-index.css", "template.css", + "diagrams.css", "class.png", "class_big.png", + "class_diagram.png", "object.png", "object_big.png", + "object_diagram.png", "package.png", "package_big.png", "trait.png", "trait_big.png", + "trait_diagram.png", "class_to_object_big.png", "object_to_class_big.png", @@ -105,6 +114,8 @@ class HtmlFactory(val universe: doc.Universe, index: doc.Index) { finally out.close() } + DiagramGenerator.initialize(universe.settings) + libResources foreach (s => copyResource("lib/" + s)) new page.Index(universe, index) writeFor this @@ -115,14 +126,17 @@ class HtmlFactory(val universe: doc.Universe, index: doc.Index) { for (letter <- index.firstLetterIndex) { new html.page.ReferenceIndex(letter._1, index, universe) writeFor this } + + DiagramGenerator.cleanup() } def writeTemplates(writeForThis: HtmlPage => Unit) { val written = mutable.HashSet.empty[DocTemplateEntity] + val diagramGenerator: DiagramGenerator = new DotDiagramGenerator(universe.settings) def writeTemplate(tpl: DocTemplateEntity) { if (!(written contains tpl)) { - writeForThis(new page.Template(universe, tpl)) + writeForThis(new page.Template(universe, diagramGenerator, tpl)) written += tpl tpl.templates map writeTemplate } diff --git a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala index 5978489585..24cff1b475 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala @@ -8,12 +8,18 @@ package doc package html package page +import model._ +import model.diagram._ +import diagram._ + import scala.xml.{ NodeSeq, Text, UnprefixedAttribute } import language.postfixOps import model._ +import model.diagram._ +import diagram._ -class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage { +class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemplateEntity) extends HtmlPage { val path = templateToPath(tpl) @@ -30,10 +36,22 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage val headers = - + + + { if (universe.settings.docDiagrams.isSetByUser) { + + + } else NodeSeq.Empty } + val valueMembers = @@ -614,7 +632,19 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage case _ => NodeSeq.Empty } - memberComment ++ paramComments ++ attributesBlock ++ linearization ++ subclasses + val typeHierarchy = if (s.docDiagrams.isSetByUser) mbr match { + case dtpl: DocTemplateEntity if isSelf && !isReduced && dtpl.inheritanceDiagram.isDefined => + makeDiagramHtml(dtpl, dtpl.inheritanceDiagram, "Type Hierarchy", "inheritance-diagram") + case _ => NodeSeq.Empty + } else NodeSeq.Empty // diagrams not generated + + val contentHierarchy = if (s.docDiagrams.isSetByUser) mbr match { + case dtpl: DocTemplateEntity if isSelf && !isReduced && dtpl.contentDiagram.isDefined => + makeDiagramHtml(dtpl, dtpl.contentDiagram, "Content Hierarchy", "content-diagram") + case _ => NodeSeq.Empty + } else NodeSeq.Empty // diagrams not generated + + memberComment ++ paramComments ++ attributesBlock ++ linearization ++ subclasses ++ typeHierarchy ++ contentHierarchy } def kindToString(mbr: MemberEntity): String = { @@ -920,9 +950,25 @@ class Template(universe: doc.Universe, tpl: DocTemplateEntity) extends HtmlPage xml.Text(ub.typeParamName + " is a subclass of " + ub.upperBound.name + " (" + ub.typeParamName + " <: ") ++ typeToHtml(ub.upperBound, true) ++ xml.Text(")") } + + def makeDiagramHtml(tpl: DocTemplateEntity, diagram: Option[Diagram], description: String, id: String) = { + val s = universe.settings + val diagramSvg = generator.generate(diagram.get, tpl, this) + if (diagramSvg != NodeSeq.Empty) { +
    + { description } + Learn more about scaladoc diagrams +
    { + diagramSvg + }
    +
    + } else NodeSeq.Empty + } } object Template { + /* Vlad: Lesson learned the hard way: don't put any stateful code that references the model here, + * it won't be garbage collected and you'll end up filling the heap with garbage */ def isImplicit(mbr: MemberEntity) = mbr.byConversion.isDefined def isShadowedImplicit(mbr: MemberEntity): Boolean = diff --git a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala new file mode 100644 index 0000000000..61c1819d11 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala @@ -0,0 +1,53 @@ +/** + * @author Damien Obrist + * @author Vlad Ureche + */ +package scala.tools.nsc +package doc +package html +package page +package diagram + +import scala.xml.NodeSeq +import scala.tools.nsc.doc.html.HtmlPage +import scala.tools.nsc.doc.model.diagram.Diagram +import scala.tools.nsc.doc.model.DocTemplateEntity + +trait DiagramGenerator { + + /** + * Generates a visualization of the internal representation + * of a diagram. + * + * @param d The model of the diagram + * @param p The page the diagram will be embedded in (needed for link generation) + * @return The HTML to be embedded in the Scaladoc page + */ + def generate(d: Diagram, t: DocTemplateEntity, p: HtmlPage):NodeSeq +} + +object DiagramGenerator { + + // TODO: This is tailored towards the dot generator, since it's the only generator. In the future it should be more + // general. + + private[this] var dotRunner: DotRunner = null + private[this] var settings: doc.Settings = null + + def initialize(s: doc.Settings) = + settings = s + + def getDotRunner() = { + if (dotRunner == null) + dotRunner = new DotRunner(settings) + dotRunner + } + + def cleanup() = { + DiagramStats.printStats(settings) + if (dotRunner != null) { + dotRunner.cleanup() + dotRunner = null + } + } +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala new file mode 100644 index 0000000000..f5a1d1b088 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala @@ -0,0 +1,470 @@ +/** + * @author Damien Obrist + * @author Vlad Ureche + */ +package scala.tools.nsc +package doc +package html +package page +package diagram + +import scala.xml.{NodeSeq, XML, PrefixedAttribute, Elem, MetaData, Null, UnprefixedAttribute} +import scala.collection.immutable._ +import javax.xml.parsers.SAXParser +import model._ +import model.diagram._ + +class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { + + // the page where the diagram will be embedded + private var page: HtmlPage = null + // path to the "lib" folder relative to the page + private var pathToLib: String = null + // maps nodes to unique indices + private var node2Index: Map[Node, Int] = null + // maps an index to its corresponding node + private var index2Node: Map[Int, Node] = null + // true if the current diagram is a class diagram + private var isClassDiagram = false + // incoming implicit nodes (needed for determining the CSS class of a node) + private var incomingImplicitNodes: List[Node] = List() + // the suffix used when there are two many classes to show + private final val MultiSuffix = " classes/traits" + // used to generate unique node and edge ids (i.e. avoid conflicts with multiple diagrams) + private var counter = 0 + + def generate(diagram: Diagram, template: DocTemplateEntity, page: HtmlPage):NodeSeq = { + counter = counter + 1; + this.page = page + pathToLib = "../" * (page.templateToPath(template).size - 1) + "lib/" + val dot = generateDot(diagram) + val result = generateSVG(dot, template) + // clean things up a bit, so we don't leave garbage on the heap + this.page = null + node2Index = null + index2Node = null + incomingImplicitNodes = List() + result + } + + /** + * Generates a dot string for a given diagram. + */ + private def generateDot(d: Diagram) = { + // inheritance nodes (all nodes except thisNode and implicit nodes) + var nodes: List[Node] = null + // inheritance edges (all edges except implicit edges) + var edges: List[(Node, List[Node])] = null + + // timing + var tDot = -System.currentTimeMillis + + // variables specific to class diagrams: + // current node of a class diagram + var thisNode:Node = null + var subClasses = List[Node]() + var superClasses = List[Node]() + var incomingImplicits = List[Node]() + var outgoingImplicits = List[Node]() + var subClassesTooltip: Option[String] = None + var superClassesTooltip: Option[String] = None + var incomingImplicitsTooltip: Option[String] = None + var outgoingImplicitsTooltip: Option[String] = None + isClassDiagram = false + + d match { + case ClassDiagram(_thisNode, _superClasses, _subClasses, _incomingImplicits, _outgoingImplicits) => + + def textTypeEntity(text: String) = + new TypeEntity { + val name = text + def refEntity: SortedMap[Int, (TemplateEntity, Int)] = SortedMap() + } + + // it seems dot chokes on node names over 8000 chars, so let's limit the size of the string + // conservatively, we'll limit at 4000, to be sure: + def limitSize(str: String) = if (str.length > 4000) str.substring(0, 3996) + " ..." else str + + // avoid overcrowding the diagram: + // if there are too many super / sub / implicit nodes, represent + // them by on node with a corresponding tooltip + superClasses = if (_superClasses.length > settings.docDiagramsMaxNormalClasses.value) { + superClassesTooltip = Some(limitSize(_superClasses.map(_.tpe.name).mkString(", "))) + List(NormalNode(textTypeEntity(_superClasses.length + MultiSuffix), None)) + } else _superClasses + + subClasses = if (_subClasses.length > settings.docDiagramsMaxNormalClasses.value) { + subClassesTooltip = Some(limitSize(_subClasses.map(_.tpe.name).mkString(", "))) + List(NormalNode(textTypeEntity(_subClasses.length + MultiSuffix), None)) + } else _subClasses + + incomingImplicits = if (_incomingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { + incomingImplicitsTooltip = Some(limitSize(_incomingImplicits.map(_.tpe.name).mkString(", "))) + List(ImplicitNode(textTypeEntity(_incomingImplicits.length + MultiSuffix), None)) + } else _incomingImplicits + + outgoingImplicits = if (_outgoingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { + outgoingImplicitsTooltip = Some(limitSize(_outgoingImplicits.map(_.tpe.name).mkString(", "))) + List(ImplicitNode(textTypeEntity(_outgoingImplicits.length + MultiSuffix), None)) + } else _outgoingImplicits + + thisNode = _thisNode + nodes = List() + edges = (thisNode -> superClasses) :: subClasses.map(_ -> List(thisNode)) + node2Index = (thisNode::subClasses:::superClasses:::incomingImplicits:::outgoingImplicits).zipWithIndex.toMap + isClassDiagram = true + incomingImplicitNodes = incomingImplicits + case _ => + nodes = d.nodes + edges = d.edges + node2Index = d.nodes.zipWithIndex.toMap + incomingImplicitNodes = List() + } + index2Node = node2Index map {_.swap} + + val implicitsDot = { + if (!isClassDiagram) "" + else { + // dot cluster containing thisNode + val thisCluster = "subgraph clusterThis {\n" + + "style=\"invis\"\n" + + node2Dot(thisNode, None) + + "}" + // dot cluster containing incoming implicit nodes, if any + val incomingCluster = { + if(incomingImplicits.isEmpty) "" + else "subgraph clusterIncoming {\n" + + "style=\"invis\"\n" + + incomingImplicits.reverse.map(n => node2Dot(n, incomingImplicitsTooltip)).mkString + + (if (incomingImplicits.size > 1) + incomingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" + else "") + + "}" + } + // dot cluster containing outgoing implicit nodes, if any + val outgoingCluster = { + if(outgoingImplicits.isEmpty) "" + else "subgraph clusterOutgoing {\n" + + "style=\"invis\"\n" + + outgoingImplicits.reverse.map(n => node2Dot(n, outgoingImplicitsTooltip)).mkString + + (if (outgoingImplicits.size > 1) + outgoingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" + else "") + + "}" + } + + // assemble clusters into another cluster + val incomingTooltip = incomingImplicits.map(_.name).mkString(", ") + " can be implicitly converted to " + thisNode.name + val outgoingTooltip = thisNode.name + " can be implicitly converted to " + outgoingImplicits.map(_.name).mkString(", ") + "subgraph clusterAll {\n" + + "style=\"invis\"\n" + + outgoingCluster + "\n" + + thisCluster + "\n" + + incomingCluster + "\n" + + // incoming implicit edge + (if (!incomingImplicits.isEmpty) { + val n = incomingImplicits.last + "node" + node2Index(n) +" -> node" + node2Index(thisNode) + + " [id=\"" + cssClass(n, thisNode) + "|" + node2Index(n) + "_" + node2Index(thisNode) + "\", tooltip=\"" + incomingTooltip + "\"" + + ", constraint=\"false\", minlen=\"2\", ltail=\"clusterIncoming\", lhead=\"clusterThis\", label=\"implicitly\"];\n" + } else "") + + // outgoing implicit edge + (if (!outgoingImplicits.isEmpty) { + val n = outgoingImplicits.head + "node" + node2Index(thisNode) + " -> node" + node2Index(n) + + " [id=\"" + cssClass(thisNode, n) + "|" + node2Index(thisNode) + "_" + node2Index(n) + "\", tooltip=\"" + outgoingTooltip + "\"" + + ", constraint=\"false\", minlen=\"2\", ltail=\"clusterThis\", lhead=\"clusterOutgoing\", label=\"implicitly\"];\n" + } else "") + + "}" + } + } + + // assemble graph + val graph = "digraph G {\n" + + // graph / node / edge attributes + graphAttributesStr + + "node [" + nodeAttributesStr + "];\n" + + "edge [" + edgeAttributesStr + "];\n" + + implicitsDot + "\n" + + // inheritance nodes + nodes.map(n => node2Dot(n, None)).mkString + + subClasses.map(n => node2Dot(n, subClassesTooltip)).mkString + + superClasses.map(n => node2Dot(n, superClassesTooltip)).mkString + + // inheritance edges + edges.map{ case (from, tos) => tos.map(to => { + val id = "graph" + counter + "_" + node2Index(to) + "_" + node2Index(from) + // the X -> Y edge is inverted twice to keep the diagram flowing the right way + // that is, an edge from node X to Y will result in a dot instruction nodeY -> nodeX [dir="back"] + "node" + node2Index(to) + " -> node" + node2Index(from) + + " [id=\"" + cssClass(to, from) + "|" + id + "\", " + + "tooltip=\"" + from.name + (if (from.name.endsWith(MultiSuffix)) " are subtypes of " else " is a subtype of ") + + to.name + "\", dir=\"back\", arrowtail=\"empty\"];\n" + }).mkString}.mkString + + "}" + + tDot += System.currentTimeMillis + DiagramStats.addDotGenerationTime(tDot) + + graph + } + + /** + * Generates the dot string of a given node. + */ + private def node2Dot(node: Node, tooltip: Option[String]) = { + + // escape HTML characters in node names + def escape(name: String) = name.replace("&", "&").replace("<", "<").replace(">", ">"); + + // assemble node attribues in a map + var attr = scala.collection.mutable.Map[String, String]() + + // link + node.doctpl match { + case Some(tpl) => attr += "URL" -> (page.relativeLinkTo(tpl) + "#inheritance-diagram") + case _ => + } + + // tooltip + tooltip match { + case Some(text) => attr += "tooltip" -> text + // show full name where available (instead of TraversableOps[A] show scala.collection.parallel.TraversableOps[A]) + case None if node.tpl.isDefined => attr += "tooltip" -> node.tpl.get.qualifiedName + case _ => + } + + // styles + if(node.isImplicitNode) + attr ++= implicitStyle + else if(node.isOutsideNode) + attr ++= outsideStyle + else if(node.isTraitNode) + attr ++= traitStyle + else if(node.isClassNode) + attr ++= classStyle + else if(node.isObjectNode) + attr ++= objectStyle + else + attr ++= defaultStyle + + // HTML label + var name = escape(node.name) + var img = "" + if(node.isTraitNode) + img = "trait_diagram.png" + else if(node.isClassNode) + img = "class_diagram.png" + else if(node.isObjectNode) + img = "object_diagram.png" + + if(!img.equals("")) { + img = "" + name = name + " " + } + val label = "<" + + "" + img + "" + + "
    " + name + "
    >" + + // dot does not allow to specify a CSS class, therefore + // set the id to "{class}|{id}", which will be used in + // the transform method + val id = "graph" + counter + "_" + node2Index(node) + attr += ("id" -> (cssClass(node) + "|" + id)) + + // return dot string + "node" + node2Index(node) + " [label=" + label + "," + flatten(attr.toMap) + "];\n" + } + + /** + * Returns the CSS class for an edge connecting node1 and node2. + */ + private def cssClass(node1: Node, node2: Node): String = { + if (node1.isImplicitNode && node2.isThisNode) + "implicit-incoming" + else if (node1.isThisNode && node2.isImplicitNode) + "implicit-outgoing" + else + "inheritance" + } + + /** + * Returns the CSS class for a node. + */ + private def cssClass(node: Node): String = + if (node.isImplicitNode && incomingImplicitNodes.contains(node)) + "implicit-incoming" + else if (node.isImplicitNode) + "implicit-outgoing" + else if (node.isObjectNode) + "object" + else if (node.isThisNode) + "this" + cssBaseClass(node, "") + else if (node.isOutsideNode) + "outside" + cssBaseClass(node, "") + else + cssBaseClass(node, "default") + + private def cssBaseClass(node: Node, default: String) = + if (node.isClassNode) + " class" + else if (node.isTraitNode) + " trait" + else if (node.isObjectNode) + " trait" + else + default + + /** + * Calls dot with a given dot string and returns the SVG output. + */ + private def generateSVG(dotInput: String, template: DocTemplateEntity) = { + val dotOutput = DiagramGenerator.getDotRunner.feedToDot(dotInput, template) + var tSVG = -System.currentTimeMillis + + val result = if (dotOutput != null) { + val src = scala.io.Source.fromString(dotOutput); + try { + val cpa = scala.xml.parsing.ConstructingParser.fromSource(src, false) + val doc = cpa.document() + if (doc != null) + transform(doc.docElem) + else + NodeSeq.Empty + } catch { + case exc => + if (settings.docDiagramsDebug.value) { + settings.printMsg("\n\n**********************************************************************") + settings.printMsg("Encountered an error while generating page for " + template.qualifiedName) + settings.printMsg(dotInput.toString.split("\n").mkString("\nDot input:\n\t","\n\t","")) + settings.printMsg(dotOutput.toString.split("\n").mkString("\nDot output:\n\t","\n\t","")) + settings.printMsg(exc.getStackTrace.mkString("\nException: " + exc.toString + ":\n\tat ", "\n\tat ","")) + settings.printMsg("\n\n**********************************************************************") + } else { + settings.printMsg("\nThe diagram for " + template.qualifiedName + " could not be created due to an internal error.") + settings.printMsg("Use " + settings.docDiagramsDebug.name + " for more information and please file this as a bug.") + } + NodeSeq.Empty + } + } else + NodeSeq.Empty + + tSVG += System.currentTimeMillis + DiagramStats.addSvgTime(tSVG) + + result + } + + /** + * Transforms the SVG generated by dot: + * - adds a class attribute to the SVG element + * - changes the path of the node images from absolute to relative + * - assigns id and class attributes to nodes and edges + * - removes title elements + */ + private def transform(e:scala.xml.Node): scala.xml.Node = e match { + // add an id and class attribute to the SVG element + case Elem(prefix, "svg", attribs, scope, child @ _*) => { + val klass = if (isClassDiagram) "class-diagram" else "package-diagram" + Elem(prefix, "svg", attribs, scope, child map(x => transform(x)) : _*) % + new UnprefixedAttribute("id", "graph" + counter, Null) % + new UnprefixedAttribute("class", klass, Null) + } + // change the path of the node images from absolute to relative + case img @ => { + val href = (img \ "@{http://www.w3.org/1999/xlink}href").toString + val file = href.substring(href.lastIndexOf("/") + 1, href.size) + img.asInstanceOf[Elem] % + new PrefixedAttribute("xlink", "href", pathToLib + file, Null) + } + // assign id and class attributes to edges and nodes: + // the id attribute generated by dot has the format: "{class}|{id}" + case g @ Elem(prefix, "g", attribs, scope, children @ _*) if (List("edge", "node").contains((g \ "@class").toString)) => { + val res = new Elem(prefix, "g", attribs, scope, (children map(x => transform(x))): _*) + val dotId = (g \ "@id").toString + if (dotId.count(_ == '|') == 1) { + val Array(klass, id) = dotId.toString.split("\\|") + res % new UnprefixedAttribute("id", id, Null) % + new UnprefixedAttribute("class", (g \ "@class").toString + " " + klass, Null) + } + else res + } + // remove titles + case { _* } => + scala.xml.Text("") + // apply recursively + case Elem(prefix, label, attribs, scope, child @ _*) => + Elem(prefix, label, attribs, scope, child map(x => transform(x)) : _*) + case x => x + } + + /* graph / node / edge attributes */ + + private val graphAttributes: Map[String, String] = Map( + "compound" -> "true", + "rankdir" -> "TB" + ) + + private val nodeAttributes = Map( + "shape" -> "rectangle", + "style" -> "filled", + "penwidth" -> "1", + "margin" -> "0.08,0.01", + "width" -> "0.0", + "height" -> "0.0", + "fontname" -> "Arial", + "fontsize" -> "10.00" + ) + + private val edgeAttributes = Map( + "color" -> "#d4d4d4", + "arrowsize" -> "0.5", + "fontcolor" -> "#aaaaaa", + "fontsize" -> "10.00", + "fontname" -> "Arial" + ) + + private val defaultStyle = Map( + "color" -> "#ababab", + "fillcolor" -> "#e1e1e1", + "fontcolor" -> "#7d7d7d", + "margin" -> "0.1,0.04" + ) + + private val implicitStyle = Map( + "color" -> "#ababab", + "fillcolor" -> "#e1e1e1", + "fontcolor" -> "#7d7d7d" + ) + + private val outsideStyle = Map( + "color" -> "#ababab", + "fillcolor" -> "#e1e1e1", + "fontcolor" -> "#7d7d7d" + ) + + private val traitStyle = Map( + "color" -> "#37657D", + "fillcolor" -> "#498AAD", + "fontcolor" -> "#ffffff" + ) + + private val classStyle = Map( + "color" -> "#115F3B", + "fillcolor" -> "#0A955B", + "fontcolor" -> "#ffffff" + ) + + private val objectStyle = Map( + "color" -> "#102966", + "fillcolor" -> "#3556a7", + "fontcolor" -> "#ffffff" + ) + + private def flatten(attributes: Map[String, String]) = attributes.map{ case (key, value) => key + "=\"" + value + "\"" }.mkString(", ") + + private val graphAttributesStr = graphAttributes.map{ case (key, value) => key + "=\"" + value + "\";\n" }.mkString + private val nodeAttributesStr = flatten(nodeAttributes) + private val edgeAttributesStr = flatten(edgeAttributes) +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala new file mode 100644 index 0000000000..37600fa908 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala @@ -0,0 +1,227 @@ +package scala.tools.nsc +package doc +package html +package page +package diagram + +import java.io.InputStream +import java.io.OutputStream +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.io.BufferedWriter +import java.io.BufferedReader +import java.io.IOException +import scala.sys.process._ +import scala.concurrent.SyncVar + +import model._ +import model.diagram._ + +/** This class takes care of running the graphviz dot utility */ +class DotRunner(settings: doc.Settings) { + + private[this] var dotRestarts = 0 + private[this] var dotProcess: DotProcess = null + + def feedToDot(dotInput: String, template: DocTemplateEntity): String = { + + if (dotProcess == null) { + if (dotRestarts < settings.docDiagramsDotRestart.value) { + if (dotRestarts != 0) + settings.printMsg("A new graphviz dot process will be created...\n") + dotRestarts += 1 + dotProcess = new DotProcess(settings) + } else + return null + } + + val tStart = System.currentTimeMillis + val result = dotProcess.feedToDot(dotInput, template.qualifiedName) + val tFinish = System.currentTimeMillis + DiagramStats.addDotRunningTime(tFinish - tStart) + + if (result == null) { + dotProcess.cleanup() + dotProcess = null + if (dotRestarts == settings.docDiagramsDotRestart.value) { + settings.printMsg("\n") + settings.printMsg("**********************************************************************") + settings.printMsg("Diagrams will be disabled for this run beucause the graphviz dot tool") + settings.printMsg("has malfunctioned too many times. These scaladoc flags may help:") + settings.printMsg("") + val baseList = List(settings.docDiagramsDebug, + settings.docDiagramsDotPath, + settings.docDiagramsDotRestart, + settings.docDiagramsDotTimeout) + val width = (baseList map (_.helpSyntax.length)).max + def helpStr(s: doc.Settings#Setting) = ("%-" + width + "s") format (s.helpSyntax) + " " + s.helpDescription + baseList.foreach((sett: doc.Settings#Setting) => settings.printMsg(helpStr(sett))) + settings.printMsg("\nPlease note that graphviz package version 2.26 or above is required.") + settings.printMsg("**********************************************************************\n\n") + + } + } + + result + } + + def cleanup() = + if (dotProcess != null) + dotProcess.cleanup() +} + +class DotProcess(settings: doc.Settings) { + + @volatile var error: Boolean = false // signal an error + val inputString = new SyncVar[String] // used for the dot process input + val outputString = new SyncVar[String] // used for the dot process output + val errorBuffer: StringBuffer = new StringBuffer() // buffer used for both dot process error console AND logging + + // set in only one place, in the main thread + var process: Process = null + var templateName: String = "" + var templateInput: String = "" + + def feedToDot(input: String, template: String): String = { + + templateName = template + templateInput = input + + try { + + // process creation + if (process == null) { + val procIO = new ProcessIO(inputFn(_), outputFn(_), errorFn(_)) + val processBuilder: ProcessBuilder = Seq(settings.docDiagramsDotPath.value, "-Tsvg") + process = processBuilder.run(procIO) + } + + // pass the input and wait for the output + assert(!inputString.isSet) + assert(!outputString.isSet) + inputString.put(input) + var result = outputString.take(settings.docDiagramsDotTimeout.value * 1000) + if (error) result = null + + result + + } catch { + case exc => + errorBuffer.append(" Main thread in " + templateName + ": " + + (if (exc.isInstanceOf[NoSuchElementException]) "Timeout" else "Exception: " + exc)) + error = true + return null + } + } + + def cleanup(): Unit = { + + // we'll need to know if there was any error for reporting + val _error = error + + if (process != null) { + // if there's no error, this should exit cleanly + if (!error) feedToDot("", "") + + // just in case there's any thread hanging, this will take it out of the loop + error = true + process.destroy() + // we'll need to unblock the input again + if (!inputString.isSet) inputString.put("") + if (outputString.isSet) outputString.take() + } + + if (_error) { + if (settings.docDiagramsDebug.value) { + settings.printMsg("\n**********************************************************************") + settings.printMsg("The graphviz dot diagram tool has malfunctioned and will be restarted.") + settings.printMsg("\nThe following is the log of the failure:") + settings.printMsg(errorBuffer.toString) + settings.printMsg(" Cleanup: Last template: " + templateName) + settings.printMsg(" Cleanup: Last dot input: \n " + templateInput.replaceAll("\n","\n ") + "\n") + settings.printMsg(" Cleanup: Dot path: " + settings.docDiagramsDotPath.value) + if (process != null) + settings.printMsg(" Cleanup: Dot exit code: " + process.exitValue) + settings.printMsg("**********************************************************************") + } else { + // we shouldn't just sit there for 50s not reporting anything, no? + settings.printMsg("Graphviz dot encountered an error when generating the diagram for") + settings.printMsg(templateName + ". Use the " + settings.docDiagramsDebug.name + " flag") + settings.printMsg("for more information.") + } + } + } + + /* The standard input passing function */ + private[this] def inputFn(stdin: OutputStream): Unit = { + val writer = new BufferedWriter(new OutputStreamWriter(stdin)) + try { + var input = inputString.take() + + while (!error) { + if (input == "") { + // empty => signal to finish + stdin.close() + return + } else { + // send output to dot + writer.write(input + "\n\n") + writer.flush() + } + + if (!error) input = inputString.take() + } + stdin.close() + } catch { + case exc => + error = true + stdin.close() + errorBuffer.append(" Input thread in " + templateName + ": Exception: " + exc + "\n") + } + } + + private[this] def outputFn(stdOut: InputStream): Unit = { + val reader = new BufferedReader(new InputStreamReader(stdOut)) + var buffer: StringBuilder = new StringBuilder() + try { + var line = reader.readLine + while (!error && line != null) { + buffer.append(line + "\n") + // signal the last element in the svg (only for output) + if (line == "") { + outputString.put(buffer.toString) + buffer.setLength(0) + } + if (error) { stdOut.close(); return } + line = reader.readLine + } + assert(!outputString.isSet) + outputString.put(buffer.toString) + stdOut.close() + } catch { + case exc => + error = true + stdOut.close() + errorBuffer.append(" Output thread in " + templateName + ": Exception: " + exc + "\n") + } + } + + private[this] def errorFn(stdErr: InputStream): Unit = { + val reader = new BufferedReader(new InputStreamReader(stdErr)) + var buffer: StringBuilder = new StringBuilder() + try { + var line = reader.readLine + while (line != null) { + errorBuffer.append(" DOT : " + line + "\n") + error = true + line = reader.readLine + } + stdErr.close() + } catch { + case exc => + error = true + stdErr.close() + errorBuffer.append(" Error thread in " + templateName + ": Exception: " + exc + "\n") + } + } +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/class_diagram.png b/src/compiler/scala/tools/nsc/doc/html/resource/lib/class_diagram.png new file mode 100644 index 0000000000..9d7aec792b Binary files /dev/null and b/src/compiler/scala/tools/nsc/doc/html/resource/lib/class_diagram.png differ diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.css b/src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.css new file mode 100644 index 0000000000..04d29580b7 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.css @@ -0,0 +1,135 @@ +.diagram-container +{ + display: none; +} + +.diagram +{ + overflow: hidden; + padding-top:15px; +} + +.diagram svg +{ + display: block; + position: absolute; + visibility: hidden; + margin: auto; +} + +.diagram-help +{ + float:right; + display:none; +} + +.magnifying +{ + cursor: -webkit-zoom-in ! important; + cursor: -moz-zoom-in ! important; + cursor: pointer; +} + +#close-link +{ + position: absolute; + z-index: 100; + font-family: Arial, sans-serif; + font-size: 10pt; + text-decoration: underline; + color: #315479; +} + +#close:hover +{ + text-decoration: none; +} + +svg a +{ + cursor:pointer; +} + +svg text +{ + font-size: 10px; +} + +/* try to move the node text 1px in order to be vertically + centered (does not work in all browsers) */ +svg .node text +{ + transform: translate(0px,1px); + -ms-transform: translate(0px,1px); + -webkit-transform: translate(0px,1px); + -o-transform: translate(0px,1px); + -moz-transform: translate(0px,1px); +} + +/* hover effect for edges */ + +svg .edge.over text, +svg .edge.implicit-incoming.over polygon, +svg .edge.implicit-outgoing.over polygon +{ + fill: #202020; +} + +svg .edge.over path, +svg .edge.over polygon +{ + stroke: #202020; +} + +/* hover effect for nodes in class diagrams */ + +svg.class-diagram .node +{ + opacity: 0.75; +} + +svg.class-diagram .node.this +{ + opacity: 1.0; +} + +svg.class-diagram .node.over +{ + opacity: 1.0; +} + +svg .node.over polygon +{ + stroke: #202020; +} + +/* hover effect for nodes in package diagrams */ + +svg.package-diagram .node.class.over polygon, +svg.class-diagram .node.this.class.over polygon +{ + fill: #098552; + fill: #04663e; +} + +svg.package-diagram .node.trait.over polygon, +svg.class-diagram .node.this.trait.over polygon +{ + fill: #3c7b9b; + fill: #235d7b; +} + +svg.package-diagram .node.object.over polygon +{ + fill: #183377; +} + +svg.package-diagram .node.outside.over polygon +{ + fill: #d4d4d4; +} + +svg.package-diagram .node.default.over polygon +{ + fill: #d4d4d4; +} diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.js b/src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.js new file mode 100644 index 0000000000..478f2e38ac --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/diagrams.js @@ -0,0 +1,324 @@ +/** + * JavaScript functions enhancing the SVG diagrams. + * + * @author Damien Obrist + */ + +var diagrams = {}; + +/** + * Initializes the diagrams in the main window. + */ +$(document).ready(function() +{ + // hide diagrams in browsers not supporting SVG + if(Modernizr && !Modernizr.inlinesvg) + return; + + // only execute this in the main window + if(diagrams.isPopup) + return; + + if($("#content-diagram").length) + $("#inheritance-diagram").css("padding-bottom", "20px"); + + $(".diagram-container").css("display", "block"); + + $(".diagram").each(function() { + // store inital dimensions + $(this).data("width", $("svg", $(this)).width()); + $(this).data("height", $("svg", $(this)).height()); + // store unscaled clone of SVG element + $(this).data("svg", $(this).get(0).childNodes[0].cloneNode(true)); + }); + + // make diagram visible, hide container + $(".diagram").css("display", "none"); + $(".diagram svg").css({ + "position": "static", + "visibility": "visible", + "z-index": "auto" + }); + + // enable linking to diagrams + if($(location).attr("hash") == "#inheritance-diagram") { + diagrams.toggle($("#inheritance-diagram-container"), true); + } else if($(location).attr("hash") == "#content-diagram") { + diagrams.toggle($("#content-diagram-container"), true); + } + + $(".diagram-link").click(function() { + diagrams.toggle($(this).parent()); + }); + + // register resize function + $(window).resize(diagrams.resize); + + // don't bubble event to parent div + // when clicking on a node of a resized + // diagram + $("svg a").click(function(e) { + e.stopPropagation(); + }); + + diagrams.initHighlighting(); +}); + +/** + * Initializes the diagrams in the popup. + */ +diagrams.initPopup = function(id) +{ + // copy diagram from main window + if(!jQuery.browser.msie) + $("body").append(opener.$("#" + id).data("svg")); + + // positioning + $("svg").css("position", "absolute"); + $(window).resize(function() + { + var svg_w = $("svg").css("width").replace("px", ""); + var svg_h = $("svg").css("height").replace("px", ""); + var x = $(window).width() / 2 - svg_w / 2; + if(x < 0) x = 0; + var y = $(window).height() / 2 - svg_h / 2; + if(y < 0) y = 0; + $("svg").css("left", x + "px"); + $("svg").css("top", y + "px"); + }); + $(window).resize(); + + diagrams.initHighlighting(); + $("svg a").click(function(e) { + opener.diagrams.redirectFromPopup(this.href.baseVal); + window.close(); + }); + $(document).keyup(function(e) { + if (e.keyCode == 27) window.close(); + }); +} + +/** + * Initializes highlighting for nodes and edges. + */ +diagrams.initHighlighting = function() +{ + // helper function since $.hover doesn't work in IE + + function hover(elements, fn) + { + elements.mouseover(fn); + elements.mouseout(fn); + } + + // inheritance edges + + hover($("svg .edge.inheritance"), function(evt){ + var toggleClass = evt.type == "mouseout" ? diagrams.removeClass : diagrams.addClass; + var parts = $(this).attr("id").split("_"); + toggleClass($("#" + parts[0] + "_" + parts[1])); + toggleClass($("#" + parts[0] + "_" + parts[2])); + toggleClass($(this)); + }); + + // nodes + + hover($("svg .node"), function(evt){ + var toggleClass = evt.type == "mouseout" ? diagrams.removeClass : diagrams.addClass; + toggleClass($(this)); + var parts = $(this).attr("id").split("_"); + var index = parts[1]; + $("svg#" + parts[0] + " .edge.inheritance").each(function(){ + var parts2 = $(this).attr("id").split("_"); + if(parts2[1] == index) + { + toggleClass($("#" + parts2[0] + "_" + parts2[2])); + toggleClass($(this)); + } else if(parts2[2] == index) + { + toggleClass($("#" + parts2[0] + "_" + parts2[1])); + toggleClass($(this)); + } + }); + }); + + // incoming implicits + + hover($("svg .node.implicit-incoming"), function(evt){ + var toggleClass = evt.type == "mouseout" ? diagrams.removeClass : diagrams.addClass; + toggleClass($(this)); + toggleClass($("svg .edge.implicit-incoming")); + toggleClass($("svg .node.this")); + }); + + hover($("svg .edge.implicit-incoming"), function(evt){ + var toggleClass = evt.type == "mouseout" ? diagrams.removeClass : diagrams.addClass; + toggleClass($(this)); + toggleClass($("svg .node.this")); + $("svg .node.implicit-incoming").each(function(){ + toggleClass($(this)); + }); + }); + + // implicit outgoing nodes + + hover($("svg .node.implicit-outgoing"), function(evt){ + var toggleClass = evt.type == "mouseout" ? diagrams.removeClass : diagrams.addClass; + toggleClass($(this)); + toggleClass($("svg .edge.implicit-outgoing")); + toggleClass($("svg .node.this")); + }); + + hover($("svg .edge.implicit-outgoing"), function(evt){ + var toggleClass = evt.type == "mouseout" ? diagrams.removeClass : diagrams.addClass; + toggleClass($(this)); + toggleClass($("svg .node.this")); + $("svg .node.implicit-outgoing").each(function(){ + toggleClass($(this)); + }); + }); +}; + +/** + * Resizes the diagrams according to the available width. + */ +diagrams.resize = function() +{ + // available width + var availableWidth = $("body").width() - 20; + + $(".diagram-container").each(function() { + // unregister click event on whole div + $(".diagram", this).unbind("click"); + var diagramWidth = $(".diagram", this).data("width"); + var diagramHeight = $(".diagram", this).data("height"); + + if(diagramWidth > availableWidth) + { + // resize diagram + var height = diagramHeight / diagramWidth * availableWidth; + $(".diagram svg", this).width(availableWidth); + $(".diagram svg", this).height(height); + + // register click event on whole div + $(".diagram", this).click(function() { + diagrams.popup($(this)); + }); + $(".diagram", this).addClass("magnifying"); + } + else + { + // restore full size of diagram + $(".diagram svg", this).width(diagramWidth); + $(".diagram svg", this).height(diagramHeight); + // don't show custom cursor any more + $(".diagram", this).removeClass("magnifying"); + } + }); +}; + +/** + * Shows or hides a diagram depending on its current state. + */ +diagrams.toggle = function(container, dontAnimate) +{ + // change class of link + $(".diagram-link", container).toggleClass("open"); + // get element to show / hide + var div = $(".diagram", container); + if (div.is(':visible')) + { + $(".diagram-help", container).hide(); + div.unbind("click"); + div.removeClass("magnifying"); + div.slideUp(100); + } + else + { + diagrams.resize(); + if(dontAnimate) + div.show(); + else + div.slideDown(100); + $(".diagram-help", container).show(); + } +}; + +/** + * Opens a popup containing a copy of a diagram. + */ +diagrams.windows = {}; +diagrams.popup = function(diagram) +{ + var id = diagram.attr("id"); + if(!diagrams.windows[id] || diagrams.windows[id].closed) { + var title = $(".symbol .name", $("#signature")).text(); + // cloning from parent window to popup somehow doesn't work in IE + // therefore include the SVG as a string into the HTML + var svgIE = jQuery.browser.msie ? $("
    ").append(diagram.data("svg")).html() : ""; + var html = '' + + '\n' + + '\n' + + '\n' + + ' \n' + + ' ' + title + '\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' Close this window\n' + + ' ' + svgIE + '\n' + + ' \n' + + ''; + + var padding = 30; + var screenHeight = screen.availHeight; + var screenWidth = screen.availWidth; + var w = Math.min(screenWidth, diagram.data("width") + 2 * padding); + var h = Math.min(screenHeight, diagram.data("height") + 2 * padding); + var left = (screenWidth - w) / 2; + var top = (screenHeight - h) / 2; + var parameters = "height=" + h + ", width=" + w + ", left=" + left + ", top=" + top + ", scrollbars=yes, location=no, resizable=yes"; + var win = window.open("about:blank", "_blank", parameters); + win.document.open(); + win.document.write(html); + win.document.close(); + diagrams.windows[id] = win; + } + win.focus(); +}; + +/** + * This method is called from within the popup when a node is clicked. + */ +diagrams.redirectFromPopup = function(url) +{ + window.location = url; +}; + +/** + * Helper method that adds a class to a SVG element. + */ +diagrams.addClass = function(svgElem, newClass) { + newClass = newClass || "over"; + var classes = svgElem.attr("class"); + if ($.inArray(newClass, classes.split(/\s+/)) == -1) { + classes += (classes ? ' ' : '') + newClass; + svgElem.attr("class", classes); + } +}; + +/** + * Helper method that removes a class from a SVG element. + */ +diagrams.removeClass = function(svgElem, oldClass) { + oldClass = oldClass || "over"; + var classes = svgElem.attr("class"); + classes = $.grep(classes.split(/\s+/), function(n, i) { return n != oldClass; }).join(' '); + svgElem.attr("class", classes); +}; + diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/modernizr.custom.js b/src/compiler/scala/tools/nsc/doc/html/resource/lib/modernizr.custom.js new file mode 100644 index 0000000000..4688d633fe --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/modernizr.custom.js @@ -0,0 +1,4 @@ +/* Modernizr 2.5.3 (Custom Build) | MIT & BSD + * Build: http://www.modernizr.com/download/#-inlinesvg + */ +;window.Modernizr=function(a,b,c){function u(a){i.cssText=a}function v(a,b){return u(prefixes.join(a+";")+(b||""))}function w(a,b){return typeof a===b}function x(a,b){return!!~(""+a).indexOf(b)}function y(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:w(f,"function")?f.bind(d||b):f}return!1}var d="2.5.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s={}.hasOwnProperty,t;!w(s,"undefined")&&!w(s.call,"undefined")?t=function(a,b){return s.call(a,b)}:t=function(a,b){return b in a&&w(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==l.svg};for(var z in m)t(m,z)&&(r=z.toLowerCase(),e[r]=m[z](),p.push((e[r]?"":"no-")+r));return u(""),h=j=null,e._version=d,e}(this,this.document); \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/object_diagram.png b/src/compiler/scala/tools/nsc/doc/html/resource/lib/object_diagram.png new file mode 100644 index 0000000000..6e9f2f743f Binary files /dev/null and b/src/compiler/scala/tools/nsc/doc/html/resource/lib/object_diagram.png differ diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/raphael-min.js b/src/compiler/scala/tools/nsc/doc/html/resource/lib/raphael-min.js new file mode 100644 index 0000000000..d30dbad858 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/raphael-min.js @@ -0,0 +1,10 @@ +// ┌────────────────────────────────────────────────────────────────────┐ \\ +// │ Raphaël 2.1.0 - JavaScript Vector Library │ \\ +// ├────────────────────────────────────────────────────────────────────┤ \\ +// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ +// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\ +// ├────────────────────────────────────────────────────────────────────┤ \\ +// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ +// └────────────────────────────────────────────────────────────────────┘ \\ + +(function(a){var b="0.3.4",c="hasOwnProperty",d=/[\.\/]/,e="*",f=function(){},g=function(a,b){return a-b},h,i,j={n:{}},k=function(a,b){var c=j,d=i,e=Array.prototype.slice.call(arguments,2),f=k.listeners(a),l=0,m=!1,n,o=[],p={},q=[],r=h,s=[];h=a,i=0;for(var t=0,u=f.length;tf*b.top){e=b.percents[y],p=b.percents[y-1]||0,t=t/b.top*(e-p),o=b.percents[y+1],j=b.anim[e];break}f&&d.attr(b.anim[b.percents[y]])}if(!!j){if(!k){for(var A in j)if(j[g](A))if(U[g](A)||d.paper.customAttributes[g](A)){u[A]=d.attr(A),u[A]==null&&(u[A]=T[A]),v[A]=j[A];switch(U[A]){case C:w[A]=(v[A]-u[A])/t;break;case"colour":u[A]=a.getRGB(u[A]);var B=a.getRGB(v[A]);w[A]={r:(B.r-u[A].r)/t,g:(B.g-u[A].g)/t,b:(B.b-u[A].b)/t};break;case"path":var D=bR(u[A],v[A]),E=D[1];u[A]=D[0],w[A]=[];for(y=0,z=u[A].length;yd)return d;while(cf?c=e:d=e,e=(d-c)/2+c}return e}function n(a,b){var c=o(a,b);return((l*c+k)*c+j)*c}function m(a){return((i*a+h)*a+g)*a}var g=3*b,h=3*(d-b)-g,i=1-g-h,j=3*c,k=3*(e-c)-j,l=1-j-k;return n(a,1/(200*f))}function cq(){return this.x+q+this.y+q+this.width+" × "+this.height}function cp(){return this.x+q+this.y}function cb(a,b,c,d,e,f){a!=null?(this.a=+a,this.b=+b,this.c=+c,this.d=+d,this.e=+e,this.f=+f):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0)}function bH(b,c,d){b=a._path2curve(b),c=a._path2curve(c);var e,f,g,h,i,j,k,l,m,n,o=d?0:[];for(var p=0,q=b.length;p=0&&y<=1&&A>=0&&A<=1&&(d?n++:n.push({x:x.x,y:x.y,t1:y,t2:A}))}}return n}function bF(a,b){return bG(a,b,1)}function bE(a,b){return bG(a,b)}function bD(a,b,c,d,e,f,g,h){if(!(x(a,c)x(e,g)||x(b,d)x(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(!k)return;var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(n<+y(a,c).toFixed(2)||n>+x(a,c).toFixed(2)||n<+y(e,g).toFixed(2)||n>+x(e,g).toFixed(2)||o<+y(b,d).toFixed(2)||o>+x(b,d).toFixed(2)||o<+y(f,h).toFixed(2)||o>+x(f,h).toFixed(2))return;return{x:l,y:m}}}function bC(a,b,c,d,e,f,g,h,i){if(!(i<0||bB(a,b,c,d,e,f,g,h)n)k/=2,l+=(m1?1:i<0?0:i;var j=i/2,k=12,l=[-0.1252,.1252,-0.3678,.3678,-0.5873,.5873,-0.7699,.7699,-0.9041,.9041,-0.9816,.9816],m=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],n=0;for(var o=0;od;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}function bx(){return this.hex}function bv(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),h=d.cache=d.cache||{},i=d.count=d.count||[];if(h[g](f)){bu(i,f);return c?c(h[f]):h[f]}i.length>=1e3&&delete h[i.shift()],i.push(f),h[f]=a[m](b,e);return c?c(h[f]):h[f]}return d}function bu(a,b){for(var c=0,d=a.length;c',bl=bk.firstChild,bl.style.behavior="url(#default#VML)";if(!bl||typeof bl.adj!="object")return a.type=p;bk=null}a.svg=!(a.vml=a.type=="VML"),a._Paper=j,a.fn=k=j.prototype=a.prototype,a._id=0,a._oid=0,a.is=function(a,b){b=v.call(b);if(b=="finite")return!M[g](+a);if(b=="array")return a instanceof Array;return b=="null"&&a===null||b==typeof a&&a!==null||b=="object"&&a===Object(a)||b=="array"&&Array.isArray&&Array.isArray(a)||H.call(a).slice(8,-1).toLowerCase()==b},a.angle=function(b,c,d,e,f,g){if(f==null){var h=b-d,i=c-e;if(!h&&!i)return 0;return(180+w.atan2(-i,-h)*180/B+360)%360}return a.angle(b,c,f,g)-a.angle(d,e,f,g)},a.rad=function(a){return a%360*B/180},a.deg=function(a){return a*180/B%360},a.snapTo=function(b,c,d){d=a.is(d,"finite")?d:10;if(a.is(b,E)){var e=b.length;while(e--)if(z(b[e]-c)<=d)return b[e]}else{b=+b;var f=c%b;if(fb-d)return c-f+b}return c};var bn=a.createUUID=function(a,b){return function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(a,b).toUpperCase()}}(/[xy]/g,function(a){var b=w.random()*16|0,c=a=="x"?b:b&3|8;return c.toString(16)});a.setWindow=function(b){eve("raphael.setWindow",a,h.win,b),h.win=b,h.doc=h.win.document,a._engine.initWin&&a._engine.initWin(h.win)};var bo=function(b){if(a.vml){var c=/^\s+|\s+$/g,d;try{var e=new ActiveXObject("htmlfile");e.write(""),e.close(),d=e.body}catch(f){d=createPopup().document.body}var g=d.createTextRange();bo=bv(function(a){try{d.style.color=r(a).replace(c,p);var b=g.queryCommandValue("ForeColor");b=(b&255)<<16|b&65280|(b&16711680)>>>16;return"#"+("000000"+b.toString(16)).slice(-6)}catch(e){return"none"}})}else{var i=h.doc.createElement("i");i.title="Raphaël Colour Picker",i.style.display="none",h.doc.body.appendChild(i),bo=bv(function(a){i.style.color=a;return h.doc.defaultView.getComputedStyle(i,p).getPropertyValue("color")})}return bo(b)},bp=function(){return"hsb("+[this.h,this.s,this.b]+")"},bq=function(){return"hsl("+[this.h,this.s,this.l]+")"},br=function(){return this.hex},bs=function(b,c,d){c==null&&a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b&&(d=b.b,c=b.g,b=b.r);if(c==null&&a.is(b,D)){var e=a.getRGB(b);b=e.r,c=e.g,d=e.b}if(b>1||c>1||d>1)b/=255,c/=255,d/=255;return[b,c,d]},bt=function(b,c,d,e){b*=255,c*=255,d*=255;var f={r:b,g:c,b:d,hex:a.rgb(b,c,d),toString:br};a.is(e,"finite")&&(f.opacity=e);return f};a.color=function(b){var c;a.is(b,"object")&&"h"in b&&"s"in b&&"b"in b?(c=a.hsb2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):a.is(b,"object")&&"h"in b&&"s"in b&&"l"in b?(c=a.hsl2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):(a.is(b,"string")&&(b=a.getRGB(b)),a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b?(c=a.rgb2hsl(b),b.h=c.h,b.s=c.s,b.l=c.l,c=a.rgb2hsb(b),b.v=c.b):(b={hex:"none"},b.r=b.g=b.b=b.h=b.s=b.v=b.l=-1)),b.toString=br;return b},a.hsb2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,a=a.h,d=a.o),a*=360;var e,f,g,h,i;a=a%360/60,i=c*b,h=i*(1-z(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return bt(e,f,g,d)},a.hsl2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h);if(a>1||b>1||c>1)a/=360,b/=100,c/=100;a*=360;var e,f,g,h,i;a=a%360/60,i=2*b*(c<.5?c:1-c),h=i*(1-z(a%2-1)),e=f=g=c-i/2,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return bt(e,f,g,d)},a.rgb2hsb=function(a,b,c){c=bs(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;f=x(a,b,c),g=f-y(a,b,c),d=g==0?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=g==0?0:g/f;return{h:d,s:e,b:f,toString:bp}},a.rgb2hsl=function(a,b,c){c=bs(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;g=x(a,b,c),h=y(a,b,c),i=g-h,d=i==0?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=i==0?0:f<.5?i/(2*f):i/(2-2*f);return{h:d,s:e,l:f,toString:bq}},a._path2string=function(){return this.join(",").replace(Y,"$1")};var bw=a._preload=function(a,b){var c=h.doc.createElement("img");c.style.cssText="position:absolute;left:-9999em;top:-9999em",c.onload=function(){b.call(this),this.onload=null,h.doc.body.removeChild(this)},c.onerror=function(){h.doc.body.removeChild(this)},h.doc.body.appendChild(c),c.src=a};a.getRGB=bv(function(b){if(!b||!!((b=r(b)).indexOf("-")+1))return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bx};if(b=="none")return{r:-1,g:-1,b:-1,hex:"none",toString:bx};!X[g](b.toLowerCase().substring(0,2))&&b.charAt()!="#"&&(b=bo(b));var c,d,e,f,h,i,j,k=b.match(L);if(k){k[2]&&(f=R(k[2].substring(5),16),e=R(k[2].substring(3,5),16),d=R(k[2].substring(1,3),16)),k[3]&&(f=R((i=k[3].charAt(3))+i,16),e=R((i=k[3].charAt(2))+i,16),d=R((i=k[3].charAt(1))+i,16)),k[4]&&(j=k[4][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),k[1].toLowerCase().slice(0,4)=="rgba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100));if(k[5]){j=k[5][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsb2rgb(d,e,f,h)}if(k[6]){j=k[6][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsla"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsl2rgb(d,e,f,h)}k={r:d,g:e,b:f,toString:bx},k.hex="#"+(16777216|f|e<<8|d<<16).toString(16).slice(1),a.is(h,"finite")&&(k.opacity=h);return k}return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bx}},a),a.hsb=bv(function(b,c,d){return a.hsb2rgb(b,c,d).hex}),a.hsl=bv(function(b,c,d){return a.hsl2rgb(b,c,d).hex}),a.rgb=bv(function(a,b,c){return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)}),a.getColor=function(a){var b=this.getColor.start=this.getColor.start||{h:0,s:1,b:a||.75},c=this.hsb2rgb(b.h,b.s,b.b);b.h+=.075,b.h>1&&(b.h=0,b.s-=.2,b.s<=0&&(this.getColor.start={h:0,s:1,b:b.b}));return c.hex},a.getColor.reset=function(){delete this.start},a.parsePathString=function(b){if(!b)return null;var c=bz(b);if(c.arr)return bJ(c.arr);var d={a:7,c:6,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,z:0},e=[];a.is(b,E)&&a.is(b[0],E)&&(e=bJ(b)),e.length||r(b).replace(Z,function(a,b,c){var f=[],g=b.toLowerCase();c.replace(_,function(a,b){b&&f.push(+b)}),g=="m"&&f.length>2&&(e.push([b][n](f.splice(0,2))),g="l",b=b=="m"?"l":"L");if(g=="r")e.push([b][n](f));else while(f.length>=d[g]){e.push([b][n](f.splice(0,d[g])));if(!d[g])break}}),e.toString=a._path2string,c.arr=bJ(e);return e},a.parseTransformString=bv(function(b){if(!b)return null;var c={r:3,s:4,t:2,m:6},d=[];a.is(b,E)&&a.is(b[0],E)&&(d=bJ(b)),d.length||r(b).replace($,function(a,b,c){var e=[],f=v.call(b);c.replace(_,function(a,b){b&&e.push(+b)}),d.push([b][n](e))}),d.toString=a._path2string;return d});var bz=function(a){var b=bz.ps=bz.ps||{};b[a]?b[a].sleep=100:b[a]={sleep:100},setTimeout(function(){for(var c in b)b[g](c)&&c!=a&&(b[c].sleep--,!b[c].sleep&&delete b[c])});return b[a]};a.findDotsAtSegment=function(a,b,c,d,e,f,g,h,i){var j=1-i,k=A(j,3),l=A(j,2),m=i*i,n=m*i,o=k*a+l*3*i*c+j*3*i*i*e+n*g,p=k*b+l*3*i*d+j*3*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,x=j*e+i*g,y=j*f+i*h,z=90-w.atan2(q-s,r-t)*180/B;(q>s||r=a.x&&b<=a.x2&&c>=a.y&&c<=a.y2},a.isBBoxIntersect=function(b,c){var d=a.isPointInsideBBox;return d(c,b.x,b.y)||d(c,b.x2,b.y)||d(c,b.x,b.y2)||d(c,b.x2,b.y2)||d(b,c.x,c.y)||d(b,c.x2,c.y)||d(b,c.x,c.y2)||d(b,c.x2,c.y2)||(b.xc.x||c.xb.x)&&(b.yc.y||c.yb.y)},a.pathIntersection=function(a,b){return bH(a,b)},a.pathIntersectionNumber=function(a,b){return bH(a,b,1)},a.isPointInsidePath=function(b,c,d){var e=a.pathBBox(b);return a.isPointInsideBBox(e,c,d)&&bH(b,[["M",c,d],["H",e.x2+10]],1)%2==1},a._removedFactory=function(a){return function(){eve("raphael.log",null,"Raphaël: you are calling to method “"+a+"” of removed object",a)}};var bI=a.pathBBox=function(a){var b=bz(a);if(b.bbox)return b.bbox;if(!a)return{x:0,y:0,width:0,height:0,x2:0,y2:0};a=bR(a);var c=0,d=0,e=[],f=[],g;for(var h=0,i=a.length;h1&&(v=w.sqrt(v),c=v*c,d=v*d);var x=c*c,y=d*d,A=(f==g?-1:1)*w.sqrt(z((x*y-x*u*u-y*t*t)/(x*u*u+y*t*t))),C=A*c*u/d+(a+h)/2,D=A*-d*t/c+(b+i)/2,E=w.asin(((b-D)/d).toFixed(9)),F=w.asin(((i-D)/d).toFixed(9));E=aF&&(E=E-B*2),!g&&F>E&&(F=F-B*2)}else E=j[0],F=j[1],C=j[2],D=j[3];var G=F-E;if(z(G)>k){var H=F,I=h,J=i;F=E+k*(g&&F>E?1:-1),h=C+c*w.cos(F),i=D+d*w.sin(F),m=bO(h,i,c,d,e,0,g,I,J,[F,H,C,D])}G=F-E;var K=w.cos(E),L=w.sin(E),M=w.cos(F),N=w.sin(F),O=w.tan(G/4),P=4/3*c*O,Q=4/3*d*O,R=[a,b],S=[a+P*L,b-Q*K],T=[h+P*N,i-Q*M],U=[h,i];S[0]=2*R[0]-S[0],S[1]=2*R[1]-S[1];if(j)return[S,T,U][n](m);m=[S,T,U][n](m).join()[s](",");var V=[];for(var W=0,X=m.length;W"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bP(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bP(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y)),i=f-2*d+b-(h-2*f+d),j=2*(d-b)-2*(f-d),k=b-d,l=(-j+w.sqrt(j*j-4*i*k))/2/i,n=(-j-w.sqrt(j*j-4*i*k))/2/i,z(l)>"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bP(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bP(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y));return{min:{x:y[m](0,p),y:y[m](0,o)},max:{x:x[m](0,p),y:x[m](0,o)}}}),bR=a._path2curve=bv(function(a,b){var c=!b&&bz(a);if(!b&&c.curve)return bJ(c.curve);var d=bL(a),e=b&&bL(b),f={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},g={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},h=function(a,b){var c,d;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null);switch(a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"][n](bO[m](0,[b.x,b.y][n](a.slice(1))));break;case"S":c=b.x+(b.x-(b.bx||b.x)),d=b.y+(b.y-(b.by||b.y)),a=["C",c,d][n](a.slice(1));break;case"T":b.qx=b.x+(b.x-(b.qx||b.x)),b.qy=b.y+(b.y-(b.qy||b.y)),a=["C"][n](bN(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"][n](bN(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"][n](bM(b.x,b.y,a[1],a[2]));break;case"H":a=["C"][n](bM(b.x,b.y,a[1],b.y));break;case"V":a=["C"][n](bM(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"][n](bM(b.x,b.y,b.X,b.Y))}return a},i=function(a,b){if(a[b].length>7){a[b].shift();var c=a[b];while(c.length)a.splice(b++,0,["C"][n](c.splice(0,6)));a.splice(b,1),l=x(d.length,e&&e.length||0)}},j=function(a,b,c,f,g){a&&b&&a[g][0]=="M"&&b[g][0]!="M"&&(b.splice(g,0,["M",f.x,f.y]),c.bx=0,c.by=0,c.x=a[g][1],c.y=a[g][2],l=x(d.length,e&&e.length||0))};for(var k=0,l=x(d.length,e&&e.length||0);ke){if(c&&!l.start){m=cs(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n),k+=["C"+m.start.x,m.start.y,m.m.x,m.m.y,m.x,m.y];if(f)return k;l.start=k,k=["M"+m.x,m.y+"C"+m.n.x,m.n.y,m.end.x,m.end.y,i[5],i[6]].join(),n+=j,g=+i[5],h=+i[6];continue}if(!b&&!c){m=cs(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);return{x:m.x,y:m.y,alpha:m.alpha}}}n+=j,g=+i[5],h=+i[6]}k+=i.shift()+i}l.end=k,m=b?n:c?l:a.findDotsAtSegment(g,h,i[0],i[1],i[2],i[3],i[4],i[5],1),m.alpha&&(m={x:m.x,y:m.y,alpha:m.alpha});return m}},cu=ct(1),cv=ct(),cw=ct(0,1);a.getTotalLength=cu,a.getPointAtLength=cv,a.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return cw(a,b).end;var d=cw(a,c,1);return b?cw(d,b).end:d},cl.getTotalLength=function(){if(this.type=="path"){if(this.node.getTotalLength)return this.node.getTotalLength();return cu(this.attrs.path)}},cl.getPointAtLength=function(a){if(this.type=="path")return cv(this.attrs.path,a)},cl.getSubpath=function(b,c){if(this.type=="path")return a.getSubpath(this.attrs.path,b,c)};var cx=a.easing_formulas={linear:function(a){return a},"<":function(a){return A(a,1.7)},">":function(a){return A(a,.48)},"<>":function(a){var b=.48-a/1.04,c=w.sqrt(.1734+b*b),d=c-b,e=A(z(d),1/3)*(d<0?-1:1),f=-c-b,g=A(z(f),1/3)*(f<0?-1:1),h=e+g+.5;return(1-h)*3*h*h+h*h*h},backIn:function(a){var b=1.70158;return a*a*((b+1)*a-b)},backOut:function(a){a=a-1;var b=1.70158;return a*a*((b+1)*a+b)+1},elastic:function(a){if(a==!!a)return a;return A(2,-10*a)*w.sin((a-.075)*2*B/.3)+1},bounce:function(a){var b=7.5625,c=2.75,d;a<1/c?d=b*a*a:a<2/c?(a-=1.5/c,d=b*a*a+.75):a<2.5/c?(a-=2.25/c,d=b*a*a+.9375):(a-=2.625/c,d=b*a*a+.984375);return d}};cx.easeIn=cx["ease-in"]=cx["<"],cx.easeOut=cx["ease-out"]=cx[">"],cx.easeInOut=cx["ease-in-out"]=cx["<>"],cx["back-in"]=cx.backIn,cx["back-out"]=cx.backOut;var cy=[],cz=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){setTimeout(a,16)},cA=function(){var b=+(new Date),c=0;for(;c1&&!d.next){for(s in k)k[g](s)&&(r[s]=d.totalOrigin[s]);d.el.attr(r),cE(d.anim,d.el,d.anim.percents[0],null,d.totalOrigin,d.repeat-1)}d.next&&!d.stop&&cE(d.anim,d.el,d.next,null,d.totalOrigin,d.repeat)}}a.svg&&m&&m.paper&&m.paper.safari(),cy.length&&cz(cA)},cB=function(a){return a>255?255:a<0?0:a};cl.animateWith=function(b,c,d,e,f,g){var h=this;if(h.removed){g&&g.call(h);return h}var i=d instanceof cD?d:a.animation(d,e,f,g),j,k;cE(i,h,i.percents[0],null,h.attr());for(var l=0,m=cy.length;l.5)*2-1;i(m-.5,2)+i(n-.5,2)>.25&&(n=f.sqrt(.25-i(m-.5,2))*e+.5)&&n!=.5&&(n=n.toFixed(5)-1e-5*e)}return l}),e=e.split(/\s*\-\s*/);if(j=="linear"){var t=e.shift();t=-d(t);if(isNaN(t))return null;var u=[0,0,f.cos(a.rad(t)),f.sin(a.rad(t))],v=1/(g(h(u[2]),h(u[3]))||1);u[2]*=v,u[3]*=v,u[2]<0&&(u[0]=-u[2],u[2]=0),u[3]<0&&(u[1]=-u[3],u[3]=0)}var w=a._parseDots(e);if(!w)return null;k=k.replace(/[\(\)\s,\xb0#]/g,"_"),b.gradient&&k!=b.gradient.id&&(p.defs.removeChild(b.gradient),delete b.gradient);if(!b.gradient){s=q(j+"Gradient",{id:k}),b.gradient=s,q(s,j=="radial"?{fx:m,fy:n}:{x1:u[0],y1:u[1],x2:u[2],y2:u[3],gradientTransform:b.matrix.invert()}),p.defs.appendChild(s);for(var x=0,y=w.length;x1?G.opacity/100:G.opacity});case"stroke":G=a.getRGB(p),i.setAttribute(o,G.hex),o=="stroke"&&G[b]("opacity")&&q(i,{"stroke-opacity":G.opacity>1?G.opacity/100:G.opacity}),o=="stroke"&&d._.arrows&&("startString"in d._.arrows&&t(d,d._.arrows.startString),"endString"in d._.arrows&&t(d,d._.arrows.endString,1));break;case"gradient":(d.type=="circle"||d.type=="ellipse"||c(p).charAt()!="r")&&r(d,p);break;case"opacity":k.gradient&&!k[b]("stroke-opacity")&&q(i,{"stroke-opacity":p>1?p/100:p});case"fill-opacity":if(k.gradient){H=a._g.doc.getElementById(i.getAttribute("fill").replace(/^url\(#|\)$/g,l)),H&&(I=H.getElementsByTagName("stop"),q(I[I.length-1],{"stop-opacity":p}));break};default:o=="font-size"&&(p=e(p,10)+"px");var J=o.replace(/(\-.)/g,function(a){return a.substring(1).toUpperCase()});i.style[J]=p,d._.dirty=1,i.setAttribute(o,p)}}y(d,f),i.style.visibility=m},x=1.2,y=function(d,f){if(d.type=="text"&&!!(f[b]("text")||f[b]("font")||f[b]("font-size")||f[b]("x")||f[b]("y"))){var g=d.attrs,h=d.node,i=h.firstChild?e(a._g.doc.defaultView.getComputedStyle(h.firstChild,l).getPropertyValue("font-size"),10):10;if(f[b]("text")){g.text=f.text;while(h.firstChild)h.removeChild(h.firstChild);var j=c(f.text).split("\n"),k=[],m;for(var n=0,o=j.length;n"));var $=X.getBoundingClientRect();t.W=m.w=($.right-$.left)/Y,t.H=m.h=($.bottom-$.top)/Y,t.X=m.x,t.Y=m.y+t.H/2,("x"in i||"y"in i)&&(t.path.v=a.format("m{0},{1}l{2},{1}",f(m.x*u),f(m.y*u),f(m.x*u)+1));var _=["x","y","text","font","font-family","font-weight","font-style","font-size"];for(var ba=0,bb=_.length;ba.25&&(c=e.sqrt(.25-i(b-.5,2))*((c>.5)*2-1)+.5),m=b+n+c);return o}),f=f.split(/\s*\-\s*/);if(l=="linear"){var p=f.shift();p=-d(p);if(isNaN(p))return null}var q=a._parseDots(f);if(!q)return null;b=b.shape||b.node;if(q.length){b.removeChild(g),g.on=!0,g.method="none",g.color=q[0].color,g.color2=q[q.length-1].color;var r=[];for(var s=0,t=q.length;s')}}catch(c){F=function(a){return b.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">')}}},a._engine.initWin(a._g.win),a._engine.create=function(){var b=a._getContainer.apply(0,arguments),c=b.container,d=b.height,e,f=b.width,g=b.x,h=b.y;if(!c)throw new Error("VML container not found.");var i=new a._Paper,j=i.canvas=a._g.doc.createElement("div"),k=j.style;g=g||0,h=h||0,f=f||512,d=d||342,i.width=f,i.height=d,f==+f&&(f+="px"),d==+d&&(d+="px"),i.coordsize=u*1e3+n+u*1e3,i.coordorigin="0 0",i.span=a._g.doc.createElement("span"),i.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;",j.appendChild(i.span),k.cssText=a.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden",f,d),c==1?(a._g.doc.body.appendChild(j),k.left=g+"px",k.top=h+"px",k.position="absolute"):c.firstChild?c.insertBefore(j,c.firstChild):c.appendChild(j),i.renderfix=function(){};return i},a.prototype.clear=function(){a.eve("raphael.clear",this),this.canvas.innerHTML=o,this.span=a._g.doc.createElement("span"),this.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;",this.canvas.appendChild(this.span),this.bottom=this.top=null},a.prototype.remove=function(){a.eve("raphael.remove",this),this.canvas.parentNode.removeChild(this.canvas);for(var b in this)this[b]=typeof this[b]=="function"?a._removedFactory(b):null;return!0};var G=a.st;for(var H in E)E[b](H)&&!G[b](H)&&(G[H]=function(a){return function(){var b=arguments;return this.forEach(function(c){c[a].apply(c,b)})}}(H))}(window.Raphael) \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css index b25f0d0b3f..1bee55313b 100644 --- a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css @@ -163,7 +163,7 @@ text-decoration: none; background: url("arrow-right.png") no-repeat 0 3px transparent; } -.toggleContainer.open .toggle { +.toggleContainer .toggle.open { background: url("arrow-down.png") no-repeat 0 3px transparent; } @@ -171,10 +171,6 @@ text-decoration: none; margin-top: 5px; } -.toggleContainer .showElement { - padding-left: 15px; -} - .value #definition { background-color: #2C475C; /* blue */ background-image:url('defbg-blue.gif'); @@ -341,11 +337,7 @@ div.members > ol > li:last-child { font-style: italic; } -.signature .symbol .implicit.deprecated { - text-decoration: line-through; -} - -.signature .symbol .name.deprecated { +.signature .symbol .deprecated { text-decoration: line-through; } diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js index 33fbd83bee..c418c3280b 100644 --- a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js @@ -8,7 +8,8 @@ $(document).ready(function(){ name == 'scala.Predef.any2stringfmt' || name == 'scala.Predef.any2stringadd' || name == 'scala.Predef.any2ArrowAssoc' || - name == 'scala.Predef.any2Ensuring' + name == 'scala.Predef.any2Ensuring' || + name == 'scala.collection.TraversableOnce.alternateImplicit' }; $("#linearization li:gt(0)").filter(function(){ @@ -184,21 +185,18 @@ $(document).ready(function(){ }); /* Linear super types and known subclasses */ - function toggleShowContentFct(outerElement){ - var content = $(".hiddenContent", outerElement); - var vis = $(":visible", content); - if (vis.length > 0) { + function toggleShowContentFct(e){ + e.toggleClass("open"); + var content = $(".hiddenContent", e.parent().get(0)); + if (content.is(':visible')) { content.slideUp(100); - $(".showElement", outerElement).show(); - $(".hideElement", outerElement).hide(); } else { content.slideDown(100); - $(".showElement", outerElement).hide(); - $(".hideElement", outerElement).show(); } }; - $(".toggleContainer").click(function() { + + $(".toggle:not(.diagram-link)").click(function() { toggleShowContentFct($(this)); }); diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/trait_diagram.png b/src/compiler/scala/tools/nsc/doc/html/resource/lib/trait_diagram.png new file mode 100644 index 0000000000..88983254ce Binary files /dev/null and b/src/compiler/scala/tools/nsc/doc/html/resource/lib/trait_diagram.png differ -- cgit v1.2.3 From f8057d22235c77d69d72d6ea4d4ebdc2eeb95cdf Mon Sep 17 00:00:00 2001 From: Vlad Ureche Date: Wed, 13 Jun 2012 17:19:00 +0200 Subject: Documented SyncVar Since we used it in the DocRunner and noticed it could have better documentation. Review by @heathermiller. --- src/library/scala/concurrent/SyncVar.scala | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/library/scala/concurrent/SyncVar.scala b/src/library/scala/concurrent/SyncVar.scala index 5a6d95c2ed..292014706d 100644 --- a/src/library/scala/concurrent/SyncVar.scala +++ b/src/library/scala/concurrent/SyncVar.scala @@ -53,6 +53,8 @@ class SyncVar[A] { value } + /** Waits for this SyncVar to become defined and returns + * the result */ def take(): A = synchronized { try get finally unsetVal() @@ -64,7 +66,8 @@ class SyncVar[A] { * the SyncVar. * * @param timeout the amount of milliseconds to wait, 0 means forever - * @return `None` if variable is undefined after `timeout`, `Some(value)` otherwise + * @return the value or a throws an exception if the timeout occurs + * @throws NoSuchElementException on timeout */ def take(timeout: Long): A = synchronized { try get(timeout).get @@ -72,25 +75,28 @@ class SyncVar[A] { } // TODO: this method should be private - // [Heather] the reason why: it doesn't take into consideration + // [Heather] the reason why: it doesn't take into consideration // whether or not the SyncVar is already defined. So, set has been // deprecated in order to eventually be able to make "setting" private @deprecated("Use `put` instead, as `set` is potentionally error-prone", "2.10.0") def set(x: A): Unit = setVal(x) + /** Places a value in the SyncVar. If the SyncVar already has a stored value, + * it waits until another thread takes it */ def put(x: A): Unit = synchronized { while (isDefined) wait() setVal(x) } + /** Checks whether a value is stored in the synchronized variable */ def isSet: Boolean = synchronized { isDefined } // TODO: this method should be private - // [Heather] the reason why: it doesn't take into consideration + // [Heather] the reason why: it doesn't take into consideration // whether or not the SyncVar is already defined. So, unset has been - // deprecated in order to eventually be able to make "unsetting" private + // deprecated in order to eventually be able to make "unsetting" private @deprecated("Use `take` instead, as `unset` is potentionally error-prone", "2.10.0") def unset(): Unit = synchronized { isDefined = false @@ -98,7 +104,7 @@ class SyncVar[A] { notifyAll() } - // `setVal` exists so as to retroactively deprecate `set` without + // `setVal` exists so as to retroactively deprecate `set` without // deprecation warnings where we use `set` internally. The // implementation of `set` was moved to `setVal` to achieve this private def setVal(x: A): Unit = synchronized { @@ -107,13 +113,13 @@ class SyncVar[A] { notifyAll() } - // `unsetVal` exists so as to retroactively deprecate `unset` without + // `unsetVal` exists so as to retroactively deprecate `unset` without // deprecation warnings where we use `unset` internally. The // implementation of `unset` was moved to `unsetVal` to achieve this private def unsetVal(): Unit = synchronized { isDefined = false value = None - notifyAll() + notifyAll() } } -- cgit v1.2.3 From f8cb1aee92fa19e38a1481a4e614cd866ef238c0 Mon Sep 17 00:00:00 2001 From: Vlad Ureche Date: Thu, 28 Jun 2012 00:03:56 +0200 Subject: Diagram tweaks #1 - relaxed the restrictions on nodes - nodes can be classes, traits and objects, both stand-alone and companion objects -- all are added to the diagram, but usually companion objects are filtered out as they don't have any superclasses - changed the rules for default diagram creation: - classes and traits (and AnyRef) get inheritance diagrams - packages and objects get content diagrams (can be overridden by @contentDiagram [hideDiagram] and @inheritanceDiagram [hideDiagram]) - tweaked the model to register subclasses of Any - hardcoded the scala package diagram to show all relations - enabled @contentDiagram showInheritedNodes by default and changed the setting to hideInheritedNodes (and added a test for this) - better node selection (can select nodes that don't have a corresponding trait) - fixed the docsite link in member selection, which was broken since the first commit :)) --- .../scala/tools/nsc/doc/html/page/Template.scala | 2 +- .../scala/tools/nsc/doc/model/ModelFactory.scala | 17 +-- .../doc/model/ModelFactoryImplicitSupport.scala | 14 +-- .../nsc/doc/model/comment/CommentFactory.scala | 6 +- .../tools/nsc/doc/model/diagram/Diagram.scala | 2 +- .../doc/model/diagram/DiagramDirectiveParser.scala | 56 +++++---- .../nsc/doc/model/diagram/DiagramFactory.scala | 131 ++++++++++++++------- src/library/scala/package.scala | 1 + test/scaladoc/run/diagrams-filtering.scala | 8 +- test/scaladoc/run/diagrams-inherited-nodes.check | 1 + test/scaladoc/run/diagrams-inherited-nodes.scala | 69 +++++++++++ 11 files changed, 223 insertions(+), 84 deletions(-) create mode 100644 test/scaladoc/run/diagrams-inherited-nodes.check create mode 100644 test/scaladoc/run/diagrams-inherited-nodes.scala diff --git a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala index 24cff1b475..0d0410c7e2 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala @@ -144,7 +144,7 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp
  • Hide All
  • Show all
  • - Learn more about member selection + Learn more about member selection
    } { diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala index 8eb6e358b9..57625a5e15 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala @@ -45,8 +45,6 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { memberSym.isOmittablePrefix || (closestPackage(memberSym) == closestPackage(templateSym)) } - private lazy val noSubclassCache = Set[Symbol](AnyClass, AnyRefClass, ObjectClass) - def makeModel: Option[Universe] = { val universe = new Universe { thisUniverse => thisFactory.universe = thisUniverse @@ -270,7 +268,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def parentTypes = if (sym.isPackage || sym == AnyClass) List() else { val tps = sym.tpe.parents map { _.asSeenFrom(sym.thisType, sym) } - makeParentTypes(RefinedType(tps, EmptyScope), inTpl) + makeParentTypes(RefinedType(tps, EmptyScope), Some(this), inTpl) } protected def linearizationFromSymbol(symbol: Symbol): List[(TemplateEntity, TypeEntity)] = { @@ -290,7 +288,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { /* Subclass cache */ private lazy val subClassesCache = ( - if (noSubclassCache(sym)) null + if (sym == AnyRefClass) null else mutable.ListBuffer[DocTemplateEntity]() ) def registerSubClass(sc: DocTemplateEntity): Unit = { @@ -796,10 +794,15 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } /** Get the types of the parents of the current class, ignoring the refinements */ - def makeParentTypes(aType: Type, inTpl: => TemplateImpl): List[(TemplateEntity, TypeEntity)] = aType match { + def makeParentTypes(aType: Type, tpl: Option[DocTemplateImpl], inTpl: TemplateImpl): List[(TemplateEntity, TypeEntity)] = aType match { case RefinedType(parents, defs) => - val ignoreParents = Set[Symbol](AnyClass, ObjectClass) - val filtParents = parents filterNot (x => ignoreParents(x.typeSymbol)) + val ignoreParents = Set[Symbol](AnyRefClass, ObjectClass) + val filtParents = + // we don't want to expose too many links to AnyRef, that will just be redundant information + if (tpl.isDefined && (!tpl.get.isObject && parents.length < 2)) + parents + else + parents.filterNot((p: Type) => ignoreParents(p.typeSymbol)) filtParents.map(parent => { val templateEntity = makeTemplate(parent.typeSymbol) val typeEntity = makeType(parent, inTpl) diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala index e7f4a9c79b..8cbf2ac1b6 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala @@ -91,7 +91,7 @@ trait ModelFactoryImplicitSupport { * default Scala imports (Predef._ for example) and the companion object of the current class, if one exists. In the * future we might want to extend this to more complex scopes. */ - def makeImplicitConversions(sym: Symbol, inTpl: => DocTemplateImpl): List[ImplicitConversionImpl] = + def makeImplicitConversions(sym: Symbol, inTpl: DocTemplateImpl): List[ImplicitConversionImpl] = // Nothing and Null are somewhat special -- they can be transformed by any implicit conversion available in scope. // But we don't want that, so we'll simply refuse to find implicit conversions on for Nothing and Null if (!(sym.isClass || sym.isTrait || sym == AnyRefClass) || sym == NothingClass || sym == NullClass) Nil @@ -148,7 +148,7 @@ trait ModelFactoryImplicitSupport { * - we also need to transform implicit parameters in the view's signature into constraints, such that Numeric[T4] * appears as a constraint */ - def makeImplicitConversion(sym: Symbol, result: SearchResult, constrs: List[TypeConstraint], context: Context, inTpl: => DocTemplateImpl): List[ImplicitConversionImpl] = + def makeImplicitConversion(sym: Symbol, result: SearchResult, constrs: List[TypeConstraint], context: Context, inTpl: DocTemplateImpl): List[ImplicitConversionImpl] = if (result.tree == EmptyTree) Nil else { // `result` will contain the type of the view (= implicit conversion method) @@ -206,7 +206,7 @@ trait ModelFactoryImplicitSupport { } } - def makeImplicitConstraints(types: List[Type], sym: Symbol, context: Context, inTpl: => DocTemplateImpl): List[Constraint] = + def makeImplicitConstraints(types: List[Type], sym: Symbol, context: Context, inTpl: DocTemplateImpl): List[Constraint] = types.flatMap((tpe:Type) => { // TODO: Before creating constraints, map typeVarToOriginOrWildcard on the implicitTypes val implType = typeVarToOriginOrWildcard(tpe) @@ -282,7 +282,7 @@ trait ModelFactoryImplicitSupport { } }) - def makeSubstitutionConstraints(subst: TreeTypeSubstituter, inTpl: => DocTemplateImpl): List[Constraint] = + def makeSubstitutionConstraints(subst: TreeTypeSubstituter, inTpl: DocTemplateImpl): List[Constraint] = (subst.from zip subst.to) map { case (from, to) => new EqualTypeParamConstraint { @@ -292,7 +292,7 @@ trait ModelFactoryImplicitSupport { } } - def makeBoundedConstraints(tparams: List[Symbol], constrs: List[TypeConstraint], inTpl: => DocTemplateImpl): List[Constraint] = + def makeBoundedConstraints(tparams: List[Symbol], constrs: List[TypeConstraint], inTpl: DocTemplateImpl): List[Constraint] = (tparams zip constrs) flatMap { case (tparam, constr) => { uniteConstraints(constr) match { @@ -341,7 +341,7 @@ trait ModelFactoryImplicitSupport { val convSym: Symbol, val toType: Type, val constrs: List[Constraint], - inTpl: => DocTemplateImpl) + inTpl: DocTemplateImpl) extends ImplicitConversion { def source: DocTemplateEntity = inTpl @@ -365,7 +365,7 @@ trait ModelFactoryImplicitSupport { case _ => error("Scaladoc implicits: Could not create template for: " + toType + " of type " + toType.getClass); None } - def targetTypeComponents: List[(TemplateEntity, TypeEntity)] = makeParentTypes(toType, inTpl) + def targetTypeComponents: List[(TemplateEntity, TypeEntity)] = makeParentTypes(toType, None, inTpl) def convertorMethod: Either[MemberEntity, String] = { var convertor: MemberEntity = null diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala index a46be37d60..2099315cc6 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala @@ -30,12 +30,12 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => protected val commentCache = mutable.HashMap.empty[(global.Symbol, TemplateImpl), Comment] - def addCommentBody(sym: global.Symbol, inTpl: => TemplateImpl, docStr: String, docPos: global.Position): global.Symbol = { + def addCommentBody(sym: global.Symbol, inTpl: TemplateImpl, docStr: String, docPos: global.Position): global.Symbol = { commentCache += (sym, inTpl) -> parse(docStr, docStr, docPos) sym } - def comment(sym: global.Symbol, inTpl: => DocTemplateImpl): Option[Comment] = { + def comment(sym: global.Symbol, inTpl: DocTemplateImpl): Option[Comment] = { val key = (sym, inTpl) if (commentCache isDefinedAt key) Some(commentCache(key)) @@ -50,7 +50,7 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => * cases we have to give some `inTpl` comments (parent class for example) * to the comment of the symbol. * This function manages some of those cases : Param accessor and Primary constructor */ - def defineComment(sym: global.Symbol, inTpl: => DocTemplateImpl):Option[Comment] = { + def defineComment(sym: global.Symbol, inTpl: DocTemplateImpl):Option[Comment] = { //param accessor case // We just need the @param argument, we put it into the body diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala index 28a8c7d37d..d80999e149 100644 --- a/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala @@ -109,7 +109,7 @@ case class NormalNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node case class ImplicitNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isImplicitNode = true } /** An outside node is shown in packages when a class from a different package makes it to the package diagram due to - * its relation to a class in the package (and @contentDiagram showInheritedNodes annotation) */ + * its relation to a class in the template (see @contentDiagram hideInheritedNodes annotation) */ case class OutsideNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isOutsideNode = true } diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala index beaa045df4..49cfaffc2e 100644 --- a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala @@ -19,6 +19,8 @@ import html.page.diagram.DiagramStats trait DiagramDirectiveParser { this: ModelFactory with DiagramFactory with CommentFactory with TreeFactory => + import this.global.definitions.AnyRefClass + ///// DIAGRAM FILTERS ////////////////////////////////////////////////////////////////////////////////////////////// /** @@ -48,16 +50,22 @@ trait DiagramDirectiveParser { /** Hide subclasses (for type hierarchy diagrams) */ def hideSubclasses: Boolean /** Show related classes from other objects/traits/packages (for content diagrams) */ - def showInheritedNodes: Boolean + def hideInheritedNodes: Boolean /** Hide a node from the diagram */ - def hideNode(clazz: TemplateEntity): Boolean + def hideNode(clazz: Node): Boolean /** Hide an edge from the diagram */ - def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean + def hideEdge(clazz1: Node, clazz2: Node): Boolean } /** Main entry point into this trait: generate the filter for inheritance diagrams */ def makeInheritanceDiagramFilter(template: DocTemplateImpl): DiagramFilter = { - val defaultFilter = if (template.isClass || template.isTrait) FullDiagram else NoDiagramAtAll + + val defaultFilter = + if (template.isClass || template.isTrait || template.sym == AnyRefClass) + FullDiagram + else + NoDiagramAtAll + if (template.comment.isDefined) makeDiagramFilter(template, template.comment.get.inheritDiagram, defaultFilter, true) else @@ -83,9 +91,9 @@ trait DiagramDirectiveParser { val hideOutgoingImplicits: Boolean = false val hideSuperclasses: Boolean = false val hideSubclasses: Boolean = false - val showInheritedNodes: Boolean = false - def hideNode(clazz: TemplateEntity): Boolean = false - def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean = false + val hideInheritedNodes: Boolean = false + def hideNode(clazz: Node): Boolean = false + def hideEdge(clazz1: Node, clazz2: Node): Boolean = false } /** Hide the diagram completely, no need for special filtering */ @@ -95,9 +103,9 @@ trait DiagramDirectiveParser { val hideOutgoingImplicits: Boolean = true val hideSuperclasses: Boolean = true val hideSubclasses: Boolean = true - val showInheritedNodes: Boolean = false - def hideNode(clazz: TemplateEntity): Boolean = true - def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean = true + val hideInheritedNodes: Boolean = true + def hideNode(clazz: Node): Boolean = true + def hideEdge(clazz1: Node, clazz2: Node): Boolean = true } /** The AnnotationDiagramFilter trait directs the diagram engine according to an annotation @@ -107,12 +115,18 @@ trait DiagramDirectiveParser { hideOutgoingImplicits: Boolean, hideSuperclasses: Boolean, hideSubclasses: Boolean, - showInheritedNodes: Boolean, + hideInheritedNodes: Boolean, hideNodesFilter: List[Pattern], hideEdgesFilter: List[(Pattern, Pattern)]) extends DiagramFilter { - def hideNode(clazz: TemplateEntity): Boolean = { - val qualifiedName = clazz.qualifiedName + private[this] def getName(n: Node): String = + if (n.tpl.isDefined) + n.tpl.get.qualifiedName + else + n.name + + def hideNode(clazz: Node): Boolean = { + val qualifiedName = getName(clazz) for (hideFilter <- hideNodesFilter) if (hideFilter.matcher(qualifiedName).matches) { // println(hideFilter + ".matcher(" + qualifiedName + ").matches = " + hideFilter.matcher(qualifiedName).matches) @@ -121,9 +135,9 @@ trait DiagramDirectiveParser { false } - def hideEdge(clazz1: TemplateEntity, clazz2: TemplateEntity): Boolean = { - val clazz1Name = clazz1.qualifiedName - val clazz2Name = clazz2.qualifiedName + def hideEdge(clazz1: Node, clazz2: Node): Boolean = { + val clazz1Name = getName(clazz1) + val clazz2Name = getName(clazz2) for ((clazz1Filter, clazz2Filter) <- hideEdgesFilter) { if (clazz1Filter.matcher(clazz1Name).matches && clazz2Filter.matcher(clazz2Name).matches) { @@ -162,7 +176,7 @@ trait DiagramDirectiveParser { var hideOutgoingImplicits0: Boolean = false var hideSuperclasses0: Boolean = false var hideSubclasses0: Boolean = false - var showInheritedNodes0: Boolean = false + var hideInheritedNodes0: Boolean = false var hideNodesFilter0: List[Pattern] = Nil var hideEdgesFilter0: List[(Pattern, Pattern)] = Nil @@ -190,8 +204,8 @@ trait DiagramDirectiveParser { hideSuperclasses0 = true case "hideSubclasses" if isInheritanceDiagram => hideSubclasses0 = true - case "showInheritedNodes" if !isInheritanceDiagram => - showInheritedNodes0 = true + case "hideInheritedNodes" if !isInheritanceDiagram => + hideInheritedNodes0 = true case HideNodesRegex(last) => val matcher = NodeSpecPattern.matcher(entry) while (matcher.find()) { @@ -225,7 +239,7 @@ trait DiagramDirectiveParser { (hideOutgoingImplicits0 == false) && (hideSuperclasses0 == false) && (hideSubclasses0 == false) && - (showInheritedNodes0 == false) && + (hideInheritedNodes0 == false) && (hideDiagram0 == false)) FullDiagram else @@ -235,7 +249,7 @@ trait DiagramDirectiveParser { hideOutgoingImplicits = hideOutgoingImplicits0, hideSuperclasses = hideSuperclasses0, hideSubclasses = hideSubclasses0, - showInheritedNodes = showInheritedNodes0, + hideInheritedNodes = hideInheritedNodes0, hideNodesFilter = hideNodesFilter0, hideEdgesFilter = hideEdgesFilter0) diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala index 4ae5e7a5cb..3f054b969b 100644 --- a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala @@ -9,6 +9,8 @@ import collection.mutable // statistics import html.page.diagram.DiagramStats +import scala.collection.immutable.SortedMap + /** * This trait takes care of generating the diagram for classes and packages * @@ -18,6 +20,20 @@ import html.page.diagram.DiagramStats trait DiagramFactory extends DiagramDirectiveParser { this: ModelFactory with DiagramFactory with CommentFactory with TreeFactory => + import this.global.definitions._ + import this.global._ + + // the following can used for hardcoding different relations into the diagram, for bootstrapping purposes + lazy val AnyNode = normalNode(AnyClass) + lazy val AnyRefNode = normalNode(AnyRefClass) + lazy val AnyValNode = normalNode(AnyValClass) + lazy val NullNode = normalNode(NullClass) + lazy val NothingNode = normalNode(NothingClass) + def normalNode(sym: Symbol) = + NormalNode(makeTemplate(sym).ownType, Some(makeTemplate(sym))) + def aggregationNode(text: String) = + NormalNode(new TypeEntity { val name = text; val refEntity = SortedMap[Int, (TemplateEntity, Int)]() }, None) + /** Create the inheritance diagram for this template */ def makeInheritanceDiagram(tpl: DocTemplateImpl): Option[Diagram] = { @@ -35,28 +51,36 @@ trait DiagramFactory extends DiagramDirectiveParser { val thisNode = ThisNode(tpl.ownType, Some(tpl)) // superclasses - var superclasses = List[Node]() - tpl.parentTypes.collect { case p: (TemplateEntity, TypeEntity) if !classExcluded(p._1) => p } foreach { - t: (TemplateEntity, TypeEntity) => - val n = NormalNode(t._2, Some(t._1)) - superclasses ::= n - } - val filteredSuperclasses = if (diagramFilter.hideSuperclasses) Nil else superclasses + var superclasses: List[Node] = + tpl.parentTypes.collect { + case p: (TemplateEntity, TypeEntity) if !classExcluded(p._1) => NormalNode(p._2, Some(p._1)) + }.reverse // incoming implcit conversions lazy val incomingImplicitNodes = tpl.incomingImplicitlyConvertedClasses.map(tpl => ImplicitNode(tpl.ownType, Some(tpl))) - val filteredIncomingImplicits = if (diagramFilter.hideIncomingImplicits) Nil else incomingImplicitNodes // subclasses - val subclasses = tpl.directSubClasses.flatMap { - case d: TemplateEntity if !classExcluded(d) => List(NormalNode(d.ownType, Some(d))) - case _ => Nil - } - val filteredSubclasses = if (diagramFilter.hideSubclasses) Nil else subclasses + var subclasses: List[Node] = + tpl.directSubClasses.flatMap { + case d: TemplateEntity if !classExcluded(d) => List(NormalNode(d.ownType, Some(d))) + case _ => Nil + }.sortBy(_.tpl.get.name)(implicitly[Ordering[String]].reverse) // outgoing implicit coversions - lazy val implicitNodes = tpl.outgoingImplicitlyConvertedClasses.map(pair => ImplicitNode(pair._2, Some(pair._1))) - val filteredImplicitOutgoingNodes = if (diagramFilter.hideOutgoingImplicits) Nil else implicitNodes + lazy val outgoingImplicitNodes = tpl.outgoingImplicitlyConvertedClasses.map(pair => ImplicitNode(pair._2, Some(pair._1))) + + // TODO: Everyone should be able to use the @{inherit,content}Diagram annotation to change the diagrams. + // Currently, it's possible to leave nodes and edges out, but there's no way to create new nodes and edges + // The implementation would need to add the annotations and the logic to select nodes (or create new ones) + // and add edges to the diagram -- I bet it wouldn't take too long for someone to do it (one or two days + // at most) and it would be a great add to the diagrams. + if (tpl.sym == AnyRefClass) + subclasses = List(aggregationNode("All user-defined classes and traits")) + + val filteredSuperclasses = if (diagramFilter.hideSuperclasses) Nil else superclasses + val filteredIncomingImplicits = if (diagramFilter.hideIncomingImplicits) Nil else incomingImplicitNodes + val filteredSubclasses = if (diagramFilter.hideSubclasses) Nil else subclasses + val filteredImplicitOutgoingNodes = if (diagramFilter.hideOutgoingImplicits) Nil else outgoingImplicitNodes // final diagram filter filterDiagram(ClassDiagram(thisNode, filteredSuperclasses.reverse, filteredSubclasses.reverse, filteredIncomingImplicits, filteredImplicitOutgoingNodes), diagramFilter) @@ -82,39 +106,68 @@ trait DiagramFactory extends DiagramDirectiveParser { if (diagramFilter == NoDiagramAtAll) None else { - var mapNodes = Map[DocTemplateEntity, Node]() - var nodesShown = Set[DocTemplateEntity]() - var edgesAll = List[(DocTemplateEntity, List[DocTemplateEntity])]() + var mapNodes = Map[TemplateEntity, Node]() + var nodesShown = Set[TemplateEntity]() + var edgesAll = List[(TemplateEntity, List[TemplateEntity])]() // classes is the entire set of classes and traits in the package, they are the superset of nodes in the diagram // we collect classes, traits and objects without a companion, which are usually used as values(e.g. scala.None) - val dnodes = pack.members collect { - case d: DocTemplateEntity if d.isClass || d.isTrait || (d.isObject && !d.companion.isDefined) && - ((d.inTemplate == pack) || diagramFilter.showInheritedNodes) => d + val nodesAll = pack.members collect { + case d: TemplateEntity if ((!diagramFilter.hideInheritedNodes) || (d.inTemplate == pack)) => d } // for each node, add its subclasses - for (node <- dnodes if !classExcluded(node)) { - val superClasses = node.parentTypes.collect { - case (tpl: DocTemplateEntity, tpe) if tpl.inTemplate == pack && !classExcluded(tpl) => tpl - case (tpl: DocTemplateEntity, tpe) if tpl.inTemplate != pack && !classExcluded(tpl) && diagramFilter.showInheritedNodes && (pack.members contains tpl) => tpl - } - - if (!superClasses.isEmpty) { - nodesShown += node - nodesShown ++= superClasses + for (node <- nodesAll if !classExcluded(node)) { + node match { + case dnode: DocTemplateImpl => + var superClasses = dnode.parentTypes.map(_._1) + + superClasses = superClasses.filter(nodesAll.contains(_)) + + // TODO: Everyone should be able to use the @{inherit,content}Diagram annotation to change the diagrams. + if (pack.sym == ScalaPackage) + if (dnode.sym == NullClass) + superClasses = List(makeTemplate(AnyRefClass)) + else if (dnode.sym == NothingClass) + superClasses = (List(NullClass) ::: ScalaValueClasses).map(makeTemplate(_)) + + if (!superClasses.isEmpty) { + nodesShown += dnode + nodesShown ++= superClasses + } + edgesAll ::= dnode -> superClasses + case _ => } - edgesAll ::= node -> superClasses mapNodes += node -> (if (node.inTemplate == pack) NormalNode(node.ownType, Some(node)) else OutsideNode(node.ownType, Some(node))) } if (nodesShown.isEmpty) None else { - val nodes = dnodes.filter(nodesShown.contains(_)).map(mapNodes(_)) + val nodes = nodesAll.filter(nodesShown.contains(_)).map(mapNodes(_)) val edges = edgesAll.map(pair => (mapNodes(pair._1), pair._2.map(mapNodes(_)))).filterNot(pair => pair._2.isEmpty) - filterDiagram(PackageDiagram(nodes, edges), diagramFilter) + val diagram = + // TODO: Everyone should be able to use the @{inherit,content}Diagram annotation to change the diagrams. + if (pack.sym == ScalaPackage) { + // Tried it, but it doesn't look good: + // var anyRefSubtypes: List[Node] = List(mapNodes(makeTemplate(AnyRefClass))) + // var dirty = true + // do { + // val length = anyRefSubtypes.length + // anyRefSubtypes :::= edges.collect { case p: (Node, List[Node]) if p._2.exists(anyRefSubtypes.contains(_)) => p._1 } + // anyRefSubtypes = anyRefSubtypes.distinct + // dirty = (anyRefSubtypes.length != length) + // } while (dirty) + // println(anyRefSubtypes) + val anyRefSubtypes = Nil + val allAnyRefTypes = aggregationNode("All AnyRef subtypes") + val nullTemplate = makeTemplate(NullClass) + PackageDiagram(allAnyRefTypes::nodes, (mapNodes(nullTemplate), allAnyRefTypes::anyRefSubtypes)::edges.filterNot(_._1.tpl == Some(nullTemplate))) + } else + PackageDiagram(nodes, edges) + + filterDiagram(diagram, diagramFilter) } } @@ -137,18 +190,16 @@ trait DiagramFactory extends DiagramDirectiveParser { else { // Final diagram, with the filtered nodes and edges diagram match { - case ClassDiagram(thisNode, _, _, _, _) if diagramFilter.hideNode(thisNode.tpl.get) => + case ClassDiagram(thisNode, _, _, _, _) if diagramFilter.hideNode(thisNode) => None case ClassDiagram(thisNode, superClasses, subClasses, incomingImplicits, outgoingImplicits) => def hideIncoming(node: Node): Boolean = - if (node.tpl.isDefined) diagramFilter.hideNode(node.tpl.get) || diagramFilter.hideEdge(node.tpl.get, thisNode.tpl.get) - else false // hopefully we won't need to fallback here + diagramFilter.hideNode(node) || diagramFilter.hideEdge(node, thisNode) def hideOutgoing(node: Node): Boolean = - if (node.tpl.isDefined) diagramFilter.hideNode(node.tpl.get) || diagramFilter.hideEdge(thisNode.tpl.get, node.tpl.get) - else false // hopefully we won't need to fallback here + diagramFilter.hideNode(node) || diagramFilter.hideEdge(thisNode, node) // println(thisNode) // println(superClasses.map(cl => "super: " + cl + " " + hideOutgoing(cl)).mkString("\n")) @@ -166,8 +217,8 @@ trait DiagramFactory extends DiagramDirectiveParser { // (3) are destinations of hidden classes val edges: List[(Node, List[Node])] = diagram.edges.flatMap({ - case (source@Node(_, Some(tpl1)), dests) if !diagramFilter.hideNode(tpl1) => - val dests2 = dests.collect({ case node@Node(_, Some(tpl2)) if (!(diagramFilter.hideEdge(tpl1, tpl2) || diagramFilter.hideNode(tpl2))) => node }) + case (source, dests) if !diagramFilter.hideNode(source) => + val dests2 = dests.collect({ case dest if (!(diagramFilter.hideEdge(source, dest) || diagramFilter.hideNode(dest))) => dest }) if (dests2 != Nil) List((source, dests2)) else diff --git a/src/library/scala/package.scala b/src/library/scala/package.scala index e3890d7a9d..6460db534d 100644 --- a/src/library/scala/package.scala +++ b/src/library/scala/package.scala @@ -9,6 +9,7 @@ /** * Core Scala types. They are always available without an explicit import. + * @contentDiagram hideNodes "scala.Serializable" */ package object scala { type Throwable = java.lang.Throwable diff --git a/test/scaladoc/run/diagrams-filtering.scala b/test/scaladoc/run/diagrams-filtering.scala index dfde5cac52..8cb32180a1 100644 --- a/test/scaladoc/run/diagrams-filtering.scala +++ b/test/scaladoc/run/diagrams-filtering.scala @@ -54,11 +54,11 @@ object Test extends ScaladocModelTest { assert(packDiag.edges.map(_._2.length).sum == 5) // trait A - // Assert we have just 2 nodes and 1 edge + // Assert we have just 3 nodes and 2 edges val A = base._trait("A") val ADiag = A.inheritanceDiagram.get - assert(ADiag.nodes.length == 2) - assert(ADiag.edges.map(_._2.length).sum == 1) + assert(ADiag.nodes.length == 3) + assert(ADiag.edges.map(_._2.length).sum == 2) // trait C val C = base._trait("C") @@ -82,7 +82,7 @@ object Test extends ScaladocModelTest { val (outgoingSuperclass, outgoingImplicit) = outgoing.head._2.partition(_.isNormalNode) assert(outgoingSuperclass.length == 2) // B and C - assert(outgoingImplicit.length == 1) // T + assert(outgoingImplicit.length == 1, outgoingImplicit) // T val (incomingSubclass, incomingImplicit) = incoming.partition(_._1.isNormalNode) assert(incomingSubclass.length == 2) // F and G diff --git a/test/scaladoc/run/diagrams-inherited-nodes.check b/test/scaladoc/run/diagrams-inherited-nodes.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/diagrams-inherited-nodes.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/diagrams-inherited-nodes.scala b/test/scaladoc/run/diagrams-inherited-nodes.scala new file mode 100644 index 0000000000..8ac382aab8 --- /dev/null +++ b/test/scaladoc/run/diagrams-inherited-nodes.scala @@ -0,0 +1,69 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + override def code = """ + package scala.test.scaladoc.diagrams.inherited.nodes { + + /** @contentDiagram + * @inheritanceDiagram hideDiagram */ + trait T1 { + trait A1 + trait A2 extends A1 + trait A3 extends A2 + } + + /** @contentDiagram + * @inheritanceDiagram hideDiagram */ + trait T2 extends T1 { + trait B1 extends A1 + trait B2 extends A2 with B1 + trait B3 extends A3 with B2 + } + + /** @contentDiagram + * @inheritanceDiagram hideDiagram */ + trait T3 { + self: T1 with T2 => + trait C1 extends B1 + trait C2 extends B2 with C1 + trait C3 extends B3 with C2 + } + + /** @contentDiagram + * @inheritanceDiagram hideDiagram */ + trait T4 extends T3 with T2 with T1 { + trait D1 extends C1 + trait D2 extends C2 with D1 + trait D3 extends C3 with D2 + } + } + """ + + // diagrams must be started. In case there's an error with dot, it should not report anything + def scaladocSettings = "-diagrams" + + def testModel(rootPackage: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + // base package + // Assert we have 7 nodes and 6 edges + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("diagrams")._package("inherited")._package("nodes") + + def checkDiagram(t: String, nodes: Int, edges: Int) = { + // trait T1 + val T = base._trait(t) + val TDiag = T.contentDiagram.get + assert(TDiag.nodes.length == nodes, t + ": " + TDiag.nodes + ".length == " + nodes) + assert(TDiag.edges.map(_._2.length).sum == edges, t + ": " + TDiag.edges.mkString("List(\n", ",\n", "\n)") + ".map(_._2.length).sum == " + edges) + } + + checkDiagram("T1", 3, 2) + checkDiagram("T2", 6, 7) + checkDiagram("T3", 3, 2) + checkDiagram("T4", 12, 17) + } +} \ No newline at end of file -- cgit v1.2.3 From c410b57d55ee10b565356b8595744a804fd2fa2f Mon Sep 17 00:00:00 2001 From: Vlad Ureche Date: Thu, 28 Jun 2012 04:30:30 +0200 Subject: Diagram tweaks #2 - fixed the AnyRef linking (SI-5780) - added tooltips to implicit conversions in diagrams - fixed the intermittent dot error where node images would be left out (dot is not reliable at all -- with all the mechanisms in place to fail gracefully, we still get dot errors crawling their way into diagrams - and that usually means no diagram generated, which is the most appropriate way to fail, I think...) --- .../nsc/doc/html/page/diagram/DiagramStats.scala | 8 ++ .../html/page/diagram/DotDiagramGenerator.scala | 93 ++++++++++----- .../scala/tools/nsc/doc/model/Entity.scala | 4 +- .../scala/tools/nsc/doc/model/ModelFactory.scala | 37 +++--- .../tools/nsc/doc/model/diagram/Diagram.scala | 9 +- .../nsc/doc/model/diagram/DiagramFactory.scala | 16 ++- test/scaladoc/resources/doc-root/Any.scala | 114 ++++++++++++++++++ test/scaladoc/resources/doc-root/AnyRef.scala | 131 +++++++++++++++++++++ test/scaladoc/resources/doc-root/Nothing.scala | 23 ++++ test/scaladoc/resources/doc-root/Null.scala | 17 +++ test/scaladoc/run/SI-5780.check | 1 + test/scaladoc/run/SI-5780.scala | 25 ++++ 12 files changed, 419 insertions(+), 59 deletions(-) create mode 100644 test/scaladoc/resources/doc-root/Any.scala create mode 100644 test/scaladoc/resources/doc-root/AnyRef.scala create mode 100644 test/scaladoc/resources/doc-root/Nothing.scala create mode 100644 test/scaladoc/resources/doc-root/Null.scala create mode 100644 test/scaladoc/run/SI-5780.check create mode 100644 test/scaladoc/run/SI-5780.scala diff --git a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala index 16d859894f..ec00cace75 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala @@ -38,6 +38,8 @@ object DiagramStats { private[this] val dotGenTrack = new TimeTracker("dot diagram generation") private[this] val dotRunTrack = new TimeTracker("dot process runnning") private[this] val svgTrack = new TimeTracker("svg processing") + private[this] var brokenImages = 0 + private[this] var fixedImages = 0 def printStats(settings: Settings) = { if (settings.docDiagramsDebug.value) { @@ -47,6 +49,9 @@ object DiagramStats { dotGenTrack.printStats(settings.printMsg) dotRunTrack.printStats(settings.printMsg) svgTrack.printStats(settings.printMsg) + println(" Broken images: " + brokenImages) + println(" Fixed images: " + fixedImages) + println("") } } @@ -55,4 +60,7 @@ object DiagramStats { def addDotGenerationTime(ms: Long) = dotGenTrack.addTime(ms) def addDotRunningTime(ms: Long) = dotRunTrack.addTime(ms) def addSvgTime(ms: Long) = svgTrack.addTime(ms) + + def addBrokenImage(): Unit = brokenImages += 1 + def addFixedImage(): Unit = fixedImages += 1 } \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala index f5a1d1b088..dc6f941c30 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala @@ -66,10 +66,6 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { var superClasses = List[Node]() var incomingImplicits = List[Node]() var outgoingImplicits = List[Node]() - var subClassesTooltip: Option[String] = None - var superClassesTooltip: Option[String] = None - var incomingImplicitsTooltip: Option[String] = None - var outgoingImplicitsTooltip: Option[String] = None isClassDiagram = false d match { @@ -89,23 +85,23 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { // if there are too many super / sub / implicit nodes, represent // them by on node with a corresponding tooltip superClasses = if (_superClasses.length > settings.docDiagramsMaxNormalClasses.value) { - superClassesTooltip = Some(limitSize(_superClasses.map(_.tpe.name).mkString(", "))) - List(NormalNode(textTypeEntity(_superClasses.length + MultiSuffix), None)) + val superClassesTooltip = Some(limitSize(_superClasses.map(_.tpe.name).mkString(", "))) + List(NormalNode(textTypeEntity(_superClasses.length + MultiSuffix), None, superClassesTooltip)) } else _superClasses subClasses = if (_subClasses.length > settings.docDiagramsMaxNormalClasses.value) { - subClassesTooltip = Some(limitSize(_subClasses.map(_.tpe.name).mkString(", "))) - List(NormalNode(textTypeEntity(_subClasses.length + MultiSuffix), None)) + val subClassesTooltip = Some(limitSize(_subClasses.map(_.tpe.name).mkString(", "))) + List(NormalNode(textTypeEntity(_subClasses.length + MultiSuffix), None, subClassesTooltip)) } else _subClasses incomingImplicits = if (_incomingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { - incomingImplicitsTooltip = Some(limitSize(_incomingImplicits.map(_.tpe.name).mkString(", "))) - List(ImplicitNode(textTypeEntity(_incomingImplicits.length + MultiSuffix), None)) + val incomingImplicitsTooltip = Some(limitSize(_incomingImplicits.map(_.tpe.name).mkString(", "))) + List(ImplicitNode(textTypeEntity(_incomingImplicits.length + MultiSuffix), None, incomingImplicitsTooltip)) } else _incomingImplicits outgoingImplicits = if (_outgoingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { - outgoingImplicitsTooltip = Some(limitSize(_outgoingImplicits.map(_.tpe.name).mkString(", "))) - List(ImplicitNode(textTypeEntity(_outgoingImplicits.length + MultiSuffix), None)) + val outgoingImplicitsTooltip = Some(limitSize(_outgoingImplicits.map(_.tpe.name).mkString(", "))) + List(ImplicitNode(textTypeEntity(_outgoingImplicits.length + MultiSuffix), None, outgoingImplicitsTooltip)) } else _outgoingImplicits thisNode = _thisNode @@ -128,14 +124,14 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { // dot cluster containing thisNode val thisCluster = "subgraph clusterThis {\n" + "style=\"invis\"\n" + - node2Dot(thisNode, None) + + node2Dot(thisNode) + "}" // dot cluster containing incoming implicit nodes, if any val incomingCluster = { if(incomingImplicits.isEmpty) "" else "subgraph clusterIncoming {\n" + "style=\"invis\"\n" + - incomingImplicits.reverse.map(n => node2Dot(n, incomingImplicitsTooltip)).mkString + + incomingImplicits.reverse.map(n => node2Dot(n)).mkString + (if (incomingImplicits.size > 1) incomingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" @@ -147,7 +143,7 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { if(outgoingImplicits.isEmpty) "" else "subgraph clusterOutgoing {\n" + "style=\"invis\"\n" + - outgoingImplicits.reverse.map(n => node2Dot(n, outgoingImplicitsTooltip)).mkString + + outgoingImplicits.reverse.map(n => node2Dot(n)).mkString + (if (outgoingImplicits.size > 1) outgoingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" @@ -189,9 +185,9 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { "edge [" + edgeAttributesStr + "];\n" + implicitsDot + "\n" + // inheritance nodes - nodes.map(n => node2Dot(n, None)).mkString + - subClasses.map(n => node2Dot(n, subClassesTooltip)).mkString + - superClasses.map(n => node2Dot(n, superClassesTooltip)).mkString + + nodes.map(n => node2Dot(n)).mkString + + subClasses.map(n => node2Dot(n)).mkString + + superClasses.map(n => node2Dot(n)).mkString + // inheritance edges edges.map{ case (from, tos) => tos.map(to => { val id = "graph" + counter + "_" + node2Index(to) + "_" + node2Index(from) @@ -213,7 +209,7 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { /** * Generates the dot string of a given node. */ - private def node2Dot(node: Node, tooltip: Option[String]) = { + private def node2Dot(node: Node) = { // escape HTML characters in node names def escape(name: String) = name.replace("&", "&").replace("<", "<").replace(">", ">"); @@ -228,7 +224,7 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { } // tooltip - tooltip match { + node.tooltip match { case Some(text) => attr += "tooltip" -> text // show full name where available (instead of TraversableOps[A] show scala.collection.parallel.TraversableOps[A]) case None if node.tpl.isDefined => attr += "tooltip" -> node.tpl.get.qualifiedName @@ -294,25 +290,23 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { */ private def cssClass(node: Node): String = if (node.isImplicitNode && incomingImplicitNodes.contains(node)) - "implicit-incoming" + "implicit-incoming" + cssBaseClass(node, "", " ") else if (node.isImplicitNode) - "implicit-outgoing" - else if (node.isObjectNode) - "object" + "implicit-outgoing" + cssBaseClass(node, "", " ") else if (node.isThisNode) - "this" + cssBaseClass(node, "") + "this" + cssBaseClass(node, "", " ") else if (node.isOutsideNode) - "outside" + cssBaseClass(node, "") + "outside" + cssBaseClass(node, "", " ") else - cssBaseClass(node, "default") + cssBaseClass(node, "default", "") - private def cssBaseClass(node: Node, default: String) = + private def cssBaseClass(node: Node, default: String, space: String) = if (node.isClassNode) - " class" + space + "class" else if (node.isTraitNode) - " trait" + space + "trait" else if (node.isObjectNode) - " trait" + space + "object" else default @@ -381,10 +375,31 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { // assign id and class attributes to edges and nodes: // the id attribute generated by dot has the format: "{class}|{id}" case g @ Elem(prefix, "g", attribs, scope, children @ _*) if (List("edge", "node").contains((g \ "@class").toString)) => { - val res = new Elem(prefix, "g", attribs, scope, (children map(x => transform(x))): _*) + var res = new Elem(prefix, "g", attribs, scope, (children map(x => transform(x))): _*) val dotId = (g \ "@id").toString if (dotId.count(_ == '|') == 1) { val Array(klass, id) = dotId.toString.split("\\|") + /* Sometimes dot "forgets" to add the image -- that's very annoying, but it seems pretty random, and simple + * tests like excute 20K times and diff the output don't trigger the bug -- so it's up to us to place the image + * back in the node */ + val kind = getKind(klass) + if (kind != "") + if (((g \ "a" \ "image").isEmpty)) { + DiagramStats.addBrokenImage() + val xposition = getPosition(g, "x", -22) + val yposition = getPosition(g, "y", -11.3334) + if (xposition.isDefined && yposition.isDefined) { + val imageNode = + val anchorNode = (g \ "a") match { + case Seq(Elem(prefix, "a", attribs, scope, children @ _*)) => + transform(new Elem(prefix, "a", attribs, scope, (children ++ imageNode): _*)) + case _ => + g \ "a" + } + res = new Elem(prefix, "g", attribs, scope, anchorNode: _*) + DiagramStats.addFixedImage() + } + } res % new UnprefixedAttribute("id", id, Null) % new UnprefixedAttribute("class", (g \ "@class").toString + " " + klass, Null) } @@ -399,6 +414,20 @@ class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { case x => x } + def getKind(klass: String): String = + if (klass.contains("class")) "class" + else if (klass.contains("trait")) "trait" + else if (klass.contains("object")) "object" + else "" + + def getPosition(g: xml.Node, axis: String, offset: Double): Option[Double] = { + val node = g \ "a" \ "text" \ ("@" + axis) + if (node.isEmpty) + None + else + Some(node.toString.toDouble + offset) + } + /* graph / node / edge attributes */ private val graphAttributes: Map[String, String] = Map( diff --git a/src/compiler/scala/tools/nsc/doc/model/Entity.scala b/src/compiler/scala/tools/nsc/doc/model/Entity.scala index e1bbf28dc7..2901daafd6 100644 --- a/src/compiler/scala/tools/nsc/doc/model/Entity.scala +++ b/src/compiler/scala/tools/nsc/doc/model/Entity.scala @@ -278,11 +278,11 @@ trait DocTemplateEntity extends TemplateEntity with MemberEntity { def implicitsShadowing: Map[MemberEntity, ImplicitMemberShadowing] /** Classes that can be implcitly converted to this class */ - def incomingImplicitlyConvertedClasses: List[DocTemplateEntity] + def incomingImplicitlyConvertedClasses: List[(DocTemplateEntity, ImplicitConversion)] /** Classes to which this class can be implicitly converted to NOTE: Some classes might not be included in the scaladoc run so they will be NoDocTemplateEntities */ - def outgoingImplicitlyConvertedClasses: List[(TemplateEntity, TypeEntity)] + def outgoingImplicitlyConvertedClasses: List[(TemplateEntity, TypeEntity, ImplicitConversion)] /** If this template takes place in inheritance and implicit conversion relations, it will be shown in this diagram */ def inheritanceDiagram: Option[Diagram] diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala index 57625a5e15..9fa6619e9f 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala @@ -299,14 +299,14 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def directSubClasses = allSubClasses.filter(_.parentTypes.map(_._1).contains(this)) /* Implcitly convertible class cache */ - private var implicitlyConvertibleClassesCache: mutable.ListBuffer[DocTemplateEntity] = null - def registerImplicitlyConvertibleClass(sc: DocTemplateEntity): Unit = { + private var implicitlyConvertibleClassesCache: mutable.ListBuffer[(DocTemplateEntity, ImplicitConversionImpl)] = null + def registerImplicitlyConvertibleClass(dtpl: DocTemplateEntity, conv: ImplicitConversionImpl): Unit = { if (implicitlyConvertibleClassesCache == null) - implicitlyConvertibleClassesCache = mutable.ListBuffer[DocTemplateEntity]() - implicitlyConvertibleClassesCache += sc + implicitlyConvertibleClassesCache = mutable.ListBuffer[(DocTemplateEntity, ImplicitConversionImpl)]() + implicitlyConvertibleClassesCache += ((dtpl, conv)) } - def incomingImplicitlyConvertedClasses: List[DocTemplateEntity] = + def incomingImplicitlyConvertedClasses: List[(DocTemplateEntity, ImplicitConversionImpl)] = if (implicitlyConvertibleClassesCache == null) List() else @@ -363,18 +363,19 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { var implicitsShadowing = Map[MemberEntity, ImplicitMemberShadowing]() - lazy val outgoingImplicitlyConvertedClasses: List[(TemplateEntity, TypeEntity)] = conversions flatMap (conv => - if (!implicitExcluded(conv.conversionQualifiedName)) - conv.targetTypeComponents map { - case pair@(template, tpe) => - template match { - case d: DocTemplateImpl => d.registerImplicitlyConvertibleClass(this) - case _ => // nothing - } - pair - } - else List() - ) + lazy val outgoingImplicitlyConvertedClasses: List[(TemplateEntity, TypeEntity, ImplicitConversionImpl)] = + conversions flatMap (conv => + if (!implicitExcluded(conv.conversionQualifiedName)) + conv.targetTypeComponents map { + case pair@(template, tpe) => + template match { + case d: DocTemplateImpl => d.registerImplicitlyConvertibleClass(this, conv) + case _ => // nothing + } + (pair._1, pair._2, conv) + } + else List() + ) override def isTemplate = true lazy val definitionName = optimize(inDefinitionTemplates.head.qualifiedName + "." + name) @@ -862,7 +863,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { // nameBuffer append stripPrefixes.foldLeft(pre.prefixString)(_ stripPrefix _) // } val bSym = normalizeTemplate(aSym) - if (bSym.isNonClassType) { + if (bSym.isNonClassType && bSym != AnyRefClass) { nameBuffer append bSym.decodedName } else { val tpl = makeTemplate(bSym) diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala index d80999e149..8527ca4039 100644 --- a/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/Diagram.scala @@ -73,6 +73,7 @@ abstract class Node { def isOtherNode = !(isClassNode || isTraitNode || isObjectNode) def isImplicitNode = false def isOutsideNode = false + def tooltip: Option[String] } // different matchers, allowing you to use the pattern matcher against any node @@ -97,20 +98,20 @@ object OtherNode { def unapply(n: Node): Option[(TypeEntity, Option[TemplateEn /** The node for the current class */ -case class ThisNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isThisNode = true } +case class ThisNode(tpe: TypeEntity, tpl: Option[TemplateEntity], tooltip: Option[String] = None) extends Node { override def isThisNode = true } /** The usual node */ -case class NormalNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isNormalNode = true } +case class NormalNode(tpe: TypeEntity, tpl: Option[TemplateEntity], tooltip: Option[String] = None) extends Node { override def isNormalNode = true } /** A class or trait the thisnode can be converted to by an implicit conversion * TODO: I think it makes more sense to use the tpe links to templates instead of the TemplateEntity for implicit nodes * since some implicit conversions convert the class to complex types that cannot be represented as a single tmeplate */ -case class ImplicitNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isImplicitNode = true } +case class ImplicitNode(tpe: TypeEntity, tpl: Option[TemplateEntity], tooltip: Option[String] = None) extends Node { override def isImplicitNode = true } /** An outside node is shown in packages when a class from a different package makes it to the package diagram due to * its relation to a class in the template (see @contentDiagram hideInheritedNodes annotation) */ -case class OutsideNode(tpe: TypeEntity, tpl: Option[TemplateEntity]) extends Node { override def isOutsideNode = true } +case class OutsideNode(tpe: TypeEntity, tpl: Option[TemplateEntity], tooltip: Option[String] = None) extends Node { override def isOutsideNode = true } // Computing and offering node depth information diff --git a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala index 3f054b969b..1a8ad193aa 100644 --- a/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/diagram/DiagramFactory.scala @@ -43,12 +43,16 @@ trait DiagramFactory extends DiagramDirectiveParser { // the diagram filter val diagramFilter = makeInheritanceDiagramFilter(tpl) + def implicitTooltip(from: DocTemplateEntity, to: TemplateEntity, conv: ImplicitConversion) = + Some(from.qualifiedName + " can be implicitly converted to " + conv.targetType + " by the implicit method " + + conv.conversionShortName + " in " + conv.convertorOwner.kind + " " + conv.convertorOwner.qualifiedName) + val result = if (diagramFilter == NoDiagramAtAll) None else { // the main node - val thisNode = ThisNode(tpl.ownType, Some(tpl)) + val thisNode = ThisNode(tpl.ownType, Some(tpl), Some(tpl.qualifiedName + " (this " + tpl.kind + ")")) // superclasses var superclasses: List[Node] = @@ -57,7 +61,10 @@ trait DiagramFactory extends DiagramDirectiveParser { }.reverse // incoming implcit conversions - lazy val incomingImplicitNodes = tpl.incomingImplicitlyConvertedClasses.map(tpl => ImplicitNode(tpl.ownType, Some(tpl))) + lazy val incomingImplicitNodes = tpl.incomingImplicitlyConvertedClasses.map { + case (incomingTpl, conv) => + ImplicitNode(incomingTpl.ownType, Some(incomingTpl), implicitTooltip(from=incomingTpl, to=tpl, conv=conv)) + } // subclasses var subclasses: List[Node] = @@ -67,7 +74,10 @@ trait DiagramFactory extends DiagramDirectiveParser { }.sortBy(_.tpl.get.name)(implicitly[Ordering[String]].reverse) // outgoing implicit coversions - lazy val outgoingImplicitNodes = tpl.outgoingImplicitlyConvertedClasses.map(pair => ImplicitNode(pair._2, Some(pair._1))) + lazy val outgoingImplicitNodes = tpl.outgoingImplicitlyConvertedClasses.map { + case (outgoingTpl, outgoingType, conv) => + ImplicitNode(outgoingType, Some(outgoingTpl), implicitTooltip(from=tpl, to=tpl, conv=conv)) + } // TODO: Everyone should be able to use the @{inherit,content}Diagram annotation to change the diagrams. // Currently, it's possible to leave nodes and edges out, but there's no way to create new nodes and edges diff --git a/test/scaladoc/resources/doc-root/Any.scala b/test/scaladoc/resources/doc-root/Any.scala new file mode 100644 index 0000000000..031b7d9d8c --- /dev/null +++ b/test/scaladoc/resources/doc-root/Any.scala @@ -0,0 +1,114 @@ +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2002-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala + +/** Class `Any` is the root of the Scala class hierarchy. Every class in a Scala + * execution environment inherits directly or indirectly from this class. + */ +abstract class Any { + /** Compares the receiver object (`this`) with the argument object (`that`) for equivalence. + * + * Any implementation of this method should be an [[http://en.wikipedia.org/wiki/Equivalence_relation equivalence relation]]: + * + * - It is reflexive: for any instance `x` of type `Any`, `x.equals(x)` should return `true`. + * - It is symmetric: for any instances `x` and `y` of type `Any`, `x.equals(y)` should return `true` if and + * only if `y.equals(x)` returns `true`. + * - It is transitive: for any instances `x`, `y`, and `z` of type `AnyRef` if `x.equals(y)` returns `true` and + * `y.equals(z)` returns `true`, then `x.equals(z)` should return `true`. + * + * If you override this method, you should verify that your implementation remains an equivalence relation. + * Additionally, when overriding this method it is usually necessary to override `hashCode` to ensure that + * objects which are "equal" (`o1.equals(o2)` returns `true`) hash to the same [[scala.Int]]. + * (`o1.hashCode.equals(o2.hashCode)`). + * + * @param that the object to compare against this object for equality. + * @return `true` if the receiver object is equivalent to the argument; `false` otherwise. + */ + def equals(that: Any): Boolean + + /** Calculate a hash code value for the object. + * + * The default hashing algorithm is platform dependent. + * + * Note that it is allowed for two objects to have identical hash codes (`o1.hashCode.equals(o2.hashCode)`) yet + * not be equal (`o1.equals(o2)` returns `false`). A degenerate implementation could always return `0`. + * However, it is required that if two objects are equal (`o1.equals(o2)` returns `true`) that they have + * identical hash codes (`o1.hashCode.equals(o2.hashCode)`). Therefore, when overriding this method, be sure + * to verify that the behavior is consistent with the `equals` method. + * + * @return the hash code value for this object. + */ + def hashCode(): Int + + /** Returns a string representation of the object. + * + * The default representation is platform dependent. + * + * @return a string representation of the object. + */ + def toString(): String + + /** Returns the runtime class representation of the object. + * + * @return a class object corresponding to the runtime type of the receiver. + */ + def getClass(): Class[_] + + /** Test two objects for equality. + * The expression `x == that` is equivalent to `if (x eq null) that eq null else x.equals(that)`. + * + * @param that the object to compare against this object for equality. + * @return `true` if the receiver object is equivalent to the argument; `false` otherwise. + */ + final def ==(that: Any): Boolean = this equals that + + /** Test two objects for inequality. + * + * @param that the object to compare against this object for equality. + * @return `true` if !(this == that), false otherwise. + */ + final def != (that: Any): Boolean = !(this == that) + + /** Equivalent to `x.hashCode` except for boxed numeric types and `null`. + * For numerics, it returns a hash value which is consistent + * with value equality: if two value type instances compare + * as true, then ## will produce the same hash value for each + * of them. + * For `null` returns a hashcode where `null.hashCode` throws a + * `NullPointerException`. + * + * @return a hash value consistent with == + */ + final def ##(): Int = sys.error("##") + + /** Test whether the dynamic type of the receiver object is `T0`. + * + * Note that the result of the test is modulo Scala's erasure semantics. + * Therefore the expression `1.isInstanceOf[String]` will return `false`, while the + * expression `List(1).isInstanceOf[List[String]]` will return `true`. + * In the latter example, because the type argument is erased as part of compilation it is + * not possible to check whether the contents of the list are of the specified type. + * + * @return `true` if the receiver object is an instance of erasure of type `T0`; `false` otherwise. + */ + def isInstanceOf[T0]: Boolean = sys.error("isInstanceOf") + + /** Cast the receiver object to be of type `T0`. + * + * Note that the success of a cast at runtime is modulo Scala's erasure semantics. + * Therefore the expression `1.asInstanceOf[String]` will throw a `ClassCastException` at + * runtime, while the expression `List(1).asInstanceOf[List[String]]` will not. + * In the latter example, because the type argument is erased as part of compilation it is + * not possible to check whether the contents of the list are of the requested type. + * + * @throws ClassCastException if the receiver object is not an instance of the erasure of type `T0`. + * @return the receiver object. + */ + def asInstanceOf[T0]: T0 = sys.error("asInstanceOf") +} diff --git a/test/scaladoc/resources/doc-root/AnyRef.scala b/test/scaladoc/resources/doc-root/AnyRef.scala new file mode 100644 index 0000000000..1eefb0c806 --- /dev/null +++ b/test/scaladoc/resources/doc-root/AnyRef.scala @@ -0,0 +1,131 @@ +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2002-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala + +/** Class `AnyRef` is the root class of all ''reference types''. + * All types except the value types descend from this class. + */ +trait AnyRef extends Any { + + /** The equality method for reference types. Default implementation delegates to `eq`. + * + * See also `equals` in [[scala.Any]]. + * + * @param that the object to compare against this object for equality. + * @return `true` if the receiver object is equivalent to the argument; `false` otherwise. + */ + def equals(that: Any): Boolean = this eq that + + /** The hashCode method for reference types. See hashCode in [[scala.Any]]. + * + * @return the hash code value for this object. + */ + def hashCode: Int = sys.error("hashCode") + + /** Creates a String representation of this object. The default + * representation is platform dependent. On the java platform it + * is the concatenation of the class name, "@", and the object's + * hashcode in hexadecimal. + * + * @return a String representation of the object. + */ + def toString: String = sys.error("toString") + + /** Executes the code in `body` with an exclusive lock on `this`. + * + * @param body the code to execute + * @return the result of `body` + */ + def synchronized[T](body: => T): T + + /** Tests whether the argument (`arg0`) is a reference to the receiver object (`this`). + * + * The `eq` method implements an [[http://en.wikipedia.org/wiki/Equivalence_relation equivalence relation]] on + * non-null instances of `AnyRef`, and has three additional properties: + * + * - It is consistent: for any non-null instances `x` and `y` of type `AnyRef`, multiple invocations of + * `x.eq(y)` consistently returns `true` or consistently returns `false`. + * - For any non-null instance `x` of type `AnyRef`, `x.eq(null)` and `null.eq(x)` returns `false`. + * - `null.eq(null)` returns `true`. + * + * When overriding the `equals` or `hashCode` methods, it is important to ensure that their behavior is + * consistent with reference equality. Therefore, if two objects are references to each other (`o1 eq o2`), they + * should be equal to each other (`o1 == o2`) and they should hash to the same value (`o1.hashCode == o2.hashCode`). + * + * @param that the object to compare against this object for reference equality. + * @return `true` if the argument is a reference to the receiver object; `false` otherwise. + */ + final def eq(that: AnyRef): Boolean = sys.error("eq") + + /** Equivalent to `!(this eq that)`. + * + * @param that the object to compare against this object for reference equality. + * @return `true` if the argument is not a reference to the receiver object; `false` otherwise. + */ + final def ne(that: AnyRef): Boolean = !(this eq that) + + /** The expression `x == that` is equivalent to `if (x eq null) that eq null else x.equals(that)`. + * + * @param arg0 the object to compare against this object for equality. + * @return `true` if the receiver object is equivalent to the argument; `false` otherwise. + */ + final def ==(that: AnyRef): Boolean = + if (this eq null) that eq null + else this equals that + + /** Create a copy of the receiver object. + * + * The default implementation of the `clone` method is platform dependent. + * + * @note not specified by SLS as a member of AnyRef + * @return a copy of the receiver object. + */ + protected def clone(): AnyRef + + /** Called by the garbage collector on the receiver object when there + * are no more references to the object. + * + * The details of when and if the `finalize` method is invoked, as + * well as the interaction between `finalize` and non-local returns + * and exceptions, are all platform dependent. + * + * @note not specified by SLS as a member of AnyRef + */ + protected def finalize(): Unit + + /** A representation that corresponds to the dynamic class of the receiver object. + * + * The nature of the representation is platform dependent. + * + * @note not specified by SLS as a member of AnyRef + * @return a representation that corresponds to the dynamic class of the receiver object. + */ + def getClass(): Class[_] + + /** Wakes up a single thread that is waiting on the receiver object's monitor. + * + * @note not specified by SLS as a member of AnyRef + */ + def notify(): Unit + + /** Wakes up all threads that are waiting on the receiver object's monitor. + * + * @note not specified by SLS as a member of AnyRef + */ + def notifyAll(): Unit + + /** Causes the current Thread to wait until another Thread invokes + * the notify() or notifyAll() methods. + * + * @note not specified by SLS as a member of AnyRef + */ + def wait (): Unit + def wait (timeout: Long, nanos: Int): Unit + def wait (timeout: Long): Unit +} diff --git a/test/scaladoc/resources/doc-root/Nothing.scala b/test/scaladoc/resources/doc-root/Nothing.scala new file mode 100644 index 0000000000..eed6066039 --- /dev/null +++ b/test/scaladoc/resources/doc-root/Nothing.scala @@ -0,0 +1,23 @@ +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2002-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala + +/** `Nothing` is - together with [[scala.Null]] - at the bottom of Scala's type hierarchy. + * + * `Nothing` is a subtype of every other type (including [[scala.Null]]); there exist + * ''no instances'' of this type. Although type `Nothing` is uninhabited, it is + * nevertheless useful in several ways. For instance, the Scala library defines a value + * [[scala.collection.immutable.Nil]] of type `List[Nothing]`. Because lists are covariant in Scala, + * this makes [[scala.collection.immutable.Nil]] an instance of `List[T]`, for any element of type `T`. + * + * Another usage for Nothing is the return type for methods which never return normally. + * One example is method error in [[scala.sys]], which always throws an exception. + */ +sealed trait Nothing + diff --git a/test/scaladoc/resources/doc-root/Null.scala b/test/scaladoc/resources/doc-root/Null.scala new file mode 100644 index 0000000000..7455e78ae7 --- /dev/null +++ b/test/scaladoc/resources/doc-root/Null.scala @@ -0,0 +1,17 @@ +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2002-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala + +/** `Null` is - together with [[scala.Nothing]] - at the bottom of the Scala type hierarchy. + * + * `Null` is a subtype of all reference types; its only instance is the `null` reference. + * Since `Null` is not a subtype of value types, `null` is not a member of any such type. For instance, + * it is not possible to assign `null` to a variable of type [[scala.Int]]. + */ +sealed trait Null diff --git a/test/scaladoc/run/SI-5780.check b/test/scaladoc/run/SI-5780.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/SI-5780.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/SI-5780.scala b/test/scaladoc/run/SI-5780.scala new file mode 100644 index 0000000000..809567faec --- /dev/null +++ b/test/scaladoc/run/SI-5780.scala @@ -0,0 +1,25 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.nsc.doc.model.diagram._ +import scala.tools.partest.ScaladocModelTest + +object Test extends ScaladocModelTest { + + override def code = """ + package scala.test.scaladoc.SI5780 + + object `package` { def foo: AnyRef = "hello"; class T /* so the package is not dropped */ } + """ + + // diagrams must be started. In case there's an error with dot, it should not report anything + def scaladocSettings = "-doc-root-content " + resourcePath + "/doc-root" + + def testModel(rootPackage: Package) = { + // get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s)) + import access._ + + val foo = rootPackage._package("scala")._package("test")._package("scaladoc")._package("SI5780")._method("foo") + // check that AnyRef is properly linked to its template: + assert(foo.resultType.name == "AnyRef", foo.resultType.name + " == AnyRef") + assert(foo.resultType.refEntity.size == 1, foo.resultType.refEntity + ".size == 1") + } +} \ No newline at end of file -- cgit v1.2.3