summaryrefslogtreecommitdiff
path: root/src/scaladoc/scala/tools/nsc/doc/model/diagram/DiagramDirectiveParser.scala
blob: 44d8886e4ed7c87f35fdd5c8fa9d84668eb33b88 (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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package scala.tools.nsc.doc
package model
package diagram

import model._
import java.util.regex.{Pattern, Matcher}
import scala.util.matching.Regex

/**
 *  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 =>

  import this.global.definitions.AnyRefClass

  ///// 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 hideInheritedNodes: Boolean
    /** Hide a node from the diagram */
    def hideNode(clazz: Node): Boolean
    /** Hide an edge from the diagram */
    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 || template.sym == AnyRefClass)
        FullDiagram
      else
        NoDiagramAtAll

    if (template.comment.isDefined)
      makeDiagramFilter(template, template.comment.get.inheritDiagram, defaultFilter, isInheritanceDiagram = 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, isInheritanceDiagram = 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 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 */
  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 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
   *  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,
                                             hideInheritedNodes: Boolean,
                                             hideNodesFilter: List[Pattern],
                                             hideEdgesFilter: List[(Pattern, Pattern)]) extends DiagramFilter {

    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)
          return true
        }
      false
    }

    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) {
          // 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 + "\\)"
  // 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 hideInheritedNodes0: 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.rootMirror.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 "hideInheritedNodes" if !isInheritanceDiagram =>
              hideInheritedNodes0 = 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) &&
                 (hideInheritedNodes0 == false) &&
                 (hideDiagram0 == false))
          FullDiagram
        else
          AnnotationDiagramFilter(
            hideDiagram = hideDiagram0,
            hideIncomingImplicits = hideIncomingImplicits0,
            hideOutgoingImplicits = hideOutgoingImplicits0,
            hideSuperclasses = hideSuperclasses0,
            hideSubclasses = hideSubclasses0,
            hideInheritedNodes = hideInheritedNodes0,
            hideNodesFilter = hideNodesFilter0,
            hideEdgesFilter = hideEdgesFilter0)

      if (settings.docDiagramsDebug && result != NoDiagramAtAll && result != FullDiagram)
        settings.printMsg(template.kind + " " + template.qualifiedName + " filter: " + result)
      tFilter += System.currentTimeMillis

      result
  }
}