diff options
18 files changed, 459 insertions, 108 deletions
diff --git a/src/compiler/scala/tools/nsc/doc/DocFactory.scala b/src/compiler/scala/tools/nsc/doc/DocFactory.scala index 964227a6a5..9fccc57e44 100644 --- a/src/compiler/scala/tools/nsc/doc/DocFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/DocFactory.scala @@ -48,8 +48,8 @@ class DocFactory(val reporter: Reporter, val settings: doc.Settings) { processor /** Creates a scaladoc site for all symbols defined in this call's `source`, * as well as those defined in `sources` of previous calls to the same processor. - * @param files The list of paths (relative to the compiler's source path, - * or absolute) of files to document. */ + * @param source The list of paths (relative to the compiler's source path, + * or absolute) of files to document or the source code. */ def makeUniverse(source: Either[List[String], String]): Option[Universe] = { assert(settings.docformat.value == "html") source match { @@ -84,7 +84,8 @@ class DocFactory(val reporter: Reporter, val settings: doc.Settings) { processor with model.ModelFactoryTypeSupport with model.diagram.DiagramFactory with model.comment.CommentFactory - with model.TreeFactory { + with model.TreeFactory + with model.MemberLookup { 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 aae88aa03e..da478121e5 100644 --- a/src/compiler/scala/tools/nsc/doc/Settings.scala +++ b/src/compiler/scala/tools/nsc/doc/Settings.scala @@ -171,6 +171,11 @@ class Settings(error: String => Unit, val printMsg: String => Unit = println(_)) "Prevents generating prefixes in types, possibly creating ambiguous references, but significantly speeding up scaladoc." ) + val docNoLinkWarnings = BooleanSetting ( + "-no-link-warnings", + "Avoid warnings for ambiguous and incorrect links." + ) + // 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. @@ -183,7 +188,7 @@ class Settings(error: String => Unit, val printMsg: String => Unit = println(_)) docDiagramsDotTimeout, docDiagramsDotRestart, docImplicits, docImplicitsDebug, docImplicitsShowAll, docDiagramsMaxNormalClasses, docDiagramsMaxImplicitClasses, - docNoPrefixes + docNoPrefixes, docNoLinkWarnings, docRawOutput ) val isScaladocSpecific: String => Boolean = scaladocSpecific map (_.name) diff --git a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala index af5e90083e..226ed49aca 100644 --- a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala +++ b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala @@ -122,17 +122,30 @@ abstract class HtmlPage extends Page { thisPage => 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) - } + case EntityLink(target, link) => linkToHtml(target, link, true) + } + + def linkToHtml(text: Inline, link: LinkTo, hasLinks: Boolean) = link match { + case LinkToTpl(dtpl) => + if (hasLinks) + <a href={ relativeLinkTo(dtpl) } class="extype" name={ dtpl.qualifiedName }>{ inlineToHtml(text) }</a> + else + <span class="extype" name={ dtpl.qualifiedName }>{ inlineToHtml(text) }</span> + case LinkToMember(mbr, inTpl) => + if (hasLinks) + <a href={ relativeLinkTo(inTpl) + "#" + mbr.signature } class="extmbr" name={ mbr.qualifiedName }>{ inlineToHtml(text) }</a> + else + <span class="extmbr" name={ mbr.qualifiedName }>{ inlineToHtml(text) }</span> + case Tooltip(tooltip) => + <span class="extype" name={ tooltip }>{ inlineToHtml(text) }</span> + // TODO: add case LinkToExternal here + case NoLink => + inlineToHtml(text) } def typeToHtml(tpes: List[model.TypeEntity], hasLinks: Boolean): NodeSeq = tpes match { case Nil => - sys.error("Internal Scaladoc error") + NodeSeq.Empty case List(tpe) => typeToHtml(tpe, hasLinks) case tpe :: rest => @@ -153,21 +166,9 @@ abstract class HtmlPage extends Page { thisPage => } } def toLinksIn(inPos: Int, starts: List[Int]): NodeSeq = { - val (tpl, width) = tpe.refEntity(inPos) - (tpl match { - case LinkToTpl(dtpl:DocTemplateEntity) if hasLinks => - <a href={ relativeLinkTo(dtpl) } class="extype" name={ dtpl.qualifiedName }>{ - string.slice(inPos, inPos + width) - }</a> - case LinkToTpl(tpl) => - <span class="extype" name={ tpl.qualifiedName }>{ string.slice(inPos, inPos + width) }</span> - case LinkToMember(mbr, inTpl) if hasLinks => - <a href={ relativeLinkTo(inTpl) + "#" + mbr.signature } class="extmbr" name={ mbr.qualifiedName }>{ - string.slice(inPos, inPos + width) - }</a> - case LinkToMember(mbr, inTpl) => - <span class="extmbr" name={ mbr.qualifiedName }>{ string.slice(inPos, inPos + width) }</span> - }) ++ toLinksOut(inPos + width, starts.tail) + val (link, width) = tpe.refEntity(inPos) + val text = comment.Text(string.slice(inPos, inPos + width)) + linkToHtml(text, link, hasLinks) ++ toLinksOut(inPos + width, starts.tail) } if (hasLinks) toLinksOut(0, tpe.refEntity.keySet.toList) 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 bba838ddcf..cfd50db99f 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Template.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Template.scala @@ -64,7 +64,7 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp nonAbsValueMembers partition (_.deprecation.isDefined) val (concValueMembers, shadowedImplicitMembers) = - nonDeprValueMembers partition (!Template.isShadowedOrAmbiguousImplicit(_)) + nonDeprValueMembers partition (!_.isShadowedOrAmbiguousImplicit) val typeMembers = tpl.abstractTypes ++ tpl.aliasTypes ++ tpl.templates.filter(x => x.isTrait || x.isClass) sorted @@ -387,7 +387,7 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp { constraintText } </dd> } ++ { - if (Template.isShadowedOrAmbiguousImplicit(mbr)) { + if (mbr.isShadowedOrAmbiguousImplicit) { // These are the members that are shadowing or ambiguating the current implicit // see ImplicitMemberShadowing trait for more information val shadowingSuggestion = { @@ -402,10 +402,10 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp } val shadowingWarning: NodeSeq = - if (Template.isShadowedImplicit(mbr)) + if (mbr.isShadowedImplicit) xml.Text("This implicitly inherited member is shadowed by one or more members in this " + "class.") ++ shadowingSuggestion - else if (Template.isAmbiguousImplicit(mbr)) + else if (mbr.isAmbiguousImplicit) 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 @@ -646,13 +646,13 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp case PrivateInTemplate(owner) if (owner == mbr.inTemplate) => Some(Paragraph(CText("private"))) case PrivateInTemplate(owner) => - Some(Paragraph(Chain(List(CText("private["), EntityLink(owner.qualifiedName, () => Some(owner)), CText("]"))))) + Some(Paragraph(Chain(List(CText("private["), EntityLink(comment.Text(owner.qualifiedName), LinkToTpl(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.qualifiedName, () => Some(owner)), CText("]"))))) + Some(Paragraph(Chain(List(CText("protected["), EntityLink(comment.Text(owner.qualifiedName), LinkToTpl(owner)), CText("]"))))) case Public() => None } @@ -669,8 +669,8 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp <span class="symbol"> { val nameClass = - if (Template.isImplicit(mbr)) - if (Template.isShadowedOrAmbiguousImplicit(mbr)) + if (mbr.isImplicitlyInherited) + if (mbr.isShadowedOrAmbiguousImplicit) "implicit shadowed" else "implicit" @@ -934,12 +934,5 @@ 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 = - 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/model/Entity.scala b/src/compiler/scala/tools/nsc/doc/model/Entity.scala index 41ba95e072..42db408fee 100644 --- a/src/compiler/scala/tools/nsc/doc/model/Entity.scala +++ b/src/compiler/scala/tools/nsc/doc/model/Entity.scala @@ -56,6 +56,12 @@ trait Entity { /** Whether or not the template was defined in a package object */ def inPackageObject: Boolean + + /** Indicates whether this entity lives in the types namespace (classes, traits, abstract/alias types) */ + def isType: Boolean + + /** Indicates whether this entity lives in the terms namespace (objects, packages, methods, values) */ + def isTerm: Boolean } object Entity { @@ -183,6 +189,19 @@ trait MemberEntity extends Entity { /** The identity of this member, used for linking */ def signature: String + + /** Indicates whether the member is inherited by implicit conversion */ + def isImplicitlyInherited: Boolean + + /** Indicates whether there is another member with the same name in the template that will take precendence */ + def isShadowedImplicit: Boolean + + /** Indicates whether there are other implicitly inherited members that have similar signatures (and thus they all + * become ambiguous) */ + def isAmbiguousImplicit: Boolean + + /** Indicates whether the implicitly inherited member is shadowed or ambiguous in its template */ + def isShadowedOrAmbiguousImplicit: Boolean } object MemberEntity { diff --git a/src/compiler/scala/tools/nsc/doc/model/Links.scala b/src/compiler/scala/tools/nsc/doc/model/Links.scala new file mode 100644 index 0000000000..b76dee0f14 --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/model/Links.scala @@ -0,0 +1,24 @@ +/* NSC -- new Scala compiler + * Copyright 2007-2011 LAMP/EPFL + */ + +package scala.tools.nsc +package doc +package model + +import scala.collection._ + +abstract sealed class LinkTo +case class LinkToTpl(tpl: DocTemplateEntity) extends LinkTo +case class LinkToMember(mbr: MemberEntity, inTpl: DocTemplateEntity) extends LinkTo +case class Tooltip(name: String) extends LinkTo { def this(tpl: TemplateEntity) = this(tpl.qualifiedName) } +// case class LinkToExternal(name: String, url: String) extends LinkTo // for SI-191, whenever Manohar will have time +case object NoLink extends LinkTo // you should use Tooltip if you have a name from the user, this is only in case all fails + +object LinkToTpl { + // this makes it easier to create links + def apply(tpl: TemplateEntity) = tpl match { + case dtpl: DocTemplateEntity => new LinkToTpl(dtpl) + case ntpl: TemplateEntity => new Tooltip(ntpl.qualifiedName) + } +} diff --git a/src/compiler/scala/tools/nsc/doc/model/MemberLookup.scala b/src/compiler/scala/tools/nsc/doc/model/MemberLookup.scala new file mode 100644 index 0000000000..7b131c13ef --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/model/MemberLookup.scala @@ -0,0 +1,185 @@ +package scala.tools.nsc +package doc +package model + +import comment._ + +import scala.reflect.internal.util.FakePos //Position + +/** This trait extracts all required information for documentation from compilation units */ +trait MemberLookup { + thisFactory: ModelFactory => + + import global._ + + def memberLookup(pos: Position, query: String, inTplOpt: Option[DocTemplateImpl]): LinkTo = { + assert(modelFinished) + + var members = breakMembers(query) + //println(query + " => " + members) + + // (1) Lookup in the root package, as most of the links are qualified + var linkTo: List[LinkTo] = lookupInRootPackage(pos, members) + + // (2) Recursively go into each + if (inTplOpt.isDefined) { + var currentTpl = inTplOpt.get + while (currentTpl != null && !currentTpl.isRootPackage && (linkTo.isEmpty)) { + linkTo = lookupInTemplate(pos, members, currentTpl) + currentTpl = currentTpl.inTemplate + } + if (currentTpl == null) println("\n\n\n\n\nnull found in:" + inTplOpt + "\n\n\n\n\n\n\n\n") + } + + // (3) Look at external links + if (linkTo.isEmpty) { + // TODO: IF THIS IS THE ROOT PACKAGE, LOOK AT EXTERNAL LINKS + } + + // (4) if we still haven't found anything, create a tooltip, if we found too many, report + if (linkTo.isEmpty){ + if (!settings.docNoLinkWarnings.value) + reporter.warning(pos, "Could not find any member to link for \"" + query + "\".") + Tooltip(query) + } else { + if (linkTo.length > 1) { + + val chosen = + if (linkTo.exists(_.isInstanceOf[LinkToMember])) + linkTo.collect({case lm: LinkToMember => lm}).min(Ordering[MemberEntity].on[LinkToMember](_.mbr)) + else + linkTo.head + + def linkToString(link: LinkTo) = { + val description = + link match { + case lm@LinkToMember(mbr, inTpl) => " * " + mbr.kind + " \"" + mbr.signature + "\" in " + inTpl.kind + " " + inTpl.qualifiedName + case lt@LinkToTpl(tpl) => " * " + tpl.kind + " \"" + tpl.qualifiedName + "\"" + case other => " * " + other.toString + } + val chosenInfo = + if (link == chosen) + " [chosen]" + else + "" + description + chosenInfo + "\n" + } + if (!settings.docNoLinkWarnings.value) + reporter.warning(pos, + "The link target \"" + query + "\" is ambiguous. Several (possibly overloaded) members fit the target:\n" + + linkTo.map(link => linkToString(link)).mkString + + (if (MemberLookup.showExplanation) + "\n\n" + + "Quick crash course on using Scaladoc links\n" + + "==========================================\n" + + "Disambiguating terms and types: Prefix terms with '$' and types with '!' in case both names are in use:\n" + + " - [[scala.collection.immutable.List!.apply class List's apply method]] and\n" + + " - [[scala.collection.immutable.List$.apply object List's apply method]]\n" + + "Disambiguating overloaded members: If a term is overloaded, you can indicate the first part of its signature followed by *:\n" + + " - [[[scala.collection.immutable.List$.fill[A](Int)(⇒A):List[A]* Fill with a single parameter]]]\n" + + " - [[[scala.collection.immutable.List$.fill[A](Int,Int)(⇒A):List[List[A]]* Fill with a two parameters]]]\n" + + "Notes: \n" + + " - you can use any number of matching square brackets to avoid interference with the signature\n" + + " - you can use \\. to escape dots in prefixes (don't forget to use * at the end to match the signature!)\n" + + " - you can use \\# to escape hashes, otherwise they will be considered as delimiters, like dots.\n" + else "") + ) + chosen + } else + linkTo.head + } + } + + private abstract class SearchStrategy + private object BothTypeAndTerm extends SearchStrategy + private object OnlyType extends SearchStrategy + private object OnlyTerm extends SearchStrategy + + private def lookupInRootPackage(pos: Position, members: List[String]) = lookupInTemplate(pos, members, makeRootPackage) + + private def lookupInTemplate(pos: Position, members: List[String], inTpl: DocTemplateImpl): List[LinkTo] = { + // Maintaining compatibility with previous links is a bit tricky here: + // we have a preference for term names for all terms except for the last, where we prefer a class: + // How to do this: + // - at each step we do a DFS search with the prefered strategy + // - if the search doesn't return any members, we backtrack on the last decision + // * we look for terms with the last member's name + // * we look for types with the same name, all the way up + val result = members match { + case Nil => + Nil + case mbrName::Nil => + var members = lookupInTemplate(pos, mbrName, inTpl, OnlyType) + if (members.isEmpty) + members = lookupInTemplate(pos, mbrName, inTpl, OnlyTerm) + + members.map(_ match { + case tpl: DocTemplateEntity => LinkToTpl(tpl) + case mbr => LinkToMember(mbr, inTpl) + }) + + case tplName::rest => + + def completeSearch(mbrs: List[MemberImpl]) = + mbrs.collect({case d:DocTemplateImpl => d}).flatMap(tpl => lookupInTemplate(pos, rest, tpl)) + + var members = completeSearch(lookupInTemplate(pos, tplName, inTpl, OnlyTerm)) + if (members.isEmpty) + members = completeSearch(lookupInTemplate(pos, tplName, inTpl, OnlyType)) + + members + } + //println("lookupInTemplate(" + members + ", " + inTpl + ") => " + result) + result + } + + private def lookupInTemplate(pos: Position, member: String, inTpl: DocTemplateImpl, strategy: SearchStrategy): List[MemberImpl] = { + val name = member.stripSuffix("$").stripSuffix("!").stripSuffix("*") + val result = if (member.endsWith("$")) + inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isTerm)) + else if (member.endsWith("!")) + inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isType)) + else if (member.endsWith("*")) + inTpl.members.filter(mbr => (mbr.signature.startsWith(name))) + else { + if (strategy == BothTypeAndTerm) + inTpl.members.filter(_.name == name) + else if (strategy == OnlyType) + inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isType)) + else if (strategy == OnlyTerm) + inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isTerm)) + else + Nil + } + + //println("lookupInTemplate(" + member + ", " + inTpl + ") => " + result) + result + } + + private def breakMembers(query: String): List[String] = { + // Okay, how does this work? Well: you split on . but you don't want to split on \. => thus the ugly regex + // query.split((?<=[^\\\\])\\.).map(_.replaceAll("\\.")) + // The same code, just faster: + var members = List[String]() + var index = 0 + var last_index = 0 + val length = query.length + while (index < length) { + if ((query.charAt(index) == '.' || query.charAt(index) == '#') && + ((index == 0) || (query.charAt(index-1) != '\\'))) { + + members ::= query.substring(last_index, index).replaceAll("\\\\([#\\.])", "$1") + last_index = index + 1 + } + index += 1 + } + if (last_index < length) + members ::= query.substring(last_index, length).replaceAll("\\\\\\.", ".") + members.reverse + } +} + +object MemberLookup { + private[this] var _showExplanation = true + def showExplanation: Boolean = if (_showExplanation) { _showExplanation = false; true } else false +}
\ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala index 2ce7927f42..2642551aa8 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala @@ -24,7 +24,8 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { with ModelFactoryTypeSupport with DiagramFactory with CommentFactory - with TreeFactory => + with TreeFactory + with MemberLookup => import global._ import definitions.{ ObjectClass, NothingClass, AnyClass, AnyValClass, AnyRefClass } @@ -92,6 +93,8 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def qualifiedName = name def annotations = sym.annotations.map(makeAnnotation) def inPackageObject: Boolean = sym.owner.isModuleClass && sym.owner.sourceModule.isPackageObject + def isType = sym.name.isTypeName + def isTerm = sym.name.isTermName } trait TemplateImpl extends EntityImpl with TemplateEntity { @@ -108,7 +111,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { abstract class MemberImpl(sym: Symbol, implConv: ImplicitConversionImpl, inTpl: DocTemplateImpl) extends EntityImpl(sym, inTpl) with MemberEntity { lazy val comment = { - val commentTpl = + val inRealTpl = /* Variable precendence order for implicitly added members: Take the variable defifinitions from ... * 1. the target of the implicit conversion * 2. the definition template (owner) @@ -121,7 +124,11 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { case _ => inTpl } } else inTpl - if (commentTpl != null) thisFactory.comment(sym, commentTpl) else None + val thisTpl = this match { + case d: DocTemplateImpl => Some(d) + case _ => None + } + if (inRealTpl != null) thisFactory.comment(sym, thisTpl, inRealTpl) else None } override def inTemplate = inTpl override def toRoot: List[MemberImpl] = this :: inTpl.toRoot @@ -167,9 +174,9 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def deprecation = if (sym.isDeprecated) Some((sym.deprecationMessage, sym.deprecationVersion) match { - case (Some(msg), Some(ver)) => parseWiki("''(Since version " + ver + ")'' " + msg, NoPosition) - case (Some(msg), None) => parseWiki(msg, NoPosition) - case (None, Some(ver)) => parseWiki("''(Since version " + ver + ")''", NoPosition) + case (Some(msg), Some(ver)) => parseWiki("''(Since version " + ver + ")'' " + msg, NoPosition, Some(inTpl)) + case (Some(msg), None) => parseWiki(msg, NoPosition, Some(inTpl)) + case (None, Some(ver)) => parseWiki("''(Since version " + ver + ")''", NoPosition, Some(inTpl)) case (None, None) => Body(Nil) }) else @@ -177,9 +184,9 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def migration = if(sym.hasMigrationAnnotation) Some((sym.migrationMessage, sym.migrationVersion) match { - case (Some(msg), Some(ver)) => parseWiki("''(Changed in version " + ver + ")'' " + msg, NoPosition) - case (Some(msg), None) => parseWiki(msg, NoPosition) - case (None, Some(ver)) => parseWiki("''(Changed in version " + ver + ")''", NoPosition) + case (Some(msg), Some(ver)) => parseWiki("''(Changed in version " + ver + ")'' " + msg, NoPosition, Some(inTpl)) + case (Some(msg), None) => parseWiki(msg, NoPosition, Some(inTpl)) + case (None, Some(ver)) => parseWiki("''(Changed in version " + ver + ")''", NoPosition, Some(inTpl)) case (None, None) => Body(Nil) }) else @@ -213,28 +220,34 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { def byConversion = if (implConv ne null) Some(implConv) else None lazy val signature = { - val defParamsString = this match { + def defParams(mbr: Any): String = mbr match { case d: MemberEntity with Def => val paramLists: List[String] = - if (d.valueParams.isEmpty) Nil - else d.valueParams map (ps => ps map (_.resultType.name) mkString ("(",",",")")) - - val tParams = if (d.typeParams.isEmpty) "" else { - def boundsToString(hi: Option[TypeEntity], lo: Option[TypeEntity]): String = { - def bound0(bnd: Option[TypeEntity], pre: String): String = bnd match { - case None => "" - case Some(tpe) => pre ++ tpe.toString - } - bound0(hi, "<:") ++ bound0(lo, ">:") + if (d.valueParams.isEmpty) Nil + else d.valueParams map (ps => ps map (_.resultType.name) mkString ("(",",",")")) + paramLists.mkString + case _ => "" + } + + def tParams(mbr: Any): String = mbr match { + case hk: HigherKinded if !hk.typeParams.isEmpty => + def boundsToString(hi: Option[TypeEntity], lo: Option[TypeEntity]): String = { + def bound0(bnd: Option[TypeEntity], pre: String): String = bnd match { + case None => "" + case Some(tpe) => pre ++ tpe.toString } - "[" + d.typeParams.map(tp => tp.variance + tp.name + boundsToString(tp.hi, tp.lo)).mkString(", ") + "]" + bound0(hi, "<:") ++ bound0(lo, ">:") } - - tParams + paramLists.mkString + "[" + hk.typeParams.map(tp => tp.variance + tp.name + tParams(tp) + boundsToString(tp.hi, tp.lo)).mkString(", ") + "]" case _ => "" } - name + defParamsString +":"+ resultType.name + + (name + tParams(this) + defParams(this) +":"+ resultType.name).replaceAll("\\s","") // no spaces allowed, they break links } + def isImplicitlyInherited = { assert(modelFinished); byConversion.isDefined } + def isShadowedImplicit = isImplicitlyInherited && inTpl.implicitsShadowing.get(this).map(_.isShadowed).getOrElse(false) + def isAmbiguousImplicit = isImplicitlyInherited && inTpl.implicitsShadowing.get(this).map(_.isAmbiguous).getOrElse(false) + def isShadowedOrAmbiguousImplicit = isShadowedImplicit || isAmbiguousImplicit } /** A template that is not documented at all. The class is instantiated during lookups, to indicate that the class @@ -546,7 +559,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { import Streamable._ Path(settings.docRootContent.value) match { case f : File => { - val rootComment = closing(f.inputStream)(is => parse(slurp(is), "", NoPosition)) + val rootComment = closing(f.inputStream)(is => parse(slurp(is), "", NoPosition, Option(inTpl))) Some(rootComment) } case _ => None @@ -658,7 +671,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { 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.asInstanceOf[DocTemplateImpl]) // This hack should be removed after analyser is fixed. + thisFactory.comment(bSym.accessed, None, inTpl.asInstanceOf[DocTemplateImpl]) // This hack should be removed after analyser is fixed. override def isLazyVal = true override def useCaseOf = _useCaseOf }) @@ -744,6 +757,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { inTpl.members.find(_.sym == aSym) } + @deprecated("2.10", "Use findLinkTarget instead!") def findTemplate(query: String): Option[DocTemplateImpl] = { assert(modelFinished) docTemplatesCache.values find { (tpl: DocTemplateImpl) => tpl.qualifiedName == query && !packageDropped(tpl) && !tpl.isObject } @@ -776,7 +790,6 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { } } - /** */ def makeAnnotation(annot: AnnotationInfo): Annotation = { val aSym = annot.symbol diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryTypeSupport.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryTypeSupport.scala index 0463ac0420..5efae3257c 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactoryTypeSupport.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactoryTypeSupport.scala @@ -100,8 +100,8 @@ trait ModelFactoryTypeSupport { // (1) the owner's class LinkToMember(bMbr.get.get, oTpl.get) //ugh else - // (2) if we still couldn't find the owner, make a noDocTemplate for everything (in the correct owner!) - LinkToTpl(makeTemplate(bSym, Some(makeTemplate(owner)))) + // (2) if we still couldn't find the owner, show a tooltip with the qualified name + Tooltip(makeQualifiedName(bSym)) } // SI-4360 Showing prefixes when necessary diff --git a/src/compiler/scala/tools/nsc/doc/model/TypeEntity.scala b/src/compiler/scala/tools/nsc/doc/model/TypeEntity.scala index a16e99bf06..643067cca5 100644 --- a/src/compiler/scala/tools/nsc/doc/model/TypeEntity.scala +++ b/src/compiler/scala/tools/nsc/doc/model/TypeEntity.scala @@ -9,10 +9,6 @@ package model import scala.collection._ -abstract sealed class LinkTo -case class LinkToTpl(tpl: TemplateEntity) extends LinkTo -case class LinkToMember(mbr: MemberEntity, inTpl: DocTemplateEntity) extends LinkTo - /** A type. Note that types and templates contain the same information only for the simplest types. For example, a type * defines how a template's type parameters are instantiated (as in `List[Cow]`), what the template's prefix is * (as in `johnsFarm.Cow`), and supports compound or structural types. */ @@ -28,5 +24,4 @@ abstract class TypeEntity { /** The human-readable representation of this type. */ override def toString = name - } 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 ecc3273903..3ab8fc7805 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala @@ -44,7 +44,6 @@ final case class Body(blocks: Seq[Block]) { case inlines => Some(Chain(inlines)) } } - } /** A block-level element of text, such as a paragraph or code block. */ @@ -67,10 +66,14 @@ 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 Monospace(text: Inline) extends Inline final case class Text(text: String) extends Inline +abstract class EntityLink(val title: Inline) extends Inline { def link: LinkTo } +object EntityLink { + def apply(title: Inline, linkTo: LinkTo) = new EntityLink(title) { def link: LinkTo = linkTo} + def unapply(el: EntityLink): Option[(Inline, LinkTo)] = Some((el.title, el.link)) +} final case class HtmlTag(data: String) extends Inline { def canClose(open: HtmlTag) = { open.data.stripPrefix("<") == data.stripPrefix("</") 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 2099315cc6..eff899b789 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala @@ -23,7 +23,7 @@ import language.postfixOps * * @author Manohar Jonnalagedda * @author Gilles Dubochet */ -trait CommentFactory { thisFactory: ModelFactory with CommentFactory => +trait CommentFactory { thisFactory: ModelFactory with CommentFactory with MemberLookup=> val global: Global import global.{ reporter, definitions } @@ -31,16 +31,16 @@ 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 = { - commentCache += (sym, inTpl) -> parse(docStr, docStr, docPos) + commentCache += (sym, inTpl) -> parse(docStr, docStr, docPos, None) sym } - def comment(sym: global.Symbol, inTpl: DocTemplateImpl): Option[Comment] = { + def comment(sym: global.Symbol, currentTpl: Option[DocTemplateImpl], inTpl: DocTemplateImpl): Option[Comment] = { val key = (sym, inTpl) if (commentCache isDefinedAt key) Some(commentCache(key)) else { - val c = defineComment(sym, inTpl) + val c = defineComment(sym, currentTpl, inTpl) if (c isDefined) commentCache += (sym, inTpl) -> c.get c } @@ -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, currentTpl: Option[DocTemplateImpl], inTpl: DocTemplateImpl):Option[Comment] = { //param accessor case // We just need the @param argument, we put it into the body @@ -87,7 +87,8 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => else { val rawComment = global.expandedDocComment(sym, inTpl.sym).trim if (rawComment != "") { - val c = parse(rawComment, global.rawDocComment(sym), global.docCommentPos(sym)) + val tplOpt = if (currentTpl.isDefined) currentTpl else Some(inTpl) + val c = parse(rawComment, global.rawDocComment(sym), global.docCommentPos(sym), tplOpt) Some(c) } else None @@ -225,7 +226,8 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => * @param comment The expanded comment string (including start and end markers) to be parsed. * @param src The raw comment source string. * @param pos The position of the comment in source. */ - protected def parse(comment: String, src: String, pos: Position): Comment = { + protected def parse(comment: String, src: String, pos: Position, inTplOpt: Option[DocTemplateImpl] = None): Comment = { + assert(!inTplOpt.isDefined || inTplOpt.get != null) /** The cleaned raw comment as a list of lines. Cleaning removes comment * start and end markers, line start markers and unnecessary whitespace. */ @@ -351,7 +353,7 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => val tagsWithoutDiagram = tags.filterNot(pair => pair._1 == inheritDiagramTag || pair._1 == contentDiagramTag) val bodyTags: mutable.Map[TagKey, List[Body]] = - mutable.Map(tagsWithoutDiagram mapValues {tag => tag map (parseWiki(_, pos))} toSeq: _*) + mutable.Map(tagsWithoutDiagram mapValues {tag => tag map (parseWiki(_, pos, inTplOpt))} toSeq: _*) def oneTag(key: SimpleTagKey): Option[Body] = ((bodyTags remove key): @unchecked) match { @@ -384,7 +386,7 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => } val com = createComment ( - body0 = Some(parseWiki(docBody.toString, pos)), + body0 = Some(parseWiki(docBody.toString, pos, inTplOpt)), authors0 = allTags(SimpleTagKey("author")), see0 = allTags(SimpleTagKey("see")), result0 = oneTag(SimpleTagKey("return")), @@ -420,8 +422,10 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => * - Removed start-of-line star and one whitespace afterwards (if present). * - Removed all end-of-line whitespace. * - Only `endOfLine` is used to mark line endings. */ - def parseWiki(string: String, pos: Position): Body = { - new WikiParser(string, pos).document() + def parseWiki(string: String, pos: Position, inTplOpt: Option[DocTemplateImpl]): Body = { + assert(!inTplOpt.isDefined || inTplOpt.get != null) + + new WikiParser(string, pos, inTplOpt).document() } /** TODO @@ -429,7 +433,8 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => * @author Ingo Maier * @author Manohar Jonnalagedda * @author Gilles Dubochet */ - protected final class WikiParser(val buffer: String, pos: Position) extends CharReader(buffer) { wiki => + protected final class WikiParser(val buffer: String, pos: Position, inTplOpt: Option[DocTemplateImpl]) extends CharReader(buffer) { wiki => + assert(!inTplOpt.isDefined || inTplOpt.get != null) var summaryParsed = false @@ -617,7 +622,7 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => else if (check(",,")) subscript() else if (check("[[")) link() else { - readUntil { char == safeTagMarker || check("''") || char == '`' || check("__") || char == '^' || check(",,") || check("[[") || isInlineEnd || checkParaEnded || char == endOfLine } + readUntil { char == safeTagMarker || check("''") || char == '`' || check("__") || char == '^' || check(",,") || check("[[") || check("{{") || isInlineEnd || checkParaEnded || char == endOfLine } Text(getRead()) } } @@ -719,29 +724,27 @@ trait CommentFactory { thisFactory: ModelFactory with CommentFactory => def link(): Inline = { val SchemeUri = """([^:]+:.*)""".r jump("[[") - readUntil { check("]]") || check(" ") } + var parens = 1 + readUntil { parens += 1; !check("[") } + getRead // clear the buffer + val start = "[" * parens + val stop = "]" * parens + //println("link with " + parens + " matching parens") + readUntil { check(stop) || check(" ") } val target = getRead() val title = - if (!check("]]")) Some({ + if (!check(stop)) Some({ jump(" ") - inline(check("]]")) + inline(check(stop)) }) else None - jump("]]") + jump(stop) (target, title) match { case (SchemeUri(uri), optTitle) => Link(uri, optTitle getOrElse Text(uri)) case (qualName, optTitle) => - optTitle foreach (text => reportError(pos, "entity link to " + qualName + " cannot have a custom title'" + text + "'")) - // XXX rather than warning here we should allow unqualified names - // to refer to members of the same package. The "package exists" - // exclusion is because [[scala]] is used in some scaladoc. - if (!qualName.contains(".") && !definitions.packageExists(qualName)) - reportError(pos, "entity link to " + qualName + " should be a fully qualified name") - - // move the template resolution as late as possible - EntityLink(qualName, () => findTemplate(qualName)) + new EntityLink(optTitle getOrElse Text(target)) { def link = memberLookup(pos, target, inTplOpt) } } } diff --git a/src/partest/scala/tools/partest/ScaladocModelTest.scala b/src/partest/scala/tools/partest/ScaladocModelTest.scala index c89dd2cb8f..fb93e98726 100644 --- a/src/partest/scala/tools/partest/ScaladocModelTest.scala +++ b/src/partest/scala/tools/partest/ScaladocModelTest.scala @@ -12,7 +12,7 @@ import scala.tools.nsc.util.CommandLineParser import scala.tools.nsc.doc.{Settings, DocFactory, Universe} import scala.tools.nsc.doc.model._ import scala.tools.nsc.reporters.ConsoleReporter -import scala.tools.nsc.doc.model.comment.Comment +import scala.tools.nsc.doc.model.comment._ /** A class for testing scaladoc model generation * - you need to specify the code in the `code` method @@ -165,10 +165,21 @@ abstract class ScaladocModelTest extends DirectTest { def extractCommentText(c: Comment) = { def extractText(body: Any): String = body match { case s: String => s + case s: Seq[_] => s.toList.map(extractText(_)).mkString case p: Product => p.productIterator.toList.map(extractText(_)).mkString case _ => "" } extractText(c.body) } + + def countLinks(c: Comment, p: EntityLink => Boolean) = { + def countLinks(body: Any): Int = body match { + case el: EntityLink if p(el) => 1 + case s: Seq[_] => s.toList.map(countLinks(_)).sum + case p: Product => p.productIterator.toList.map(countLinks(_)).sum + case _ => 0 + } + countLinks(c.body) + } } } diff --git a/test/scaladoc/resources/links.scala b/test/scaladoc/resources/links.scala new file mode 100644 index 0000000000..679d0b0dce --- /dev/null +++ b/test/scaladoc/resources/links.scala @@ -0,0 +1,57 @@ +// that would be: +// SI-5079 "Scaladoc can't link to an object (only a class or trait)" +// SI-4497 "Links in ScalaDoc - Spec and implementation unsufficient" +// SI-4224 "Wiki-links should support method targets" +// SI-3695 "support non-fully-qualified type links in scaladoc comments" +package scala.test.scaladoc.links { + import language.higherKinds + class C + + trait Target { + type T + type S = String + class C + def foo(i: Int) = 2 + def foo(s: String) = 3 + def foo[A[_]](x: A[String]) = 5 + def foo[A[_[_]]](x: A[List]) = 6 + val bar: Boolean + def baz(c: scala.test.scaladoc.links.C) = 7 + } + + object Target { + type T = Int => Int + type S = Int + class C + def foo(i: Int) = 2 + def foo(z: String) = 3 + def foo[A[_]](x: A[String]) = 5 + def foo[A[_[_]]](x: A[List]) = 6 + val bar: Boolean = false + val onlyInObject = 1 + def baz(c: scala.test.scaladoc.links.C) = 7 + } + + /** + * Links to the trait: + * - [[scala.test.scaladoc.links.Target!.T trait Target -> type T]] + * - [[test.scaladoc.links.Target!.S trait Target -> type S]] + * - [[scaladoc.links.Target!.foo(Int)* trait Target -> def foo]] + * - [[links.Target!.bar trait Target -> def bar]] + * - [[[[Target!.foo[A[_[_]]]* trait Target -> def foo with 3 nested tparams]]]] (should exercise nested parens) + * - [[Target$.T object Target -> type T]] + * - [[Target$.S object Target -> type S]] + * - [[Target$.foo(Str* object Target -> def foo]] + * - [[Target$.bar object Target -> def bar]] + * - [[[[Target$.foo[A[_[_]]]* trait Target -> def foo with 3 nested tparams]]]] (should exercise nested parens) + * - [[Target.onlyInObject object Target -> def foo]] (should find the object) + * - [[Target$.C object Target -> class C]] (should link directly to C, not as a member) + * - [[Target!.C trait Target -> class C]] (should link directly to C, not as a member) + * - [[Target$.baz(links\.C)* object Target -> def baz]] (should use dots in prefix) + * - [[Target!.baz(links\.C)* trait Target -> def baz]] (should use dots in prefix) + * - [[localMethod object TEST -> localMethod]] (should use the current template to resolve link instead of inTpl, that's the package) + */ + object TEST { + def localMethod = 3 + } +} diff --git a/test/scaladoc/run/SI-5235.scala b/test/scaladoc/run/SI-5235.scala index f0c6e1cf17..6295fc7786 100644 --- a/test/scaladoc/run/SI-5235.scala +++ b/test/scaladoc/run/SI-5235.scala @@ -79,8 +79,8 @@ object Test extends ScaladocModelTest { assert(mcReverseType.name == "MyCollection",mcReverseType.name + " == MyCollection") assert(gcReverseType.refEntity(0)._1 == LinkToTpl(GenericColl), gcReverse.qualifiedName + "'s return type has a link to " + GenericColl.qualifiedName) - assert(!scReverseType.refEntity(0)._1.asInstanceOf[LinkToTpl].tpl.isDocTemplate, - scReverse.qualifiedName + "'s return type does not have links") + assert(scReverseType.refEntity(0)._1 == Tooltip("BullSh"), + scReverseType.refEntity(0)._1 + " == Tooltip(\"BullSh\")") assert(mcReverseType.refEntity(0)._1 == LinkToTpl(MyCollection), mcReverse.qualifiedName + "'s return type has a link to " + MyCollection.qualifiedName) } diff --git a/test/scaladoc/run/links.check b/test/scaladoc/run/links.check new file mode 100644 index 0000000000..619c56180b --- /dev/null +++ b/test/scaladoc/run/links.check @@ -0,0 +1 @@ +Done. diff --git a/test/scaladoc/run/links.scala b/test/scaladoc/run/links.scala new file mode 100644 index 0000000000..40ce6368ce --- /dev/null +++ b/test/scaladoc/run/links.scala @@ -0,0 +1,28 @@ +import scala.tools.nsc.doc.model._ +import scala.tools.partest.ScaladocModelTest + +// SI-5079 "Scaladoc can't link to an object (only a class or trait)" +// SI-4497 "Links in ScalaDoc - Spec and implementation unsufficient" +// SI-4224 "Wiki-links should support method targets" +// SI-3695 "support non-fully-qualified type links in scaladoc comments" +object Test extends ScaladocModelTest { + + override def resourceFile = "links.scala" + + // no need for special settings + def scaladocSettings = "" + + 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._ + + // just need to check the member exists, access methods will throw an error if there's a problem + val base = rootPackage._package("scala")._package("test")._package("scaladoc")._package("links") + val TEST = base._object("TEST") + + val memberLinks = countLinks(TEST.comment.get, _.link.isInstanceOf[LinkToMember]) + val templateLinks = countLinks(TEST.comment.get, _.link.isInstanceOf[LinkToTpl]) + assert(memberLinks == 14, memberLinks + " == 14 (the member links in object TEST)") + assert(templateLinks == 2, templateLinks + " == 2 (the template links in object TEST)") + } +}
\ No newline at end of file diff --git a/test/scaladoc/scalacheck/CommentFactoryTest.scala b/test/scaladoc/scalacheck/CommentFactoryTest.scala index b7869d5bf4..5e3141bdc0 100644 --- a/test/scaladoc/scalacheck/CommentFactoryTest.scala +++ b/test/scaladoc/scalacheck/CommentFactoryTest.scala @@ -10,7 +10,13 @@ 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 ModelFactoryTypeSupport with DiagramFactory with CommentFactory with doc.model.TreeFactory => + thisFactory: Factory + with ModelFactoryImplicitSupport + with ModelFactoryTypeSupport + with DiagramFactory + with CommentFactory + with doc.model.TreeFactory + with MemberLookup => def strip(c: Comment): Option[Inline] = { c.body match { @@ -31,7 +37,13 @@ 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 ModelFactoryTypeSupport with DiagramFactory with CommentFactory with doc.model.TreeFactory) + (new Factory(g, settings) + with ModelFactoryImplicitSupport + with ModelFactoryTypeSupport + with DiagramFactory + with CommentFactory + with doc.model.TreeFactory + with MemberLookup) } def parse(src: String, dst: Inline) = { |