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