diff options
Diffstat (limited to 'lib/rdoc/code_object/class_module.rb')
| -rw-r--r-- | lib/rdoc/code_object/class_module.rb | 888 |
1 files changed, 888 insertions, 0 deletions
diff --git a/lib/rdoc/code_object/class_module.rb b/lib/rdoc/code_object/class_module.rb new file mode 100644 index 0000000000..f6b0abb2f5 --- /dev/null +++ b/lib/rdoc/code_object/class_module.rb @@ -0,0 +1,888 @@ +# frozen_string_literal: true +## +# ClassModule is the base class for objects representing either a class or a +# module. + +class RDoc::ClassModule < RDoc::Context + + ## + # 1:: + # RDoc 3.7 + # * Added visibility, singleton and file to attributes + # * Added file to constants + # * Added file to includes + # * Added file to methods + # 2:: + # RDoc 3.13 + # * Added extends + # 3:: + # RDoc 4.0 + # * Added sections + # * Added in_files + # * Added parent name + # * Complete Constant dump + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Constants that are aliases for this class or module + + attr_accessor :constant_aliases + + ## + # Comment and the location it came from. Use #add_comment to add comments + + attr_accessor :comment_location + + ## + # Class or module this constant is an alias for + + attr_accessor :is_alias_for + + ## + # Return a RDoc::ClassModule of class +class_type+ that is a copy + # of module +module+. Used to promote modules to classes. + #-- + # TODO move to RDoc::NormalClass (I think) + + def self.from_module(class_type, mod) + klass = class_type.new mod.name + + mod.comment_location.each do |comment, location| + klass.add_comment comment, location + end + + klass.parent = mod.parent + klass.section = mod.section + + klass.attributes.concat mod.attributes + klass.method_list.concat mod.method_list + klass.aliases.concat mod.aliases + klass.external_aliases.concat mod.external_aliases + klass.constants.concat mod.constants + klass.includes.concat mod.includes + klass.extends.concat mod.extends + + klass.methods_hash.update mod.methods_hash + klass.constants_hash.update mod.constants_hash + + klass.current_section = mod.current_section + klass.in_files.concat mod.in_files + klass.sections.concat mod.sections + klass.unmatched_alias_lists = mod.unmatched_alias_lists + klass.current_section = mod.current_section + klass.visibility = mod.visibility + + klass.classes_hash.update mod.classes_hash + klass.modules_hash.update mod.modules_hash + klass.metadata.update mod.metadata + + klass.document_self = mod.received_nodoc ? nil : mod.document_self + klass.document_children = mod.document_children + klass.force_documentation = mod.force_documentation + klass.done_documenting = mod.done_documenting + + # update the parent of all children + + (klass.attributes + + klass.method_list + + klass.aliases + + klass.external_aliases + + klass.constants + + klass.includes + + klass.extends + + klass.classes + + klass.modules).each do |obj| + obj.parent = klass + obj.full_name = nil + end + + klass + end + + ## + # Creates a new ClassModule with +name+ with optional +superclass+ + # + # This is a constructor for subclasses, and must never be called directly. + + def initialize(name, superclass = nil) + @constant_aliases = [] + @is_alias_for = nil + @name = name + @superclass = superclass + @comment_location = [] # [[comment, location]] + + super() + end + + ## + # Adds +comment+ to this ClassModule's list of comments at +location+. This + # method is preferred over #comment= since it allows ri data to be updated + # across multiple runs. + + def add_comment(comment, location) + return unless document_self + + original = comment + + comment = case comment + when RDoc::Comment then + comment.normalize + else + normalize_comment comment + end + + if location.parser == RDoc::Parser::C + @comment_location.delete_if { |(_, l)| l == location } + end + + @comment_location << [comment, location] + + self.comment = original + end + + def add_things(my_things, other_things) # :nodoc: + other_things.each do |group, things| + my_things[group].each { |thing| yield false, thing } if + my_things.include? group + + things.each do |thing| + yield true, thing + end + end + end + + ## + # Ancestors list for this ClassModule: the list of included modules + # (classes will add their superclass if any). + # + # Returns the included classes or modules, not the includes + # themselves. The returned values are either String or + # RDoc::NormalModule instances (see RDoc::Include#module). + # + # The values are returned in reverse order of their inclusion, + # which is the order suitable for searching methods/attributes + # in the ancestors. The superclass, if any, comes last. + + def ancestors + includes.map { |i| i.module }.reverse + end + + def aref_prefix # :nodoc: + raise NotImplementedError, "missing aref_prefix for #{self.class}" + end + + ## + # HTML fragment reference for this module or class. See + # RDoc::NormalClass#aref and RDoc::NormalModule#aref + + def aref + "#{aref_prefix}-#{full_name}" + end + + ## + # Ancestors of this class or module only + + alias direct_ancestors ancestors + + ## + # Clears the comment. Used by the Ruby parser. + + def clear_comment + @comment = '' + end + + ## + # This method is deprecated, use #add_comment instead. + # + # Appends +comment+ to the current comment, but separated by a rule. Works + # more like <tt>+=</tt>. + + def comment=(comment) # :nodoc: + comment = case comment + when RDoc::Comment then + comment.normalize + else + normalize_comment comment + end + + comment = "#{@comment.to_s}\n---\n#{comment.to_s}" unless @comment.empty? + + super comment + end + + ## + # Prepares this ClassModule for use by a generator. + # + # See RDoc::Store#complete + + def complete(min_visibility) + update_aliases + remove_nodoc_children + embed_mixins + update_includes + update_extends + remove_invisible min_visibility + end + + ## + # Does this ClassModule or any of its methods have document_self set? + + def document_self_or_methods + document_self || method_list.any?{ |m| m.document_self } + end + + ## + # Does this class or module have a comment with content or is + # #received_nodoc true? + + def documented? + return true if @received_nodoc + return false if @comment_location.empty? + @comment_location.any? { |comment, _| not comment.empty? } + end + + ## + # Iterates the ancestors of this class or module for which an + # RDoc::ClassModule exists. + + def each_ancestor # :yields: module + return enum_for __method__ unless block_given? + + ancestors.each do |mod| + next if String === mod + next if self == mod + yield mod + end + end + + ## + # Looks for a symbol in the #ancestors. See Context#find_local_symbol. + + def find_ancestor_local_symbol(symbol) + each_ancestor do |m| + res = m.find_local_symbol(symbol) + return res if res + end + + nil + end + + ## + # Finds a class or module with +name+ in this namespace or its descendants + + def find_class_named(name) + return self if full_name == name + return self if @name == name + + @classes.values.find do |klass| + next if klass == self + klass.find_class_named name + end + end + + ## + # Return the fully qualified name of this class or module + + def full_name + @full_name ||= if RDoc::ClassModule === parent then + "#{parent.full_name}::#{@name}" + else + @name + end + end + + ## + # Return array of full_name splitted by +::+. + + def nesting_namespaces + @namespaces ||= full_name.split("::").reject(&:empty?) + end + + ## + # Return array of fully qualified nesting namespaces. + # + # For example, if full_name is +A::B::C+, this method returns <code>["A", "A::B", "A::B::C"]</code> + + def fully_qualified_nesting_namespaces + return nesting_namespaces if nesting_namespaces.length < 2 + @fqns ||= nesting_namespaces.inject([]) do |list, n| + list << (list.empty? ? n : "#{list.last}::#{n}") + end + end + + ## + # TODO: filter included items by #display? + + def marshal_dump # :nodoc: + attrs = attributes.sort.map do |attr| + next unless attr.display? + [ attr.name, attr.rw, + attr.visibility, attr.singleton, attr.file_name, + ] + end.compact + + method_types = methods_by_type.map do |type, visibilities| + visibilities = visibilities.map do |visibility, methods| + method_names = methods.map do |method| + next unless method.display? + [method.name, method.file_name] + end.compact + + [visibility, method_names.uniq] + end + + [type, visibilities] + end + + [ MARSHAL_VERSION, + @name, + full_name, + @superclass, + parse(@comment_location), + attrs, + constants.select { |constant| constant.display? }, + includes.map do |incl| + next unless incl.display? + [incl.name, parse(incl.comment), incl.file_name] + end.compact, + method_types, + extends.map do |ext| + next unless ext.display? + [ext.name, parse(ext.comment), ext.file_name] + end.compact, + @sections.values, + @in_files.map do |tl| + tl.relative_name + end, + parent.full_name, + parent.class, + ] + end + + def marshal_load(array) # :nodoc: + initialize_visibility + initialize_methods_etc + @current_section = nil + @document_self = true + @done_documenting = false + @parent = nil + @temporary_section = nil + @visibility = nil + @classes = {} + @modules = {} + + @name = array[1] + @full_name = array[2] + @superclass = array[3] + document = array[4] + + @comment = RDoc::Comment.from_document document + + @comment_location = if RDoc::Markup::Document === document.parts.first then + document + else + RDoc::Markup::Document.new document + end + + array[5].each do |name, rw, visibility, singleton, file| + singleton ||= false + visibility ||= :public + + attr = RDoc::Attr.new nil, name, rw, nil, singleton: singleton + + add_attribute attr + attr.visibility = visibility + attr.record_location RDoc::TopLevel.new file + end + + array[6].each do |constant, document, file| + case constant + when RDoc::Constant then + add_constant constant + else + constant = add_constant RDoc::Constant.new(constant, nil, RDoc::Comment.from_document(document)) + constant.record_location RDoc::TopLevel.new file + end + end + + array[7].each do |name, document, file| + incl = add_include RDoc::Include.new(name, RDoc::Comment.from_document(document)) + incl.record_location RDoc::TopLevel.new file + end + + array[8].each do |type, visibilities| + visibilities.each do |visibility, methods| + @visibility = visibility + + methods.each do |name, file| + method = RDoc::AnyMethod.new nil, name, singleton: type == 'class' + method.record_location RDoc::TopLevel.new file + add_method method + end + end + end + + array[9].each do |name, document, file| + ext = add_extend RDoc::Extend.new(name, RDoc::Comment.from_document(document)) + ext.record_location RDoc::TopLevel.new file + end if array[9] # Support Marshal version 1 + + sections = (array[10] || []).map do |section| + [section.title, section] + end + + @sections = Hash[*sections.flatten] + @current_section = add_section nil + + @in_files = [] + + (array[11] || []).each do |filename| + record_location RDoc::TopLevel.new filename + end + + @parent_name = array[12] + @parent_class = array[13] + end + + ## + # Merges +class_module+ into this ClassModule. + # + # The data in +class_module+ is preferred over the receiver. + + def merge(class_module) + @parent = class_module.parent + @parent_name = class_module.parent_name + + other_document = parse class_module.comment_location + + if other_document then + document = parse @comment_location + + document = document.merge other_document + + @comment = RDoc::Comment.from_document(document) + @comment_location = document + end + + cm = class_module + other_files = cm.in_files + + merge_collections attributes, cm.attributes, other_files do |add, attr| + if add then + add_attribute attr + else + @attributes.delete attr + @methods_hash.delete attr.pretty_name + end + end + + merge_collections constants, cm.constants, other_files do |add, const| + if add then + add_constant const + else + @constants.delete const + @constants_hash.delete const.name + end + end + + merge_collections includes, cm.includes, other_files do |add, incl| + if add then + add_include incl + else + @includes.delete incl + end + end + + @includes.uniq! # clean up + + merge_collections extends, cm.extends, other_files do |add, ext| + if add then + add_extend ext + else + @extends.delete ext + end + end + + @extends.uniq! # clean up + + merge_collections method_list, cm.method_list, other_files do |add, meth| + if add then + add_method meth + else + @method_list.delete meth + @methods_hash.delete meth.pretty_name + end + end + + merge_sections cm + + self + end + + ## + # Merges collection +mine+ with +other+ preferring other. +other_files+ is + # used to help determine which items should be deleted. + # + # Yields whether the item should be added or removed (true or false) and the + # item to be added or removed. + # + # merge_collections things, other.things, other.in_files do |add, thing| + # if add then + # # add the thing + # else + # # remove the thing + # end + # end + + def merge_collections(mine, other, other_files, &block) # :nodoc: + my_things = mine. group_by { |thing| thing.file } + other_things = other.group_by { |thing| thing.file } + + remove_things my_things, other_files, &block + add_things my_things, other_things, &block + end + + ## + # Merges the comments in this ClassModule with the comments in the other + # ClassModule +cm+. + + def merge_sections(cm) # :nodoc: + my_sections = sections.group_by { |section| section.title } + other_sections = cm.sections.group_by { |section| section.title } + + other_files = cm.in_files + + remove_things my_sections, other_files do |_, section| + @sections.delete section.title + end + + other_sections.each do |group, sections| + if my_sections.include? group + my_sections[group].each do |my_section| + other_section = cm.sections_hash[group] + + my_comments = my_section.comments + other_comments = other_section.comments + + other_files = other_section.in_files + + merge_collections my_comments, other_comments, other_files do |add, comment| + if add then + my_section.add_comment comment + else + my_section.remove_comment comment + end + end + end + else + sections.each do |section| + add_section group, section.comments + end + end + end + end + + ## + # Does this object represent a module? + + def module? + false + end + + ## + # Allows overriding the initial name. + # + # Used for modules and classes that are constant aliases. + + def name=(new_name) + @name = new_name + end + + ## + # Parses +comment_location+ into an RDoc::Markup::Document composed of + # multiple RDoc::Markup::Documents with their file set. + + def parse(comment_location) + case comment_location + when String then + super + when Array then + docs = comment_location.map do |comment, location| + doc = super comment + doc.file = location + doc + end + + RDoc::Markup::Document.new(*docs) + when RDoc::Comment then + doc = super comment_location.text, comment_location.format + doc.file = comment_location.location + doc + when RDoc::Markup::Document then + return comment_location + else + raise ArgumentError, "unknown comment class #{comment_location.class}" + end + end + + ## + # Path to this class or module for use with HTML generator output. + + def path + prefix = options.class_module_path_prefix + return http_url unless prefix + File.join(prefix, http_url) + end + + ## + # Name to use to generate the url: + # modules and classes that are aliases for another + # module or class return the name of the latter. + + def name_for_path + is_alias_for ? is_alias_for.full_name : full_name + end + + ## + # Returns the classes and modules that are not constants + # aliasing another class or module. For use by formatters + # only (caches its result). + + def non_aliases + @non_aliases ||= classes_and_modules.reject { |cm| cm.is_alias_for } + end + + ## + # Updates the child modules or classes of class/module +parent+ by + # deleting the ones that have been removed from the documentation. + # + # +parent_hash+ is either <tt>parent.modules_hash</tt> or + # <tt>parent.classes_hash</tt> and +all_hash+ is ::all_modules_hash or + # ::all_classes_hash. + + def remove_nodoc_children + prefix = self.full_name + '::' + + modules_hash.each_key do |name| + full_name = prefix + name + modules_hash.delete name unless @store.modules_hash[full_name] + end + + classes_hash.each_key do |name| + full_name = prefix + name + classes_hash.delete name unless @store.classes_hash[full_name] + end + end + + def remove_things(my_things, other_files) # :nodoc: + my_things.delete_if do |file, things| + next false unless other_files.include? file + + things.each do |thing| + yield false, thing + end + + true + end + end + + ## + # Search record used by RDoc::Generator::JsonIndex + + def search_record + [ + name, + full_name, + full_name, + '', + path, + '', + snippet(@comment_location), + ] + end + + ## + # Sets the store for this class or module and its contained code objects. + + def store=(store) + super + + @attributes .each do |attr| attr.store = store end + @constants .each do |const| const.store = store end + @includes .each do |incl| incl.store = store end + @extends .each do |ext| ext.store = store end + @method_list.each do |meth| meth.store = store end + end + + ## + # Get the superclass of this class. Attempts to retrieve the superclass + # object, returns the name if it is not known. + + def superclass + @store.find_class_named(@superclass) || @superclass + end + + ## + # Set the superclass of this class to +superclass+ + # + # where +superclass+ is one of: + # + # - +nil+ + # - a String containing the full name of the superclass + # - the RDoc::ClassModule representing the superclass + + def superclass=(superclass) + raise NoMethodError, "#{full_name} is a module" if module? + case superclass + when RDoc::ClassModule + @superclass = superclass.full_name + when nil, String + @superclass = superclass + else + raise TypeError, "superclass must be a String or RDoc::ClassModule, not #{superclass.class}" + end + end + + ## + # Get all super classes of this class in an array. The last element might be + # a string if the name is unknown. + + def super_classes + result = [] + parent = self + while parent = parent.superclass + result << parent + return result if parent.is_a?(String) + end + result + end + + def to_s # :nodoc: + if is_alias_for then + "#{self.class.name} #{self.full_name} -> #{is_alias_for}" + else + super + end + end + + ## + # 'module' or 'class' + + def type + module? ? 'module' : 'class' + end + + ## + # Updates the child modules & classes by replacing the ones that are + # aliases through a constant. + # + # The aliased module/class is replaced in the children and in + # RDoc::Store#modules_hash or RDoc::Store#classes_hash + # by a copy that has <tt>RDoc::ClassModule#is_alias_for</tt> set to + # the aliased module/class, and this copy is added to <tt>#aliases</tt> + # of the aliased module/class. + # + # Formatters can use the #non_aliases method to retrieve children that + # are not aliases, for instance to list the namespace content, since + # the aliased modules are included in the constants of the class/module, + # that are listed separately. + + def update_aliases + constants.each do |const| + next unless cm = const.is_alias_for + cm_alias = cm.dup + cm_alias.name = const.name + + # Don't move top-level aliases under Object, they look ugly there + unless RDoc::TopLevel === cm_alias.parent then + cm_alias.parent = self + cm_alias.full_name = nil # force update for new parent + end + + cm_alias.aliases.clear + cm_alias.is_alias_for = cm + + if cm.module? then + @store.modules_hash[cm_alias.full_name] = cm_alias + modules_hash[const.name] = cm_alias + else + @store.classes_hash[cm_alias.full_name] = cm_alias + classes_hash[const.name] = cm_alias + end + + cm.aliases << cm_alias + end + end + + ## + # Deletes from #includes those whose module has been removed from the + # documentation. + #-- + # FIXME: includes are not reliably removed, see _possible_bug test case + + def update_includes + includes.reject! do |include| + mod = include.module + !(String === mod) && @store.modules_hash[mod.full_name].nil? + end + + includes.uniq! + end + + ## + # Deletes from #extends those whose module has been removed from the + # documentation. + #-- + # FIXME: like update_includes, extends are not reliably removed + + def update_extends + extends.reject! do |ext| + mod = ext.module + + !(String === mod) && @store.modules_hash[mod.full_name].nil? + end + + extends.uniq! + end + + def embed_mixins + return unless options.embed_mixins + + includes.each do |include| + next if String === include.module + include.module.method_list.each do |code_object| + add_method(prepare_to_embed(code_object)) + end + include.module.constants.each do |code_object| + add_constant(prepare_to_embed(code_object)) + end + include.module.attributes.each do |code_object| + add_attribute(prepare_to_embed(code_object)) + end + end + + extends.each do |ext| + next if String === ext.module + ext.module.method_list.each do |code_object| + add_method(prepare_to_embed(code_object, true)) + end + ext.module.attributes.each do |code_object| + add_attribute(prepare_to_embed(code_object, true)) + end + end + end + + private + + def prepare_to_embed(code_object, singleton=false) + code_object = code_object.dup + code_object.mixin_from = code_object.parent + code_object.singleton = true if singleton + set_current_section(code_object.section.title, code_object.section.comment) + # add_method and add_attribute will reassign self's visibility back to the method/attribute + # so we need to sync self's visibility with the object's to properly retain that information + self.visibility = code_object.visibility + code_object + end +end |
