1##
2# ClassModule is the base class for objects representing either a class or a
3# module.
4
5class RDoc::ClassModule < RDoc::Context
6
7  ##
8  # 1::
9  #   RDoc 3.7
10  #   * Added visibility, singleton and file to attributes
11  #   * Added file to constants
12  #   * Added file to includes
13  #   * Added file to methods
14  # 2::
15  #   RDoc 3.13
16  #   * Added extends
17  # 3::
18  #   RDoc 4.0
19  #   * Added sections
20  #   * Added in_files
21  #   * Added parent name
22  #   * Complete Constant dump
23
24  MARSHAL_VERSION = 3 # :nodoc:
25
26  ##
27  # Constants that are aliases for this class or module
28
29  attr_accessor :constant_aliases
30
31  ##
32  # Comment and the location it came from.  Use #add_comment to add comments
33
34  attr_accessor :comment_location
35
36  attr_accessor :diagram # :nodoc:
37
38  ##
39  # Class or module this constant is an alias for
40
41  attr_accessor :is_alias_for
42
43  ##
44  # Return a RDoc::ClassModule of class +class_type+ that is a copy
45  # of module +module+. Used to promote modules to classes.
46  #--
47  # TODO move to RDoc::NormalClass (I think)
48
49  def self.from_module class_type, mod
50    klass = class_type.new mod.name
51
52    mod.comment_location.each do |comment, location|
53      klass.add_comment comment, location
54    end
55
56    klass.parent = mod.parent
57    klass.section = mod.section
58    klass.viewer = mod.viewer
59
60    klass.attributes.concat mod.attributes
61    klass.method_list.concat mod.method_list
62    klass.aliases.concat mod.aliases
63    klass.external_aliases.concat mod.external_aliases
64    klass.constants.concat mod.constants
65    klass.includes.concat mod.includes
66    klass.extends.concat mod.extends
67
68    klass.methods_hash.update mod.methods_hash
69    klass.constants_hash.update mod.constants_hash
70
71    klass.current_section = mod.current_section
72    klass.in_files.concat mod.in_files
73    klass.sections.concat mod.sections
74    klass.unmatched_alias_lists = mod.unmatched_alias_lists
75    klass.current_section = mod.current_section
76    klass.visibility = mod.visibility
77
78    klass.classes_hash.update mod.classes_hash
79    klass.modules_hash.update mod.modules_hash
80    klass.metadata.update mod.metadata
81
82    klass.document_self = mod.received_nodoc ? nil : mod.document_self
83    klass.document_children = mod.document_children
84    klass.force_documentation = mod.force_documentation
85    klass.done_documenting = mod.done_documenting
86
87    # update the parent of all children
88
89    (klass.attributes +
90     klass.method_list +
91     klass.aliases +
92     klass.external_aliases +
93     klass.constants +
94     klass.includes +
95     klass.extends +
96     klass.classes +
97     klass.modules).each do |obj|
98      obj.parent = klass
99      obj.full_name = nil
100    end
101
102    klass
103  end
104
105  ##
106  # Creates a new ClassModule with +name+ with optional +superclass+
107  #
108  # This is a constructor for subclasses, and must never be called directly.
109
110  def initialize(name, superclass = nil)
111    @constant_aliases = []
112    @diagram          = nil
113    @is_alias_for     = nil
114    @name             = name
115    @superclass       = superclass
116    @comment_location = [] # [[comment, location]]
117
118    super()
119  end
120
121  ##
122  # Adds +comment+ to this ClassModule's list of comments at +location+.  This
123  # method is preferred over #comment= since it allows ri data to be updated
124  # across multiple runs.
125
126  def add_comment comment, location
127    return unless document_self
128
129    original = comment
130
131    comment = case comment
132              when RDoc::Comment then
133                comment.normalize
134              else
135                normalize_comment comment
136              end
137
138    @comment_location.delete_if { |(_, l)| l == location }
139
140    @comment_location << [comment, location]
141
142    self.comment = original
143  end
144
145  def add_things my_things, other_things # :nodoc:
146    other_things.each do |group, things|
147      my_things[group].each { |thing| yield false, thing } if
148        my_things.include? group
149
150      things.each do |thing|
151        yield true, thing
152      end
153    end
154  end
155
156  ##
157  # Ancestors list for this ClassModule: the list of included modules
158  # (classes will add their superclass if any).
159  #
160  # Returns the included classes or modules, not the includes
161  # themselves. The returned values are either String or
162  # RDoc::NormalModule instances (see RDoc::Include#module).
163  #
164  # The values are returned in reverse order of their inclusion,
165  # which is the order suitable for searching methods/attributes
166  # in the ancestors. The superclass, if any, comes last.
167
168  def ancestors
169    includes.map { |i| i.module }.reverse
170  end
171
172  ##
173  # Ancestors of this class or module only
174
175  alias direct_ancestors ancestors
176
177  ##
178  # Clears the comment. Used by the ruby parser.
179
180  def clear_comment
181    @comment = ''
182  end
183
184  ##
185  # This method is deprecated, use #add_comment instead.
186  #
187  # Appends +comment+ to the current comment, but separated by a rule.  Works
188  # more like <tt>+=</tt>.
189
190  def comment= comment # :nodoc:
191    comment = case comment
192              when RDoc::Comment then
193                comment.normalize
194              else
195                normalize_comment comment
196              end
197
198    comment = "#{@comment}\n---\n#{comment}" unless @comment.empty?
199
200    super comment
201  end
202
203  ##
204  # Prepares this ClassModule for use by a generator.
205  #
206  # See RDoc::Store#complete
207
208  def complete min_visibility
209    update_aliases
210    remove_nodoc_children
211    update_includes
212    remove_invisible min_visibility
213  end
214
215  ##
216  # Does this ClassModule or any of its methods have document_self set?
217
218  def document_self_or_methods
219    document_self || method_list.any?{ |m| m.document_self }
220  end
221
222  ##
223  # Does this class or module have a comment with content or is
224  # #received_nodoc true?
225
226  def documented?
227    super or !@comment_location.empty?
228  end
229
230  ##
231  # Iterates the ancestors of this class or module for which an
232  # RDoc::ClassModule exists.
233
234  def each_ancestor # :yields: module
235    return enum_for __method__ unless block_given?
236
237    ancestors.each do |mod|
238      next if String === mod
239      next if self == mod
240      yield mod
241    end
242  end
243
244  ##
245  # Looks for a symbol in the #ancestors. See Context#find_local_symbol.
246
247  def find_ancestor_local_symbol symbol
248    each_ancestor do |m|
249      res = m.find_local_symbol(symbol)
250      return res if res
251    end
252
253    nil
254  end
255
256  ##
257  # Finds a class or module with +name+ in this namespace or its descendants
258
259  def find_class_named name
260    return self if full_name == name
261    return self if @name == name
262
263    @classes.values.find do |klass|
264      next if klass == self
265      klass.find_class_named name
266    end
267  end
268
269  ##
270  # Return the fully qualified name of this class or module
271
272  def full_name
273    @full_name ||= if RDoc::ClassModule === parent then
274                     "#{parent.full_name}::#{@name}"
275                   else
276                     @name
277                   end
278  end
279
280  ##
281  # TODO: filter included items by #display?
282
283  def marshal_dump # :nodoc:
284    attrs = attributes.sort.map do |attr|
285      [ attr.name, attr.rw,
286        attr.visibility, attr.singleton, attr.file_name,
287      ]
288    end
289
290    method_types = methods_by_type.map do |type, visibilities|
291      visibilities = visibilities.map do |visibility, methods|
292        method_names = methods.map do |method|
293          [method.name, method.file_name]
294        end
295
296        [visibility, method_names.uniq]
297      end
298
299      [type, visibilities]
300    end
301
302    [ MARSHAL_VERSION,
303      @name,
304      full_name,
305      @superclass,
306      parse(@comment_location),
307      attrs,
308      constants,
309      includes.map do |incl|
310        [incl.name, parse(incl.comment), incl.file_name]
311      end,
312      method_types,
313      extends.map do |ext|
314        [ext.name, parse(ext.comment), ext.file_name]
315      end,
316      @sections.values,
317      @in_files.map do |tl|
318        tl.relative_name
319      end,
320      parent.full_name,
321      parent.class,
322    ]
323  end
324
325  def marshal_load array # :nodoc:
326    initialize_visibility
327    initialize_methods_etc
328    @current_section   = nil
329    @document_self     = true
330    @done_documenting  = false
331    @parent            = nil
332    @temporary_section = nil
333    @visibility        = nil
334    @classes           = {}
335    @modules           = {}
336
337    @name       = array[1]
338    @full_name  = array[2]
339    @superclass = array[3]
340    @comment    = array[4]
341
342    @comment_location = if RDoc::Markup::Document === @comment.parts.first then
343                          @comment
344                        else
345                          RDoc::Markup::Document.new @comment
346                        end
347
348    array[5].each do |name, rw, visibility, singleton, file|
349      singleton  ||= false
350      visibility ||= :public
351
352      attr = RDoc::Attr.new nil, name, rw, nil, singleton
353
354      add_attribute attr
355      attr.visibility = visibility
356      attr.record_location RDoc::TopLevel.new file
357    end
358
359    array[6].each do |constant, comment, file|
360      case constant
361      when RDoc::Constant then
362        add_constant constant
363      else
364        constant = add_constant RDoc::Constant.new(constant, nil, comment)
365        constant.record_location RDoc::TopLevel.new file
366      end
367    end
368
369    array[7].each do |name, comment, file|
370      incl = add_include RDoc::Include.new(name, comment)
371      incl.record_location RDoc::TopLevel.new file
372    end
373
374    array[8].each do |type, visibilities|
375      visibilities.each do |visibility, methods|
376        @visibility = visibility
377
378        methods.each do |name, file|
379          method = RDoc::AnyMethod.new nil, name
380          method.singleton = true if type == 'class'
381          method.record_location RDoc::TopLevel.new file
382          add_method method
383        end
384      end
385    end
386
387    array[9].each do |name, comment, file|
388      ext = add_extend RDoc::Extend.new(name, comment)
389      ext.record_location RDoc::TopLevel.new file
390    end if array[9] # Support Marshal version 1
391
392    sections = (array[10] || []).map do |section|
393      [section.title, section]
394    end
395
396    @sections = Hash[*sections.flatten]
397    @current_section = add_section nil
398
399    @in_files = []
400
401    (array[11] || []).each do |filename|
402      record_location RDoc::TopLevel.new filename
403    end
404
405    @parent_name  = array[12]
406    @parent_class = array[13]
407  end
408
409  ##
410  # Merges +class_module+ into this ClassModule.
411  #
412  # The data in +class_module+ is preferred over the receiver.
413
414  def merge class_module
415    @parent      = class_module.parent
416    @parent_name = class_module.parent_name
417
418    other_document = parse class_module.comment_location
419
420    if other_document then
421      document = parse @comment_location
422
423      document = document.merge other_document
424
425      @comment = @comment_location = document
426    end
427
428    cm = class_module
429    other_files = cm.in_files
430
431    merge_collections attributes, cm.attributes, other_files do |add, attr|
432      if add then
433        add_attribute attr
434      else
435        @attributes.delete attr
436        @methods_hash.delete attr.pretty_name
437      end
438    end
439
440    merge_collections constants, cm.constants, other_files do |add, const|
441      if add then
442        add_constant const
443      else
444        @constants.delete const
445        @constants_hash.delete const.name
446      end
447    end
448
449    merge_collections includes, cm.includes, other_files do |add, incl|
450      if add then
451        add_include incl
452      else
453        @includes.delete incl
454      end
455    end
456
457    @includes.uniq! # clean up
458
459    merge_collections extends, cm.extends, other_files do |add, ext|
460      if add then
461        add_extend ext
462      else
463        @extends.delete ext
464      end
465    end
466
467    @extends.uniq! # clean up
468
469    merge_collections method_list, cm.method_list, other_files do |add, meth|
470      if add then
471        add_method meth
472      else
473        @method_list.delete meth
474        @methods_hash.delete meth.pretty_name
475      end
476    end
477
478    merge_sections cm
479
480    self
481  end
482
483  ##
484  # Merges collection +mine+ with +other+ preferring other.  +other_files+ is
485  # used to help determine which items should be deleted.
486  #
487  # Yields whether the item should be added or removed (true or false) and the
488  # item to be added or removed.
489  #
490  #   merge_collections things, other.things, other.in_files do |add, thing|
491  #     if add then
492  #       # add the thing
493  #     else
494  #       # remove the thing
495  #     end
496  #   end
497
498  def merge_collections mine, other, other_files, &block # :nodoc:
499    my_things    = mine. group_by { |thing| thing.file }
500    other_things = other.group_by { |thing| thing.file }
501
502    remove_things my_things, other_files,  &block
503    add_things    my_things, other_things, &block
504  end
505
506  ##
507  # Merges the comments in this ClassModule with the comments in the other
508  # ClassModule +cm+.
509
510  def merge_sections cm # :nodoc:
511    my_sections    =    sections.group_by { |section| section.title }
512    other_sections = cm.sections.group_by { |section| section.title }
513
514    other_files = cm.in_files
515
516    remove_things my_sections, other_files do |_, section|
517      @sections.delete section.title
518    end
519
520    other_sections.each do |group, sections|
521      if my_sections.include? group
522        my_sections[group].each do |my_section|
523          other_section = cm.sections_hash[group]
524
525          my_comments    = my_section.comments
526          other_comments = other_section.comments
527
528          other_files = other_section.in_files
529
530          merge_collections my_comments, other_comments, other_files do |add, comment|
531            if add then
532              my_section.add_comment comment
533            else
534              my_section.remove_comment comment
535            end
536          end
537        end
538      else
539        sections.each do |section|
540          add_section group, section.comments
541        end
542      end
543    end
544  end
545
546  ##
547  # Does this object represent a module?
548
549  def module?
550    false
551  end
552
553  ##
554  # Allows overriding the initial name.
555  #
556  # Used for modules and classes that are constant aliases.
557
558  def name= new_name
559    @name = new_name
560  end
561
562  ##
563  # Parses +comment_location+ into an RDoc::Markup::Document composed of
564  # multiple RDoc::Markup::Documents with their file set.
565
566  def parse comment_location
567    case comment_location
568    when String then
569      super
570    when Array then
571      docs = comment_location.map do |comment, location|
572        doc = super comment
573        doc.file = location
574        doc
575      end
576
577      RDoc::Markup::Document.new(*docs)
578    when RDoc::Comment then
579      doc = super comment_location.text, comment_location.format
580      doc.file = comment_location.location
581      doc
582    when RDoc::Markup::Document then
583      return comment_location
584    else
585      raise ArgumentError, "unknown comment class #{comment_location.class}"
586    end
587  end
588
589  ##
590  # Path to this class or module for use with HTML generator output.
591
592  def path
593    http_url @store.rdoc.generator.class_dir
594  end
595
596  ##
597  # Name to use to generate the url:
598  # modules and classes that are aliases for another
599  # module or class return the name of the latter.
600
601  def name_for_path
602    is_alias_for ? is_alias_for.full_name : full_name
603  end
604
605  ##
606  # Returns the classes and modules that are not constants
607  # aliasing another class or module. For use by formatters
608  # only (caches its result).
609
610  def non_aliases
611    @non_aliases ||= classes_and_modules.reject { |cm| cm.is_alias_for }
612  end
613
614  ##
615  # Updates the child modules or classes of class/module +parent+ by
616  # deleting the ones that have been removed from the documentation.
617  #
618  # +parent_hash+ is either <tt>parent.modules_hash</tt> or
619  # <tt>parent.classes_hash</tt> and +all_hash+ is ::all_modules_hash or
620  # ::all_classes_hash.
621
622  def remove_nodoc_children
623    prefix = self.full_name + '::'
624
625    modules_hash.each_key do |name|
626      full_name = prefix + name
627      modules_hash.delete name unless @store.modules_hash[full_name]
628    end
629
630    classes_hash.each_key do |name|
631      full_name = prefix + name
632      classes_hash.delete name unless @store.classes_hash[full_name]
633    end
634  end
635
636  def remove_things my_things, other_files # :nodoc:
637    my_things.delete_if do |file, things|
638      next false unless other_files.include? file
639
640      things.each do |thing|
641        yield false, thing
642      end
643
644      true
645    end
646  end
647
648  ##
649  # Search record used by RDoc::Generator::JsonIndex
650
651  def search_record
652    [
653      name,
654      full_name,
655      full_name,
656      '',
657      path,
658      '',
659      snippet(@comment_location),
660    ]
661  end
662
663  ##
664  # Sets the store for this class or module and its contained code objects.
665
666  def store= store
667    super
668
669    @attributes .each do |attr|  attr.store  = store end
670    @constants  .each do |const| const.store = store end
671    @includes   .each do |incl|  incl.store  = store end
672    @extends    .each do |ext|   ext.store   = store end
673    @method_list.each do |meth|  meth.store  = store end
674  end
675
676  ##
677  # Get the superclass of this class.  Attempts to retrieve the superclass
678  # object, returns the name if it is not known.
679
680  def superclass
681    @store.find_class_named(@superclass) || @superclass
682  end
683
684  ##
685  # Set the superclass of this class to +superclass+
686
687  def superclass=(superclass)
688    raise NoMethodError, "#{full_name} is a module" if module?
689    @superclass = superclass
690  end
691
692  def to_s # :nodoc:
693    if is_alias_for then
694      "#{self.class.name} #{self.full_name} -> #{is_alias_for}"
695    else
696      super
697    end
698  end
699
700  ##
701  # 'module' or 'class'
702
703  def type
704    module? ? 'module' : 'class'
705  end
706
707  ##
708  # Updates the child modules & classes by replacing the ones that are
709  # aliases through a constant.
710  #
711  # The aliased module/class is replaced in the children and in
712  # RDoc::Store#modules_hash or RDoc::Store#classes_hash
713  # by a copy that has <tt>RDoc::ClassModule#is_alias_for</tt> set to
714  # the aliased module/class, and this copy is added to <tt>#aliases</tt>
715  # of the aliased module/class.
716  #
717  # Formatters can use the #non_aliases method to retrieve children that
718  # are not aliases, for instance to list the namespace content, since
719  # the aliased modules are included in the constants of the class/module,
720  # that are listed separately.
721
722  def update_aliases
723    constants.each do |const|
724      next unless cm = const.is_alias_for
725      cm_alias = cm.dup
726      cm_alias.name = const.name
727
728      # Don't move top-level aliases under Object, they look ugly there
729      unless RDoc::TopLevel === cm_alias.parent then
730        cm_alias.parent = self
731        cm_alias.full_name = nil # force update for new parent
732      end
733
734      cm_alias.aliases.clear
735      cm_alias.is_alias_for = cm
736
737      if cm.module? then
738        @store.modules_hash[cm_alias.full_name] = cm_alias
739        modules_hash[const.name] = cm_alias
740      else
741        @store.classes_hash[cm_alias.full_name] = cm_alias
742        classes_hash[const.name] = cm_alias
743      end
744
745      cm.aliases << cm_alias
746    end
747  end
748
749  ##
750  # Deletes from #includes those whose module has been removed from the
751  # documentation.
752  #--
753  # FIXME: includes are not reliably removed, see _possible_bug test case
754
755  def update_includes
756    includes.reject! do |include|
757      mod = include.module
758      !(String === mod) && @store.modules_hash[mod.full_name].nil?
759    end
760
761    includes.uniq!
762  end
763
764  ##
765  # Deletes from #extends those whose module has been removed from the
766  # documentation.
767  #--
768  # FIXME: like update_includes, extends are not reliably removed
769
770  def update_extends
771    extends.reject! do |ext|
772      mod = ext.module
773
774      !(String === mod) && @store.modules_hash[mod.full_name].nil?
775    end
776
777    extends.uniq!
778  end
779
780end
781
782