summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/doc/model/MemberLookup.scala
blob: 7ab73cceff46aa409c18369a0c8b120d51dc377f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
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._
  import rootMirror.RootPackage, rootMirror.EmptyPackage

  def makeEntityLink(title: Inline, pos: Position, query: String, inTplOpt: Option[DocTemplateImpl]) =
    new EntityLink(title) { lazy val link = memberLookup(pos, query, inTplOpt) }

  def memberLookup(pos: Position, query: String, inTplOpt: Option[DocTemplateImpl]): LinkTo = {
    assert(modelFinished)

    var members = breakMembers(query)
    //println(query + " => " + members)

    // (1) First look in the root package, as most of the links are qualified
    val fromRoot = lookupInRootPackage(pos, members)

    // (2) Or recursively go into each containing template.
    val fromParents = inTplOpt.fold(Stream.empty[DocTemplateImpl]) { tpl =>
      Stream.iterate(tpl)(_.inTemplate)
    }.takeWhile (tpl => tpl != null && !tpl.isRootPackage).map { tpl =>
      lookupInTemplate(pos, members, tpl.asInstanceOf[EntityImpl].sym)
    }

    val syms = (fromRoot +: fromParents) find (!_.isEmpty) getOrElse Nil
    val linkTo = createLinks(syms) match {
      case Nil if !syms.isEmpty =>
        // (3) Look at external links
        syms.flatMap { case (sym, owner) =>

          // reconstruct the original link
          def linkName(sym: Symbol) = {
            def isRoot(s: Symbol) = s.isRootSymbol || s.isEmptyPackage || s.isEmptyPackageClass
            def nameString(s: Symbol) = s.nameString + (if ((s.isModule || s.isModuleClass) && !s.isPackage) "$" else "")
            val packageSuffix = if (sym.isPackage) ".package" else ""

            sym.ownerChain.reverse.filterNot(isRoot(_)).map(nameString(_)).mkString(".") + packageSuffix
          }

          if (sym.isClass || sym.isModule || sym.isTrait || sym.isPackage)
            findExternalLink(linkName(sym))
          else if (owner.isClass || owner.isModule || owner.isTrait || owner.isPackage)
            findExternalLink(linkName(owner) + "@" + externalSignature(sym))
          else
            None
        }
      case links => links
    }

    //println(createLinks(syms))
    //println(linkTo)

    // (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]) =
    if (members.length == 1)
      lookupInTemplate(pos, members, EmptyPackage) ::: lookupInTemplate(pos, members, RootPackage)
    else
      lookupInTemplate(pos, members, RootPackage)

  private def createLinks(syms: List[(Symbol, Symbol)]): List[LinkTo] =
    syms.flatMap { case (sym, owner) =>
      if (sym.isClass || sym.isModule || sym.isTrait || sym.isPackage)
        findTemplateMaybe(sym) map (LinkToTpl(_))
      else
        findTemplateMaybe(owner) flatMap { inTpl =>
          inTpl.members find (_.asInstanceOf[EntityImpl].sym == sym) map (LinkToMember(_, inTpl))
        }
    }

  private def lookupInTemplate(pos: Position, members: List[String], container: Symbol): List[(Symbol, Symbol)] = {
    // 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 syms = lookupInTemplate(pos, mbrName, container, OnlyType) map ((_, container))
        if (syms.isEmpty)
          syms = lookupInTemplate(pos, mbrName, container, OnlyTerm) map ((_, container))
        syms

      case tplName::rest =>
        def completeSearch(syms: List[Symbol]) =
          syms filter {sym => sym.isPackage || sym.isClass || sym.isModule} flatMap (lookupInTemplate(pos, rest, _))

        completeSearch(lookupInTemplate(pos, tplName, container, OnlyTerm)) match {
          case Nil => completeSearch(lookupInTemplate(pos, tplName, container, OnlyType))
          case syms => syms
      }
    }
    //println("lookupInTemplate(" + members + ", " + container + ") => " + result)
    result
  }

  private def lookupInTemplate(pos: Position, member: String, container: Symbol, strategy: SearchStrategy): List[Symbol] = {
    val name = member.stripSuffix("$").stripSuffix("!").stripSuffix("*")
    def signatureMatch(sym: Symbol): Boolean = externalSignature(sym).startsWith(name)

    // We need to cleanup the bogus classes created by the .class file parser. For example, [[scala.Predef]] resolves
    // to (bogus) class scala.Predef loaded by the class loader -- which we need to eliminate by looking at the info
    // and removing NoType classes
    def cleanupBogusClasses(syms: List[Symbol]) = { syms.filter(_.info != NoType) }

    def syms(name: Name) = container.info.nonPrivateMember(name).alternatives
    def termSyms = cleanupBogusClasses(syms(newTermName(name)))
    def typeSyms = cleanupBogusClasses(syms(newTypeName(name)))

    val result = if (member.endsWith("$"))
      termSyms
    else if (member.endsWith("!"))
      typeSyms
    else if (member.endsWith("*"))
      cleanupBogusClasses(container.info.nonPrivateDecls) filter signatureMatch
    else
      if (strategy == BothTypeAndTerm)
        termSyms ::: typeSyms
      else if (strategy == OnlyType)
        typeSyms
      else if (strategy == OnlyTerm)
        termSyms
      else
        Nil

    //println("lookupInTemplate(" + member + ", " + container + ") => " + 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) != '\\'))) {

        val member = query.substring(last_index, index).replaceAll("\\\\([#\\.])", "$1")
        // we want to allow javadoc-style links [[#member]] -- which requires us to remove empty members from the first
        // elemnt in the list
        if ((member != "") || (!members.isEmpty))
          members ::= member
        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
}