diff options
Diffstat (limited to 'lib/rdoc/code_object')
-rw-r--r-- | lib/rdoc/code_object/alias.rb | 111 | ||||
-rw-r--r-- | lib/rdoc/code_object/anon_class.rb | 10 | ||||
-rw-r--r-- | lib/rdoc/code_object/any_method.rb | 379 | ||||
-rw-r--r-- | lib/rdoc/code_object/attr.rb | 175 | ||||
-rw-r--r-- | lib/rdoc/code_object/class_module.rb | 801 | ||||
-rw-r--r-- | lib/rdoc/code_object/constant.rb | 186 | ||||
-rw-r--r-- | lib/rdoc/code_object/context.rb | 1264 | ||||
-rw-r--r-- | lib/rdoc/code_object/context/section.rb | 233 | ||||
-rw-r--r-- | lib/rdoc/code_object/extend.rb | 9 | ||||
-rw-r--r-- | lib/rdoc/code_object/ghost_method.rb | 6 | ||||
-rw-r--r-- | lib/rdoc/code_object/include.rb | 9 | ||||
-rw-r--r-- | lib/rdoc/code_object/meta_method.rb | 6 | ||||
-rw-r--r-- | lib/rdoc/code_object/method_attr.rb | 418 | ||||
-rw-r--r-- | lib/rdoc/code_object/mixin.rb | 120 | ||||
-rw-r--r-- | lib/rdoc/code_object/normal_class.rb | 92 | ||||
-rw-r--r-- | lib/rdoc/code_object/normal_module.rb | 73 | ||||
-rw-r--r-- | lib/rdoc/code_object/require.rb | 51 | ||||
-rw-r--r-- | lib/rdoc/code_object/single_class.rb | 30 | ||||
-rw-r--r-- | lib/rdoc/code_object/top_level.rb | 291 |
19 files changed, 4264 insertions, 0 deletions
diff --git a/lib/rdoc/code_object/alias.rb b/lib/rdoc/code_object/alias.rb new file mode 100644 index 0000000000..446cf9ccb4 --- /dev/null +++ b/lib/rdoc/code_object/alias.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true +## +# Represent an alias, which is an old_name/new_name pair associated with a +# particular context +#-- +# TODO implement Alias as a proxy to a method/attribute, inheriting from +# MethodAttr + +class RDoc::Alias < RDoc::CodeObject + + ## + # Aliased method's name + + attr_reader :new_name + + alias name new_name + + ## + # Aliasee method's name + + attr_reader :old_name + + ## + # Is this an alias declared in a singleton context? + + attr_accessor :singleton + + ## + # Source file token stream + + attr_reader :text + + ## + # Creates a new Alias with a token stream of +text+ that aliases +old_name+ + # to +new_name+, has +comment+ and is a +singleton+ context. + + def initialize(text, old_name, new_name, comment, singleton = false) + super() + + @text = text + @singleton = singleton + @old_name = old_name + @new_name = new_name + self.comment = comment + end + + ## + # Order by #singleton then #new_name + + def <=>(other) + [@singleton ? 0 : 1, new_name] <=> [other.singleton ? 0 : 1, other.new_name] + end + + ## + # HTML fragment reference for this alias + + def aref + type = singleton ? 'c' : 'i' + "#alias-#{type}-#{html_name}" + end + + ## + # Full old name including namespace + + def full_old_name + @full_name || "#{parent.name}#{pretty_old_name}" + end + + ## + # HTML id-friendly version of +#new_name+. + + def html_name + CGI.escape(@new_name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + end + + def inspect # :nodoc: + parent_name = parent ? parent.name : '(unknown)' + "#<%s:0x%x %s.alias_method %s, %s>" % [ + self.class, object_id, + parent_name, @old_name, @new_name, + ] + end + + ## + # '::' for the alias of a singleton method/attribute, '#' for instance-level. + + def name_prefix + singleton ? '::' : '#' + end + + ## + # Old name with prefix '::' or '#'. + + def pretty_old_name + "#{singleton ? '::' : '#'}#{@old_name}" + end + + ## + # New name with prefix '::' or '#'. + + def pretty_new_name + "#{singleton ? '::' : '#'}#{@new_name}" + end + + alias pretty_name pretty_new_name + + def to_s # :nodoc: + "alias: #{self.new_name} -> #{self.pretty_old_name} in: #{parent}" + end + +end diff --git a/lib/rdoc/code_object/anon_class.rb b/lib/rdoc/code_object/anon_class.rb new file mode 100644 index 0000000000..3c2f0e1877 --- /dev/null +++ b/lib/rdoc/code_object/anon_class.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +## +# An anonymous class like: +# +# c = Class.new do end +# +# AnonClass is currently not used. + +class RDoc::AnonClass < RDoc::ClassModule +end diff --git a/lib/rdoc/code_object/any_method.rb b/lib/rdoc/code_object/any_method.rb new file mode 100644 index 0000000000..465c4a4fb2 --- /dev/null +++ b/lib/rdoc/code_object/any_method.rb @@ -0,0 +1,379 @@ +# frozen_string_literal: true +## +# AnyMethod is the base class for objects representing methods + +class RDoc::AnyMethod < RDoc::MethodAttr + + ## + # 2:: + # RDoc 4 + # Added calls_super + # Added parent name and class + # Added section title + # 3:: + # RDoc 4.1 + # Added is_alias_for + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Don't rename \#initialize to \::new + + attr_accessor :dont_rename_initialize + + ## + # The C function that implements this method (if it was defined in a C file) + + attr_accessor :c_function + + # The section title of the method (if defined in a C file via +:category:+) + attr_accessor :section_title + + # Parameters for this method + + attr_accessor :params + + ## + # If true this method uses +super+ to call a superclass version + + attr_accessor :calls_super + + include RDoc::TokenStream + + ## + # Creates a new AnyMethod with a token stream +text+ and +name+ + + def initialize text, name + super + + @c_function = nil + @dont_rename_initialize = false + @token_stream = nil + @calls_super = false + @superclass_method = nil + end + + ## + # Adds +an_alias+ as an alias for this method in +context+. + + def add_alias an_alias, context = nil + method = self.class.new an_alias.text, an_alias.new_name + + method.record_location an_alias.file + method.singleton = self.singleton + method.params = self.params + method.visibility = self.visibility + method.comment = an_alias.comment + method.is_alias_for = self + @aliases << method + context.add_method method if context + method + end + + ## + # Prefix for +aref+ is 'method'. + + def aref_prefix + 'method' + end + + ## + # The call_seq or the param_seq with method name, if there is no call_seq. + # + # Use this for displaying a method's argument lists. + + def arglists + if @call_seq then + @call_seq + elsif @params then + "#{name}#{param_seq}" + end + end + + ## + # Different ways to call this method + + def call_seq + unless call_seq = _call_seq + call_seq = is_alias_for._call_seq if is_alias_for + end + + return unless call_seq + + deduplicate_call_seq(call_seq) + end + + ## + # Sets the different ways you can call this method. If an empty +call_seq+ + # is given nil is assumed. + # + # See also #param_seq + + def call_seq= call_seq + return if call_seq.empty? + + @call_seq = call_seq + end + + ## + # Whether the method has a call-seq. + + def has_call_seq? + !!(@call_seq || is_alias_for&._call_seq) + end + + ## + # Loads is_alias_for from the internal name. Returns nil if the alias + # cannot be found. + + def is_alias_for # :nodoc: + case @is_alias_for + when RDoc::MethodAttr then + @is_alias_for + when Array then + return nil unless @store + + klass_name, singleton, method_name = @is_alias_for + + return nil unless klass = @store.find_class_or_module(klass_name) + + @is_alias_for = klass.find_method method_name, singleton + end + end + + ## + # Dumps this AnyMethod for use by ri. See also #marshal_load + + def marshal_dump + aliases = @aliases.map do |a| + [a.name, parse(a.comment)] + end + + is_alias_for = [ + @is_alias_for.parent.full_name, + @is_alias_for.singleton, + @is_alias_for.name + ] if @is_alias_for + + [ MARSHAL_VERSION, + @name, + full_name, + @singleton, + @visibility, + parse(@comment), + @call_seq, + @block_params, + aliases, + @params, + @file.relative_name, + @calls_super, + @parent.name, + @parent.class, + @section.title, + is_alias_for, + ] + end + + ## + # Loads this AnyMethod from +array+. For a loaded AnyMethod the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize_visibility + + @dont_rename_initialize = nil + @token_stream = nil + @aliases = [] + @parent = nil + @parent_name = nil + @parent_class = nil + @section = nil + @file = nil + + version = array[0] + @name = array[1] + @full_name = array[2] + @singleton = array[3] + @visibility = array[4] + @comment = array[5] + @call_seq = array[6] + @block_params = array[7] + # 8 handled below + @params = array[9] + # 10 handled below + @calls_super = array[11] + @parent_name = array[12] + @parent_title = array[13] + @section_title = array[14] + @is_alias_for = array[15] + + array[8].each do |new_name, comment| + add_alias RDoc::Alias.new(nil, @name, new_name, comment, @singleton) + end + + @parent_name ||= if @full_name =~ /#/ then + $` + else + name = @full_name.split('::') + name.pop + name.join '::' + end + + @file = RDoc::TopLevel.new array[10] if version > 0 + end + + ## + # Method name + # + # If the method has no assigned name, it extracts it from #call_seq. + + def name + return @name if @name + + @name = + @call_seq[/^.*?\.(\w+)/, 1] || + @call_seq[/^.*?(\w+)/, 1] || + @call_seq if @call_seq + end + + ## + # A list of this method's method and yield parameters. +call-seq+ params + # are preferred over parsed method and block params. + + def param_list + if @call_seq then + params = @call_seq.split("\n").last + params = params.sub(/.*?\((.*)\)/, '\1') + params = params.sub(/(\{|do)\s*\|([^|]*)\|.*/, ',\2') + elsif @params then + params = @params.sub(/\((.*)\)/, '\1') + + params << ",#{@block_params}" if @block_params + elsif @block_params then + params = @block_params + else + return [] + end + + if @block_params then + # If this method has explicit block parameters, remove any explicit + # &block + params = params.sub(/,?\s*&\w+/, '') + else + params = params.sub(/\&(\w+)/, '\1') + end + + params = params.gsub(/\s+/, '').split(',').reject(&:empty?) + + params.map { |param| param.sub(/=.*/, '') } + end + + ## + # Pretty parameter list for this method. If the method's parameters were + # given by +call-seq+ it is preferred over the parsed values. + + def param_seq + if @call_seq then + params = @call_seq.split("\n").last + params = params.sub(/[^( ]+/, '') + params = params.sub(/(\|[^|]+\|)\s*\.\.\.\s*(end|\})/, '\1 \2') + elsif @params then + params = @params.gsub(/\s*\#.*/, '') + params = params.tr_s("\n ", " ") + params = "(#{params})" unless params[0] == ?( + else + params = '' + end + + if @block_params then + # If this method has explicit block parameters, remove any explicit + # &block + params = params.sub(/,?\s*&\w+/, '') + + block = @block_params.tr_s("\n ", " ") + if block[0] == ?( + block = block.sub(/^\(/, '').sub(/\)/, '') + end + params << " { |#{block}| ... }" + end + + params + end + + ## + # Whether to skip the method description, true for methods that have + # aliases with a call-seq that doesn't include the method name. + + def skip_description? + has_call_seq? && call_seq.nil? && !!(is_alias_for || !aliases.empty?) + end + + ## + # Sets the store for this method and its referenced code objects. + + def store= store + super + + @file = @store.add_file @file.full_name if @file + end + + ## + # For methods that +super+, find the superclass method that would be called. + + def superclass_method + return unless @calls_super + return @superclass_method if @superclass_method + + parent.each_ancestor do |ancestor| + if method = ancestor.method_list.find { |m| m.name == @name } then + @superclass_method = method + break + end + end + + @superclass_method + end + + protected + + ## + # call_seq without deduplication and alias lookup. + + def _call_seq + @call_seq if defined?(@call_seq) && @call_seq + end + + private + + ## + # call_seq with alias examples information removed, if this + # method is an alias method. + + def deduplicate_call_seq(call_seq) + return call_seq unless is_alias_for || !aliases.empty? + + method_name = self.name + method_name = method_name[0, 1] if method_name =~ /\A\[/ + + entries = call_seq.split "\n" + + ignore = aliases.map(&:name) + if is_alias_for + ignore << is_alias_for.name + ignore.concat is_alias_for.aliases.map(&:name) + end + ignore.map! { |n| n =~ /\A\[/ ? /\[.*\]/ : n} + ignore.delete(method_name) + ignore = Regexp.union(ignore) + + matching = entries.reject do |entry| + entry =~ /^\w*\.?#{ignore}[$\(\s]/ or + entry =~ /\s#{ignore}\s/ + end + + matching.empty? ? nil : matching.join("\n") + end +end diff --git a/lib/rdoc/code_object/attr.rb b/lib/rdoc/code_object/attr.rb new file mode 100644 index 0000000000..a403235933 --- /dev/null +++ b/lib/rdoc/code_object/attr.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true +## +# An attribute created by \#attr, \#attr_reader, \#attr_writer or +# \#attr_accessor + +class RDoc::Attr < RDoc::MethodAttr + + ## + # 3:: + # RDoc 4 + # Added parent name and class + # Added section title + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Is the attribute readable ('R'), writable ('W') or both ('RW')? + + attr_accessor :rw + + ## + # Creates a new Attr with body +text+, +name+, read/write status +rw+ and + # +comment+. +singleton+ marks this as a class attribute. + + def initialize(text, name, rw, comment, singleton = false) + super text, name + + @rw = rw + @singleton = singleton + self.comment = comment + end + + ## + # Attributes are equal when their names, singleton and rw are identical + + def == other + self.class == other.class and + self.name == other.name and + self.rw == other.rw and + self.singleton == other.singleton + end + + ## + # Add +an_alias+ as an attribute in +context+. + + def add_alias(an_alias, context) + new_attr = self.class.new(self.text, an_alias.new_name, self.rw, + self.comment, self.singleton) + + new_attr.record_location an_alias.file + new_attr.visibility = self.visibility + new_attr.is_alias_for = self + @aliases << new_attr + context.add_attribute new_attr + new_attr + end + + ## + # The #aref prefix for attributes + + def aref_prefix + 'attribute' + end + + ## + # Attributes never call super. See RDoc::AnyMethod#calls_super + # + # An RDoc::Attr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def calls_super # :nodoc: + false + end + + ## + # Returns attr_reader, attr_writer or attr_accessor as appropriate. + + def definition + case @rw + when 'RW' then 'attr_accessor' + when 'R' then 'attr_reader' + when 'W' then 'attr_writer' + end + end + + def inspect # :nodoc: + alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil + visibility = self.visibility + visibility = "forced #{visibility}" if force_documentation + "#<%s:0x%x %s %s (%s)%s>" % [ + self.class, object_id, + full_name, + rw, + visibility, + alias_for, + ] + end + + ## + # Dumps this Attr for use by ri. See also #marshal_load + + def marshal_dump + [ MARSHAL_VERSION, + @name, + full_name, + @rw, + @visibility, + parse(@comment), + singleton, + @file.relative_name, + @parent.full_name, + @parent.class, + @section.title + ] + end + + ## + # Loads this Attr from +array+. For a loaded Attr the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize_visibility + + @aliases = [] + @parent = nil + @parent_name = nil + @parent_class = nil + @section = nil + @file = nil + + version = array[0] + @name = array[1] + @full_name = array[2] + @rw = array[3] + @visibility = array[4] + @comment = array[5] + @singleton = array[6] || false # MARSHAL_VERSION == 0 + # 7 handled below + @parent_name = array[8] + @parent_class = array[9] + @section_title = array[10] + + @file = RDoc::TopLevel.new array[7] if version > 1 + + @parent_name ||= @full_name.split('#', 2).first + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class.name} #{full_name} #{rw} #{visibility}", "]" do + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + def to_s # :nodoc: + "#{definition} #{name} in: #{parent}" + end + + ## + # Attributes do not have token streams. + # + # An RDoc::Attr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def token_stream # :nodoc: + end + +end diff --git a/lib/rdoc/code_object/class_module.rb b/lib/rdoc/code_object/class_module.rb new file mode 100644 index 0000000000..c69e14b5e4 --- /dev/null +++ b/lib/rdoc/code_object/class_module.rb @@ -0,0 +1,801 @@ +# 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 + + attr_accessor :diagram # :nodoc: + + ## + # 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.viewer = mod.viewer + + 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 = [] + @diagram = nil + @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 + update_includes + 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 + + ## + # 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] + @comment = array[4] + + @comment_location = if RDoc::Markup::Document === @comment.parts.first then + @comment + else + RDoc::Markup::Document.new @comment + end + + array[5].each do |name, rw, visibility, singleton, file| + singleton ||= false + visibility ||= :public + + attr = RDoc::Attr.new nil, name, rw, nil, singleton + + add_attribute attr + attr.visibility = visibility + attr.record_location RDoc::TopLevel.new file + end + + array[6].each do |constant, comment, file| + case constant + when RDoc::Constant then + add_constant constant + else + constant = add_constant RDoc::Constant.new(constant, nil, comment) + constant.record_location RDoc::TopLevel.new file + end + end + + array[7].each do |name, comment, file| + incl = add_include RDoc::Include.new(name, comment) + 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 + method.singleton = true if type == 'class' + method.record_location RDoc::TopLevel.new file + add_method method + end + end + end + + array[9].each do |name, comment, file| + ext = add_extend RDoc::Extend.new(name, comment) + 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 = @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 + http_url @store.rdoc.generator.class_dir + 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+ + + def superclass=(superclass) + raise NoMethodError, "#{full_name} is a module" if module? + @superclass = superclass + 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 + +end diff --git a/lib/rdoc/code_object/constant.rb b/lib/rdoc/code_object/constant.rb new file mode 100644 index 0000000000..12b8be775c --- /dev/null +++ b/lib/rdoc/code_object/constant.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true +## +# A constant + +class RDoc::Constant < RDoc::CodeObject + + MARSHAL_VERSION = 0 # :nodoc: + + ## + # Sets the module or class this is constant is an alias for. + + attr_writer :is_alias_for + + ## + # The constant's name + + attr_accessor :name + + ## + # The constant's value + + attr_accessor :value + + ## + # The constant's visibility + + attr_accessor :visibility + + ## + # Creates a new constant with +name+, +value+ and +comment+ + + def initialize(name, value, comment) + super() + + @name = name + @value = value + + @is_alias_for = nil + @visibility = :public + + self.comment = comment + end + + ## + # Constants are ordered by name + + def <=> other + return unless self.class === other + + [parent_name, name] <=> [other.parent_name, other.name] + end + + ## + # Constants are equal when their #parent and #name is the same + + def == other + self.class == other.class and + @parent == other.parent and + @name == other.name + end + + ## + # A constant is documented if it has a comment, or is an alias + # for a documented class or module. + + def documented? + return true if super + return false unless @is_alias_for + case @is_alias_for + when String then + found = @store.find_class_or_module @is_alias_for + return false unless found + @is_alias_for = found + end + @is_alias_for.documented? + end + + ## + # Full constant name including namespace + + def full_name + @full_name ||= "#{parent_name}::#{@name}" + end + + ## + # The module or class this constant is an alias for + + def is_alias_for + case @is_alias_for + when String then + found = @store.find_class_or_module @is_alias_for + @is_alias_for = found if found + @is_alias_for + else + @is_alias_for + end + end + + def inspect # :nodoc: + "#<%s:0x%x %s::%s>" % [ + self.class, object_id, + parent_name, @name, + ] + end + + ## + # Dumps this Constant for use by ri. See also #marshal_load + + def marshal_dump + alias_name = case found = is_alias_for + when RDoc::CodeObject then found.full_name + else found + end + + [ MARSHAL_VERSION, + @name, + full_name, + @visibility, + alias_name, + parse(@comment), + @file.relative_name, + parent.name, + parent.class, + section.title, + ] + end + + ## + # Loads this Constant from +array+. For a loaded Constant the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize array[1], nil, array[5] + + @full_name = array[2] + @visibility = array[3] || :public + @is_alias_for = array[4] + # 5 handled above + # 6 handled below + @parent_name = array[7] + @parent_class = array[8] + @section_title = array[9] + + @file = RDoc::TopLevel.new array[6] + end + + ## + # Path to this constant for use with HTML generator output. + + def path + "#{@parent.path}##{@name}" + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class.name} #{full_name}", "]" do + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + ## + # Sets the store for this class or module and its contained code objects. + + def store= store + super + + @file = @store.add_file @file.full_name if @file + end + + def to_s # :nodoc: + parent_name = parent ? parent.full_name : '(unknown)' + if is_alias_for + "constant #{parent_name}::#@name -> #{is_alias_for}" + else + "constant #{parent_name}::#@name" + end + end + +end diff --git a/lib/rdoc/code_object/context.rb b/lib/rdoc/code_object/context.rb new file mode 100644 index 0000000000..c688d562c3 --- /dev/null +++ b/lib/rdoc/code_object/context.rb @@ -0,0 +1,1264 @@ +# frozen_string_literal: true +## +# A Context is something that can hold modules, classes, methods, attributes, +# aliases, requires, and includes. Classes, modules, and files are all +# Contexts. + +class RDoc::Context < RDoc::CodeObject + + include Comparable + + ## + # Types of methods + + TYPES = %w[class instance] + + ## + # If a context has these titles it will be sorted in this order. + + TOMDOC_TITLES = [nil, 'Public', 'Internal', 'Deprecated'] # :nodoc: + TOMDOC_TITLES_SORT = TOMDOC_TITLES.sort_by { |title| title.to_s } # :nodoc: + + ## + # Class/module aliases + + attr_reader :aliases + + ## + # All attr* methods + + attr_reader :attributes + + ## + # Block params to be used in the next MethodAttr parsed under this context + + attr_accessor :block_params + + ## + # Constants defined + + attr_reader :constants + + ## + # Sets the current documentation section of documentation + + attr_writer :current_section + + ## + # Files this context is found in + + attr_reader :in_files + + ## + # Modules this context includes + + attr_reader :includes + + ## + # Modules this context is extended with + + attr_reader :extends + + ## + # Methods defined in this context + + attr_reader :method_list + + ## + # Name of this class excluding namespace. See also full_name + + attr_reader :name + + ## + # Files this context requires + + attr_reader :requires + + ## + # Use this section for the next method, attribute or constant added. + + attr_accessor :temporary_section + + ## + # Hash <tt>old_name => [aliases]</tt>, for aliases + # that haven't (yet) been resolved to a method/attribute. + # (Not to be confused with the aliases of the context.) + + attr_accessor :unmatched_alias_lists + + ## + # Aliases that could not be resolved. + + attr_reader :external_aliases + + ## + # Current visibility of this context + + attr_accessor :visibility + + ## + # Current visibility of this line + + attr_writer :current_line_visibility + + ## + # Hash of registered methods. Attributes are also registered here, + # twice if they are RW. + + attr_reader :methods_hash + + ## + # Params to be used in the next MethodAttr parsed under this context + + attr_accessor :params + + ## + # Hash of registered constants. + + attr_reader :constants_hash + + ## + # Creates an unnamed empty context with public current visibility + + def initialize + super + + @in_files = [] + + @name ||= "unknown" + @parent = nil + @visibility = :public + + @current_section = Section.new self, nil, nil + @sections = { nil => @current_section } + @temporary_section = nil + + @classes = {} + @modules = {} + + initialize_methods_etc + end + + ## + # Sets the defaults for methods and so-forth + + def initialize_methods_etc + @method_list = [] + @attributes = [] + @aliases = [] + @requires = [] + @includes = [] + @extends = [] + @constants = [] + @external_aliases = [] + @current_line_visibility = nil + + # This Hash maps a method name to a list of unmatched aliases (aliases of + # a method not yet encountered). + @unmatched_alias_lists = {} + + @methods_hash = {} + @constants_hash = {} + + @params = nil + + @store ||= nil + end + + ## + # Contexts are sorted by full_name + + def <=>(other) + return nil unless RDoc::CodeObject === other + + full_name <=> other.full_name + end + + ## + # Adds an item of type +klass+ with the given +name+ and +comment+ to the + # context. + # + # Currently only RDoc::Extend and RDoc::Include are supported. + + def add klass, name, comment + if RDoc::Extend == klass then + ext = RDoc::Extend.new name, comment + add_extend ext + elsif RDoc::Include == klass then + incl = RDoc::Include.new name, comment + add_include incl + else + raise NotImplementedError, "adding a #{klass} is not implemented" + end + end + + ## + # Adds +an_alias+ that is automatically resolved + + def add_alias an_alias + return an_alias unless @document_self + + method_attr = find_method(an_alias.old_name, an_alias.singleton) || + find_attribute(an_alias.old_name, an_alias.singleton) + + if method_attr then + method_attr.add_alias an_alias, self + else + add_to @external_aliases, an_alias + unmatched_alias_list = + @unmatched_alias_lists[an_alias.pretty_old_name] ||= [] + unmatched_alias_list.push an_alias + end + + an_alias + end + + ## + # Adds +attribute+ if not already there. If it is (as method(s) or attribute), + # updates the comment if it was empty. + # + # The attribute is registered only if it defines a new method. + # For instance, <tt>attr_reader :foo</tt> will not be registered + # if method +foo+ exists, but <tt>attr_accessor :foo</tt> will be registered + # if method +foo+ exists, but <tt>foo=</tt> does not. + + def add_attribute attribute + return attribute unless @document_self + + # mainly to check for redefinition of an attribute as a method + # TODO find a policy for 'attr_reader :foo' + 'def foo=()' + register = false + + key = nil + + if attribute.rw.index 'R' then + key = attribute.pretty_name + known = @methods_hash[key] + + if known then + known.comment = attribute.comment if known.comment.empty? + elsif registered = @methods_hash[attribute.pretty_name + '='] and + RDoc::Attr === registered then + registered.rw = 'RW' + else + @methods_hash[key] = attribute + register = true + end + end + + if attribute.rw.index 'W' then + key = attribute.pretty_name + '=' + known = @methods_hash[key] + + if known then + known.comment = attribute.comment if known.comment.empty? + elsif registered = @methods_hash[attribute.pretty_name] and + RDoc::Attr === registered then + registered.rw = 'RW' + else + @methods_hash[key] = attribute + register = true + end + end + + if register then + attribute.visibility = @visibility + add_to @attributes, attribute + resolve_aliases attribute + end + + attribute + end + + ## + # Adds a class named +given_name+ with +superclass+. + # + # Both +given_name+ and +superclass+ may contain '::', and are + # interpreted relative to the +self+ context. This allows handling correctly + # examples like these: + # class RDoc::Gauntlet < Gauntlet + # module Mod + # class Object # implies < ::Object + # class SubObject < Object # this is _not_ ::Object + # + # Given <tt>class Container::Item</tt> RDoc assumes +Container+ is a module + # unless it later sees <tt>class Container</tt>. +add_class+ automatically + # upgrades +given_name+ to a class in this case. + + def add_class class_type, given_name, superclass = '::Object' + # superclass +nil+ is passed by the C parser in the following cases: + # - registering Object in 1.8 (correct) + # - registering BasicObject in 1.9 (correct) + # - registering RubyVM in 1.9 in iseq.c (incorrect: < Object in vm.c) + # + # If we later find a superclass for a registered class with a nil + # superclass, we must honor it. + + # find the name & enclosing context + if given_name =~ /^:+(\w+)$/ then + full_name = $1 + enclosing = top_level + name = full_name.split(/:+/).last + else + full_name = child_name given_name + + if full_name =~ /^(.+)::(\w+)$/ then + name = $2 + ename = $1 + enclosing = @store.classes_hash[ename] || @store.modules_hash[ename] + # HACK: crashes in actionpack/lib/action_view/helpers/form_helper.rb (metaprogramming) + unless enclosing then + # try the given name at top level (will work for the above example) + enclosing = @store.classes_hash[given_name] || + @store.modules_hash[given_name] + return enclosing if enclosing + # not found: create the parent(s) + names = ename.split('::') + enclosing = self + names.each do |n| + enclosing = enclosing.classes_hash[n] || + enclosing.modules_hash[n] || + enclosing.add_module(RDoc::NormalModule, n) + end + end + else + name = full_name + enclosing = self + end + end + + # fix up superclass + if full_name == 'BasicObject' then + superclass = nil + elsif full_name == 'Object' then + superclass = '::BasicObject' + end + + # find the superclass full name + if superclass then + if superclass =~ /^:+/ then + superclass = $' #' + else + if superclass =~ /^(\w+):+(.+)$/ then + suffix = $2 + mod = find_module_named($1) + superclass = mod.full_name + '::' + suffix if mod + else + mod = find_module_named(superclass) + superclass = mod.full_name if mod + end + end + + # did we believe it was a module? + mod = @store.modules_hash.delete superclass + + upgrade_to_class mod, RDoc::NormalClass, mod.parent if mod + + # e.g., Object < Object + superclass = nil if superclass == full_name + end + + klass = @store.classes_hash[full_name] + + if klass then + # if TopLevel, it may not be registered in the classes: + enclosing.classes_hash[name] = klass + + # update the superclass if needed + if superclass then + existing = klass.superclass + existing = existing.full_name unless existing.is_a?(String) if existing + if existing.nil? || + (existing == 'Object' && superclass != 'Object') then + klass.superclass = superclass + end + end + else + # this is a new class + mod = @store.modules_hash.delete full_name + + if mod then + klass = upgrade_to_class mod, RDoc::NormalClass, enclosing + + klass.superclass = superclass unless superclass.nil? + else + klass = class_type.new name, superclass + + enclosing.add_class_or_module(klass, enclosing.classes_hash, + @store.classes_hash) + end + end + + klass.parent = self + + klass + end + + ## + # Adds the class or module +mod+ to the modules or + # classes Hash +self_hash+, and to +all_hash+ (either + # <tt>TopLevel::modules_hash</tt> or <tt>TopLevel::classes_hash</tt>), + # unless #done_documenting is +true+. Sets the #parent of +mod+ + # to +self+, and its #section to #current_section. Returns +mod+. + + def add_class_or_module mod, self_hash, all_hash + mod.section = current_section # TODO declaring context? something is + # wrong here... + mod.parent = self + mod.full_name = nil + mod.store = @store + + unless @done_documenting then + self_hash[mod.name] = mod + # this must be done AFTER adding mod to its parent, so that the full + # name is correct: + all_hash[mod.full_name] = mod + if @store.unmatched_constant_alias[mod.full_name] then + to, file = @store.unmatched_constant_alias[mod.full_name] + add_module_alias mod, mod.name, to, file + end + end + + mod + end + + ## + # Adds +constant+ if not already there. If it is, updates the comment, + # value and/or is_alias_for of the known constant if they were empty/nil. + + def add_constant constant + return constant unless @document_self + + # HACK: avoid duplicate 'PI' & 'E' in math.c (1.8.7 source code) + # (this is a #ifdef: should be handled by the C parser) + known = @constants_hash[constant.name] + + if known then + known.comment = constant.comment if known.comment.empty? + + known.value = constant.value if + known.value.nil? or known.value.strip.empty? + + known.is_alias_for ||= constant.is_alias_for + else + @constants_hash[constant.name] = constant + add_to @constants, constant + end + + constant + end + + ## + # Adds included module +include+ which should be an RDoc::Include + + def add_include include + add_to @includes, include + + include + end + + ## + # Adds extension module +ext+ which should be an RDoc::Extend + + def add_extend ext + add_to @extends, ext + + ext + end + + ## + # Adds +method+ if not already there. If it is (as method or attribute), + # updates the comment if it was empty. + + def add_method method + return method unless @document_self + + # HACK: avoid duplicate 'new' in io.c & struct.c (1.8.7 source code) + key = method.pretty_name + known = @methods_hash[key] + + if known then + if @store then # otherwise we are loading + known.comment = method.comment if known.comment.empty? + previously = ", previously in #{known.file}" unless + method.file == known.file + @store.rdoc.options.warn \ + "Duplicate method #{known.full_name} in #{method.file}#{previously}" + end + else + @methods_hash[key] = method + if @current_line_visibility + method.visibility, @current_line_visibility = @current_line_visibility, nil + else + method.visibility = @visibility + end + add_to @method_list, method + resolve_aliases method + end + + method + end + + ## + # Adds a module named +name+. If RDoc already knows +name+ is a class then + # that class is returned instead. See also #add_class. + + def add_module(class_type, name) + mod = @classes[name] || @modules[name] + return mod if mod + + full_name = child_name name + mod = @store.modules_hash[full_name] || class_type.new(name) + + add_class_or_module mod, @modules, @store.modules_hash + end + + ## + # Adds a module by +RDoc::NormalModule+ instance. See also #add_module. + + def add_module_by_normal_module(mod) + add_class_or_module mod, @modules, @store.modules_hash + end + + ## + # Adds an alias from +from+ (a class or module) to +name+ which was defined + # in +file+. + + def add_module_alias from, from_name, to, file + return from if @done_documenting + + to_full_name = child_name to.name + + # if we already know this name, don't register an alias: + # see the metaprogramming in lib/active_support/basic_object.rb, + # where we already know BasicObject is a class when we find + # BasicObject = BlankSlate + return from if @store.find_class_or_module to_full_name + + unless from + @store.unmatched_constant_alias[child_name(from_name)] = [to, file] + return to + end + + new_to = from.dup + new_to.name = to.name + new_to.full_name = nil + + if new_to.module? then + @store.modules_hash[to_full_name] = new_to + @modules[to.name] = new_to + else + @store.classes_hash[to_full_name] = new_to + @classes[to.name] = new_to + end + + # Registers a constant for this alias. The constant value and comment + # will be updated later, when the Ruby parser adds the constant + const = RDoc::Constant.new to.name, nil, new_to.comment + const.record_location file + const.is_alias_for = from + add_constant const + + new_to + end + + ## + # Adds +require+ to this context's top level + + def add_require(require) + return require unless @document_self + + if RDoc::TopLevel === self then + add_to @requires, require + else + parent.add_require require + end + end + + ## + # Returns a section with +title+, creating it if it doesn't already exist. + # +comment+ will be appended to the section's comment. + # + # A section with a +title+ of +nil+ will return the default section. + # + # See also RDoc::Context::Section + + def add_section title, comment = nil + if section = @sections[title] then + section.add_comment comment if comment + else + section = Section.new self, title, comment + @sections[title] = section + end + + section + end + + ## + # Adds +thing+ to the collection +array+ + + def add_to array, thing + array << thing if @document_self + + thing.parent = self + thing.store = @store if @store + thing.section = current_section + end + + ## + # Is there any content? + # + # This means any of: comment, aliases, methods, attributes, external + # aliases, require, constant. + # + # Includes and extends are also checked unless <tt>includes == false</tt>. + + def any_content(includes = true) + @any_content ||= !( + @comment.empty? && + @method_list.empty? && + @attributes.empty? && + @aliases.empty? && + @external_aliases.empty? && + @requires.empty? && + @constants.empty? + ) + @any_content || (includes && !(@includes + @extends).empty? ) + end + + ## + # Creates the full name for a child with +name+ + + def child_name name + if name =~ /^:+/ + $' #' + elsif RDoc::TopLevel === self then + name + else + "#{self.full_name}::#{name}" + end + end + + ## + # Class attributes + + def class_attributes + @class_attributes ||= attributes.select { |a| a.singleton } + end + + ## + # Class methods + + def class_method_list + @class_method_list ||= method_list.select { |a| a.singleton } + end + + ## + # Array of classes in this context + + def classes + @classes.values + end + + ## + # All classes and modules in this namespace + + def classes_and_modules + classes + modules + end + + ## + # Hash of classes keyed by class name + + def classes_hash + @classes + end + + ## + # The current documentation section that new items will be added to. If + # temporary_section is available it will be used. + + def current_section + if section = @temporary_section then + @temporary_section = nil + else + section = @current_section + end + + section + end + + ## + # Is part of this thing was defined in +file+? + + def defined_in?(file) + @in_files.include?(file) + end + + def display(method_attr) # :nodoc: + if method_attr.is_a? RDoc::Attr + "#{method_attr.definition} #{method_attr.pretty_name}" + else + "method #{method_attr.pretty_name}" + end + end + + ## + # Iterator for ancestors for duck-typing. Does nothing. See + # RDoc::ClassModule#each_ancestor. + # + # This method exists to make it easy to work with Context subclasses that + # aren't part of RDoc. + + def each_ancestor(&_) # :nodoc: + end + + ## + # Iterator for attributes + + def each_attribute # :yields: attribute + @attributes.each { |a| yield a } + end + + ## + # Iterator for classes and modules + + def each_classmodule(&block) # :yields: module + classes_and_modules.sort.each(&block) + end + + ## + # Iterator for constants + + def each_constant # :yields: constant + @constants.each {|c| yield c} + end + + ## + # Iterator for included modules + + def each_include # :yields: include + @includes.each do |i| yield i end + end + + ## + # Iterator for extension modules + + def each_extend # :yields: extend + @extends.each do |e| yield e end + end + + ## + # Iterator for methods + + def each_method # :yields: method + return enum_for __method__ unless block_given? + + @method_list.sort.each { |m| yield m } + end + + ## + # Iterator for each section's contents sorted by title. The +section+, the + # section's +constants+ and the sections +attributes+ are yielded. The + # +constants+ and +attributes+ collections are sorted. + # + # To retrieve methods in a section use #methods_by_type with the optional + # +section+ parameter. + # + # NOTE: Do not edit collections yielded by this method + + def each_section # :yields: section, constants, attributes + return enum_for __method__ unless block_given? + + constants = @constants.group_by do |constant| constant.section end + attributes = @attributes.group_by do |attribute| attribute.section end + + constants.default = [] + attributes.default = [] + + sort_sections.each do |section| + yield section, constants[section].select(&:display?).sort, attributes[section].select(&:display?).sort + end + end + + ## + # Finds an attribute +name+ with singleton value +singleton+. + + def find_attribute(name, singleton) + name = $1 if name =~ /^(.*)=$/ + @attributes.find { |a| a.name == name && a.singleton == singleton } + end + + ## + # Finds an attribute with +name+ in this context + + def find_attribute_named(name) + case name + when /\A#/ then + find_attribute name[1..-1], false + when /\A::/ then + find_attribute name[2..-1], true + else + @attributes.find { |a| a.name == name } + end + end + + ## + # Finds a class method with +name+ in this context + + def find_class_method_named(name) + @method_list.find { |meth| meth.singleton && meth.name == name } + end + + ## + # Finds a constant with +name+ in this context + + def find_constant_named(name) + @constants.find do |m| + m.name == name || m.full_name == name + end + end + + ## + # Find a module at a higher scope + + def find_enclosing_module_named(name) + parent && parent.find_module_named(name) + end + + ## + # Finds an external alias +name+ with singleton value +singleton+. + + def find_external_alias(name, singleton) + @external_aliases.find { |m| m.name == name && m.singleton == singleton } + end + + ## + # Finds an external alias with +name+ in this context + + def find_external_alias_named(name) + case name + when /\A#/ then + find_external_alias name[1..-1], false + when /\A::/ then + find_external_alias name[2..-1], true + else + @external_aliases.find { |a| a.name == name } + end + end + + ## + # Finds a file with +name+ in this context + + def find_file_named name + @store.find_file_named name + end + + ## + # Finds an instance method with +name+ in this context + + def find_instance_method_named(name) + @method_list.find { |meth| !meth.singleton && meth.name == name } + end + + ## + # Finds a method, constant, attribute, external alias, module or file + # named +symbol+ in this context. + + def find_local_symbol(symbol) + find_method_named(symbol) or + find_constant_named(symbol) or + find_attribute_named(symbol) or + find_external_alias_named(symbol) or + find_module_named(symbol) or + find_file_named(symbol) + end + + ## + # Finds a method named +name+ with singleton value +singleton+. + + def find_method(name, singleton) + @method_list.find { |m| + if m.singleton + m.name == name && m.singleton == singleton + else + m.name == name && !m.singleton && !singleton + end + } + end + + ## + # Finds a instance or module method with +name+ in this context + + def find_method_named(name) + case name + when /\A#/ then + find_method name[1..-1], false + when /\A::/ then + find_method name[2..-1], true + else + @method_list.find { |meth| meth.name == name } + end + end + + ## + # Find a module with +name+ using ruby's scoping rules + + def find_module_named(name) + res = @modules[name] || @classes[name] + return res if res + return self if self.name == name + find_enclosing_module_named name + end + + ## + # Look up +symbol+, first as a module, then as a local symbol. + + def find_symbol(symbol) + find_symbol_module(symbol) || find_local_symbol(symbol) + end + + ## + # Look up a module named +symbol+. + + def find_symbol_module(symbol) + result = nil + + # look for a class or module 'symbol' + case symbol + when /^::/ then + result = @store.find_class_or_module symbol + when /^(\w+):+(.+)$/ + suffix = $2 + top = $1 + searched = self + while searched do + mod = searched.find_module_named(top) + break unless mod + result = @store.find_class_or_module "#{mod.full_name}::#{suffix}" + break if result || searched.is_a?(RDoc::TopLevel) + searched = searched.parent + end + else + searched = self + while searched do + result = searched.find_module_named(symbol) + break if result || searched.is_a?(RDoc::TopLevel) + searched = searched.parent + end + end + + result + end + + ## + # The full name for this context. This method is overridden by subclasses. + + def full_name + '(unknown)' + end + + ## + # Does this context and its methods and constants all have documentation? + # + # (Yes, fully documented doesn't mean everything.) + + def fully_documented? + documented? and + attributes.all? { |a| a.documented? } and + method_list.all? { |m| m.documented? } and + constants.all? { |c| c.documented? } + end + + ## + # URL for this with a +prefix+ + + def http_url(prefix) + path = name_for_path + path = path.gsub(/<<\s*(\w*)/, 'from-\1') if path =~ /<</ + path = [prefix] + path.split('::') + + File.join(*path.compact) + '.html' + end + + ## + # Instance attributes + + def instance_attributes + @instance_attributes ||= attributes.reject { |a| a.singleton } + end + + ## + # Instance methods + + def instance_methods + @instance_methods ||= method_list.reject { |a| a.singleton } + end + + ## + # Instance methods + #-- + # TODO remove this later + + def instance_method_list + warn '#instance_method_list is obsoleted, please use #instance_methods' + @instance_methods ||= method_list.reject { |a| a.singleton } + end + + ## + # Breaks method_list into a nested hash by type (<tt>'class'</tt> or + # <tt>'instance'</tt>) and visibility (+:public+, +:protected+, +:private+). + # + # If +section+ is provided only methods in that RDoc::Context::Section will + # be returned. + + def methods_by_type section = nil + methods = {} + + TYPES.each do |type| + visibilities = {} + RDoc::VISIBILITIES.each do |vis| + visibilities[vis] = [] + end + + methods[type] = visibilities + end + + each_method do |method| + next if section and not method.section == section + methods[method.type][method.visibility] << method + end + + methods + end + + ## + # Yields AnyMethod and Attr entries matching the list of names in +methods+. + + def methods_matching(methods, singleton = false, &block) + (@method_list + @attributes).each do |m| + yield m if methods.include?(m.name) and m.singleton == singleton + end + + each_ancestor do |parent| + parent.methods_matching(methods, singleton, &block) + end + end + + ## + # Array of modules in this context + + def modules + @modules.values + end + + ## + # Hash of modules keyed by module name + + def modules_hash + @modules + end + + ## + # Name to use to generate the url. + # <tt>#full_name</tt> by default. + + def name_for_path + full_name + end + + ## + # Changes the visibility for new methods to +visibility+ + + def ongoing_visibility=(visibility) + @visibility = visibility + end + + ## + # Record +top_level+ as a file +self+ is in. + + def record_location(top_level) + @in_files << top_level unless @in_files.include?(top_level) + end + + ## + # Should we remove this context from the documentation? + # + # The answer is yes if: + # * #received_nodoc is +true+ + # * #any_content is +false+ (not counting includes) + # * All #includes are modules (not a string), and their module has + # <tt>#remove_from_documentation? == true</tt> + # * All classes and modules have <tt>#remove_from_documentation? == true</tt> + + def remove_from_documentation? + @remove_from_documentation ||= + @received_nodoc && + !any_content(false) && + @includes.all? { |i| !i.module.is_a?(String) && i.module.remove_from_documentation? } && + classes_and_modules.all? { |cm| cm.remove_from_documentation? } + end + + ## + # Removes methods and attributes with a visibility less than +min_visibility+. + #-- + # TODO mark the visibility of attributes in the template (if not public?) + + def remove_invisible min_visibility + return if [:private, :nodoc].include? min_visibility + remove_invisible_in @method_list, min_visibility + remove_invisible_in @attributes, min_visibility + remove_invisible_in @constants, min_visibility + end + + ## + # Only called when min_visibility == :public or :private + + def remove_invisible_in array, min_visibility # :nodoc: + if min_visibility == :public then + array.reject! { |e| + e.visibility != :public and not e.force_documentation + } + else + array.reject! { |e| + e.visibility == :private and not e.force_documentation + } + end + end + + ## + # Tries to resolve unmatched aliases when a method or attribute has just + # been added. + + def resolve_aliases added + # resolve any pending unmatched aliases + key = added.pretty_name + unmatched_alias_list = @unmatched_alias_lists[key] + return unless unmatched_alias_list + unmatched_alias_list.each do |unmatched_alias| + added.add_alias unmatched_alias, self + @external_aliases.delete unmatched_alias + end + @unmatched_alias_lists.delete key + end + + ## + # Returns RDoc::Context::Section objects referenced in this context for use + # in a table of contents. + + def section_contents + used_sections = {} + + each_method do |method| + next unless method.display? + + used_sections[method.section] = true + end + + # order found sections + sections = sort_sections.select do |section| + used_sections[section] + end + + # only the default section is used + return [] if + sections.length == 1 and not sections.first.title + + sections + end + + ## + # Sections in this context + + def sections + @sections.values + end + + def sections_hash # :nodoc: + @sections + end + + ## + # Sets the current section to a section with +title+. See also #add_section + + def set_current_section title, comment + @current_section = add_section title, comment + end + + ## + # Given an array +methods+ of method names, set the visibility of each to + # +visibility+ + + def set_visibility_for(methods, visibility, singleton = false) + methods_matching methods, singleton do |m| + m.visibility = visibility + end + end + + ## + # Given an array +names+ of constants, set the visibility of each constant to + # +visibility+ + + def set_constant_visibility_for(names, visibility) + names.each do |name| + constant = @constants_hash[name] or next + constant.visibility = visibility + end + end + + ## + # Sorts sections alphabetically (default) or in TomDoc fashion (none, + # Public, Internal, Deprecated) + + def sort_sections + titles = @sections.map { |title, _| title } + + if titles.length > 1 and + TOMDOC_TITLES_SORT == + (titles | TOMDOC_TITLES).sort_by { |title| title.to_s } then + @sections.values_at(*TOMDOC_TITLES).compact + else + @sections.sort_by { |title, _| + title.to_s + }.map { |_, section| + section + } + end + end + + def to_s # :nodoc: + "#{self.class.name} #{self.full_name}" + end + + ## + # Return the TopLevel that owns us + #-- + # FIXME we can be 'owned' by several TopLevel (see #record_location & + # #in_files) + + def top_level + return @top_level if defined? @top_level + @top_level = self + @top_level = @top_level.parent until RDoc::TopLevel === @top_level + @top_level + end + + ## + # Upgrades NormalModule +mod+ in +enclosing+ to a +class_type+ + + def upgrade_to_class mod, class_type, enclosing + enclosing.modules_hash.delete mod.name + + klass = RDoc::ClassModule.from_module class_type, mod + klass.store = @store + + # if it was there, then we keep it even if done_documenting + @store.classes_hash[mod.full_name] = klass + enclosing.classes_hash[mod.name] = klass + + klass + end + + autoload :Section, "#{__dir__}/context/section" + +end diff --git a/lib/rdoc/code_object/context/section.rb b/lib/rdoc/code_object/context/section.rb new file mode 100644 index 0000000000..aecd4e0213 --- /dev/null +++ b/lib/rdoc/code_object/context/section.rb @@ -0,0 +1,233 @@ +# frozen_string_literal: true +require 'cgi/util' + +## +# A section of documentation like: +# +# # :section: The title +# # The body +# +# Sections can be referenced multiple times and will be collapsed into a +# single section. + +class RDoc::Context::Section + + include RDoc::Text + + MARSHAL_VERSION = 0 # :nodoc: + + ## + # Section comment + + attr_reader :comment + + ## + # Section comments + + attr_reader :comments + + ## + # Context this Section lives in + + attr_reader :parent + + ## + # Section title + + attr_reader :title + + ## + # Creates a new section with +title+ and +comment+ + + def initialize parent, title, comment + @parent = parent + @title = title ? title.strip : title + + @comments = [] + + add_comment comment + end + + ## + # Sections are equal when they have the same #title + + def == other + self.class === other and @title == other.title + end + + alias eql? == + + ## + # Adds +comment+ to this section + + def add_comment comment + comment = extract_comment comment + + return if comment.empty? + + case comment + when RDoc::Comment then + @comments << comment + when RDoc::Markup::Document then + @comments.concat comment.parts + when Array then + @comments.concat comment + else + raise TypeError, "unknown comment type: #{comment.inspect}" + end + end + + ## + # Anchor reference for linking to this section + + def aref + title = @title || '[untitled]' + + CGI.escape(title).gsub('%', '-').sub(/^-/, '') + end + + ## + # Extracts the comment for this section from the original comment block. + # If the first line contains :section:, strip it and use the rest. + # Otherwise remove lines up to the line containing :section:, and look + # for those lines again at the end and remove them. This lets us write + # + # # :section: The title + # # The body + + def extract_comment comment + case comment + when Array then + comment.map do |c| + extract_comment c + end + when nil + RDoc::Comment.new '' + when RDoc::Comment then + if comment.text =~ /^#[ \t]*:section:.*\n/ then + start = $` + rest = $' + + comment.text = if start.empty? then + rest + else + rest.sub(/#{start.chomp}\Z/, '') + end + end + + comment + when RDoc::Markup::Document then + comment + else + raise TypeError, "unknown comment #{comment.inspect}" + end + end + + def inspect # :nodoc: + "#<%s:0x%x %p>" % [self.class, object_id, title] + end + + def hash # :nodoc: + @title.hash + end + + ## + # The files comments in this section come from + + def in_files + return [] if @comments.empty? + + case @comments + when Array then + @comments.map do |comment| + comment.file + end + when RDoc::Markup::Document then + @comment.parts.map do |document| + document.file + end + else + raise RDoc::Error, "BUG: unknown comment class #{@comments.class}" + end + end + + ## + # Serializes this Section. The title and parsed comment are saved, but not + # the section parent which must be restored manually. + + def marshal_dump + [ + MARSHAL_VERSION, + @title, + parse, + ] + end + + ## + # De-serializes this Section. The section parent must be restored manually. + + def marshal_load array + @parent = nil + + @title = array[1] + @comments = array[2] + end + + ## + # Parses +comment_location+ into an RDoc::Markup::Document composed of + # multiple RDoc::Markup::Documents with their file set. + + def parse + case @comments + when String then + super + when Array then + docs = @comments.map do |comment, location| + doc = super comment + doc.file = location if location + doc + end + + RDoc::Markup::Document.new(*docs) + when RDoc::Comment then + doc = super @comments.text, comments.format + doc.file = @comments.location + doc + when RDoc::Markup::Document then + return @comments + else + raise ArgumentError, "unknown comment class #{comments.class}" + end + end + + ## + # The section's title, or 'Top Section' if the title is nil. + # + # This is used by the table of contents template so the name is silly. + + def plain_html + @title || 'Top Section' + end + + ## + # Removes a comment from this section if it is from the same file as + # +comment+ + + def remove_comment comment + return if @comments.empty? + + case @comments + when Array then + @comments.delete_if do |my_comment| + my_comment.file == comment.file + end + when RDoc::Markup::Document then + @comments.parts.delete_if do |document| + document.file == comment.file.name + end + else + raise RDoc::Error, "BUG: unknown comment class #{@comments.class}" + end + end + +end diff --git a/lib/rdoc/code_object/extend.rb b/lib/rdoc/code_object/extend.rb new file mode 100644 index 0000000000..7d57433de6 --- /dev/null +++ b/lib/rdoc/code_object/extend.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +## +# A Module extension to a class with \#extend +# +# RDoc::Extend.new 'Enumerable', 'comment ...' + +class RDoc::Extend < RDoc::Mixin + +end diff --git a/lib/rdoc/code_object/ghost_method.rb b/lib/rdoc/code_object/ghost_method.rb new file mode 100644 index 0000000000..25f951e35e --- /dev/null +++ b/lib/rdoc/code_object/ghost_method.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +## +# GhostMethod represents a method referenced only by a comment + +class RDoc::GhostMethod < RDoc::AnyMethod +end diff --git a/lib/rdoc/code_object/include.rb b/lib/rdoc/code_object/include.rb new file mode 100644 index 0000000000..c3e0d45e47 --- /dev/null +++ b/lib/rdoc/code_object/include.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +## +# A Module included in a class with \#include +# +# RDoc::Include.new 'Enumerable', 'comment ...' + +class RDoc::Include < RDoc::Mixin + +end diff --git a/lib/rdoc/code_object/meta_method.rb b/lib/rdoc/code_object/meta_method.rb new file mode 100644 index 0000000000..8c95a0f78c --- /dev/null +++ b/lib/rdoc/code_object/meta_method.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +## +# MetaMethod represents a meta-programmed method + +class RDoc::MetaMethod < RDoc::AnyMethod +end diff --git a/lib/rdoc/code_object/method_attr.rb b/lib/rdoc/code_object/method_attr.rb new file mode 100644 index 0000000000..61ddb32f46 --- /dev/null +++ b/lib/rdoc/code_object/method_attr.rb @@ -0,0 +1,418 @@ +# frozen_string_literal: true +## +# Abstract class representing either a method or an attribute. + +class RDoc::MethodAttr < RDoc::CodeObject + + include Comparable + + ## + # Name of this method/attribute. + + attr_accessor :name + + ## + # public, protected, private + + attr_accessor :visibility + + ## + # Is this a singleton method/attribute? + + attr_accessor :singleton + + ## + # Source file token stream + + attr_reader :text + + ## + # Array of other names for this method/attribute + + attr_reader :aliases + + ## + # The method/attribute we're aliasing + + attr_accessor :is_alias_for + + #-- + # The attributes below are for AnyMethod only. + # They are left here for the time being to + # allow ri to operate. + # TODO modify ri to avoid calling these on attributes. + #++ + + ## + # Parameters yielded by the called block + + attr_reader :block_params + + ## + # Parameters for this method + + attr_accessor :params + + ## + # Different ways to call this method + + attr_accessor :call_seq + + ## + # The call_seq or the param_seq with method name, if there is no call_seq. + + attr_reader :arglists + + ## + # Pretty parameter list for this method + + attr_reader :param_seq + + + ## + # Creates a new MethodAttr from token stream +text+ and method or attribute + # name +name+. + # + # Usually this is called by super from a subclass. + + def initialize text, name + super() + + @text = text + @name = name + + @aliases = [] + @is_alias_for = nil + @parent_name = nil + @singleton = nil + @visibility = :public + @see = false + + @arglists = nil + @block_params = nil + @call_seq = nil + @param_seq = nil + @params = nil + end + + ## + # Resets cached data for the object so it can be rebuilt by accessor methods + + def initialize_copy other # :nodoc: + @full_name = nil + end + + def initialize_visibility # :nodoc: + super + @see = nil + end + + ## + # Order by #singleton then #name + + def <=>(other) + return unless other.respond_to?(:singleton) && + other.respond_to?(:name) + + [ @singleton ? 0 : 1, name] <=> + [other.singleton ? 0 : 1, other.name] + end + + def == other # :nodoc: + equal?(other) or self.class == other.class and full_name == other.full_name + end + + ## + # A method/attribute is documented if any of the following is true: + # - it was marked with :nodoc:; + # - it has a comment; + # - it is an alias for a documented method; + # - it has a +#see+ method that is documented. + + def documented? + super or + (is_alias_for and is_alias_for.documented?) or + (see and see.documented?) + end + + ## + # A method/attribute to look at, + # in particular if this method/attribute has no documentation. + # + # It can be a method/attribute of the superclass or of an included module, + # including the Kernel module, which is always appended to the included + # modules. + # + # Returns +nil+ if there is no such method/attribute. + # The +#is_alias_for+ method/attribute, if any, is not included. + # + # Templates may generate a "see also ..." if this method/attribute + # has documentation, and "see ..." if it does not. + + def see + @see = find_see if @see == false + @see + end + + ## + # Sets the store for this class or module and its contained code objects. + + def store= store + super + + @file = @store.add_file @file.full_name if @file + end + + def find_see # :nodoc: + return nil if singleton || is_alias_for + + # look for the method + other = find_method_or_attribute name + return other if other + + # if it is a setter, look for a getter + return nil unless name =~ /[a-z_]=$/i # avoid == or === + return find_method_or_attribute name[0..-2] + end + + def find_method_or_attribute name # :nodoc: + return nil unless parent.respond_to? :ancestors + + searched = parent.ancestors + kernel = @store.modules_hash['Kernel'] + + searched << kernel if kernel && + parent != kernel && !searched.include?(kernel) + + searched.each do |ancestor| + next if String === ancestor + next if parent == ancestor + + other = ancestor.find_method_named('#' + name) || + ancestor.find_attribute_named(name) + + return other if other + end + + nil + end + + ## + # Abstract method. Contexts in their building phase call this + # to register a new alias for this known method/attribute. + # + # - creates a new AnyMethod/Attribute named <tt>an_alias.new_name</tt>; + # - adds +self+ as an alias for the new method or attribute + # - adds the method or attribute to #aliases + # - adds the method or attribute to +context+. + + def add_alias(an_alias, context) + raise NotImplementedError + end + + ## + # HTML fragment reference for this method + + def aref + type = singleton ? 'c' : 'i' + # % characters are not allowed in html names => dash instead + "#{aref_prefix}-#{type}-#{html_name}" + end + + ## + # Prefix for +aref+, defined by subclasses. + + def aref_prefix + raise NotImplementedError + end + + ## + # Attempts to sanitize the content passed by the Ruby parser: + # remove outer parentheses, etc. + + def block_params=(value) + # 'yield.to_s' or 'assert yield, msg' + return @block_params = '' if value =~ /^[\.,]/ + + # remove trailing 'if/unless ...' + return @block_params = '' if value =~ /^(if|unless)\s/ + + value = $1.strip if value =~ /^(.+)\s(if|unless)\s/ + + # outer parentheses + value = $1 if value =~ /^\s*\((.*)\)\s*$/ + value = value.strip + + # proc/lambda + return @block_params = $1 if value =~ /^(proc|lambda)(\s*\{|\sdo)/ + + # surrounding +...+ or [...] + value = $1.strip if value =~ /^\+(.*)\+$/ + value = $1.strip if value =~ /^\[(.*)\]$/ + + return @block_params = '' if value.empty? + + # global variable + return @block_params = 'str' if value =~ /^\$[&0-9]$/ + + # wipe out array/hash indices + value.gsub!(/(\w)\[[^\[]+\]/, '\1') + + # remove @ from class/instance variables + value.gsub!(/@@?([a-z0-9_]+)/, '\1') + + # method calls => method name + value.gsub!(/([A-Z:a-z0-9_]+)\.([a-z0-9_]+)(\s*\(\s*[a-z0-9_.,\s]*\s*\)\s*)?/) do + case $2 + when 'to_s' then $1 + when 'const_get' then 'const' + when 'new' then + $1.split('::').last. # ClassName => class_name + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + downcase + else + $2 + end + end + + # class prefixes + value.gsub!(/[A-Za-z0-9_:]+::/, '') + + # simple expressions + value = $1 if value =~ /^([a-z0-9_]+)\s*[-*+\/]/ + + @block_params = value.strip + end + + ## + # HTML id-friendly method/attribute name + + def html_name + require 'cgi/util' + + CGI.escape(@name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + end + + ## + # Full method/attribute name including namespace + + def full_name + @full_name ||= "#{parent_name}#{pretty_name}" + end + + def inspect # :nodoc: + alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil + visibility = self.visibility + visibility = "forced #{visibility}" if force_documentation + "#<%s:0x%x %s (%s)%s>" % [ + self.class, object_id, + full_name, + visibility, + alias_for, + ] + end + + ## + # '::' for a class method/attribute, '#' for an instance method. + + def name_prefix + @singleton ? '::' : '#' + end + + ## + # Name for output to HTML. For class methods the full name with a "." is + # used like +SomeClass.method_name+. For instance methods the class name is + # used if +context+ does not match the parent. + # + # This is to help prevent people from using :: to call class methods. + + def output_name context + return "#{name_prefix}#{@name}" if context == parent + + "#{parent_name}#{@singleton ? '.' : '#'}#{@name}" + end + + ## + # Method/attribute name with class/instance indicator + + def pretty_name + "#{name_prefix}#{@name}" + end + + ## + # Type of method/attribute (class or instance) + + def type + singleton ? 'class' : 'instance' + end + + ## + # Path to this method for use with HTML generator output. + + def path + "#{@parent.path}##{aref}" + end + + ## + # Name of our parent with special handling for un-marshaled methods + + def parent_name + @parent_name || super + end + + def pretty_print q # :nodoc: + alias_for = + if @is_alias_for.respond_to? :name then + "alias for #{@is_alias_for.name}" + elsif Array === @is_alias_for then + "alias for #{@is_alias_for.last}" + end + + q.group 2, "[#{self.class.name} #{full_name} #{visibility}", "]" do + if alias_for then + q.breakable + q.text alias_for + end + + if text then + q.breakable + q.text "text:" + q.breakable + q.pp @text + end + + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + ## + # Used by RDoc::Generator::JsonIndex to create a record for the search + # engine. + + def search_record + [ + @name, + full_name, + @name, + @parent.full_name, + path, + params, + snippet(@comment), + ] + end + + def to_s # :nodoc: + if @is_alias_for + "#{self.class.name}: #{full_name} -> #{is_alias_for}" + else + "#{self.class.name}: #{full_name}" + end + end + +end diff --git a/lib/rdoc/code_object/mixin.rb b/lib/rdoc/code_object/mixin.rb new file mode 100644 index 0000000000..fa8faefc15 --- /dev/null +++ b/lib/rdoc/code_object/mixin.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true +## +# A Mixin adds features from a module into another context. RDoc::Include and +# RDoc::Extend are both mixins. + +class RDoc::Mixin < RDoc::CodeObject + + ## + # Name of included module + + attr_accessor :name + + ## + # Creates a new Mixin for +name+ with +comment+ + + def initialize(name, comment) + super() + @name = name + self.comment = comment + @module = nil # cache for module if found + end + + ## + # Mixins are sorted by name + + def <=> other + return unless self.class === other + + name <=> other.name + end + + def == other # :nodoc: + self.class === other and @name == other.name + end + + alias eql? == # :nodoc: + + ## + # Full name based on #module + + def full_name + m = self.module + RDoc::ClassModule === m ? m.full_name : @name + end + + def hash # :nodoc: + [@name, self.module].hash + end + + def inspect # :nodoc: + "#<%s:0x%x %s.%s %s>" % [ + self.class, + object_id, + parent_name, self.class.name.downcase, @name, + ] + end + + ## + # Attempts to locate the included module object. Returns the name if not + # known. + # + # The scoping rules of Ruby to resolve the name of an included module are: + # - first look into the children of the current context; + # - if not found, look into the children of included modules, + # in reverse inclusion order; + # - if still not found, go up the hierarchy of names. + # + # This method has <code>O(n!)</code> behavior when the module calling + # include is referencing nonexistent modules. Avoid calling #module until + # after all the files are parsed. This behavior is due to ruby's constant + # lookup behavior. + # + # As of the beginning of October, 2011, no gem includes nonexistent modules. + + def module + return @module if @module + + # search the current context + return @name unless parent + full_name = parent.child_name(@name) + @module = @store.modules_hash[full_name] + return @module if @module + return @name if @name =~ /^::/ + + # search the includes before this one, in reverse order + searched = parent.includes.take_while { |i| i != self }.reverse + searched.each do |i| + inc = i.module + next if String === inc + full_name = inc.child_name(@name) + @module = @store.modules_hash[full_name] + return @module if @module + end + + # go up the hierarchy of names + up = parent.parent + while up + full_name = up.child_name(@name) + @module = @store.modules_hash[full_name] + return @module if @module + up = up.parent + end + + @name + end + + ## + # Sets the store for this class or module and its contained code objects. + + def store= store + super + + @file = @store.add_file @file.full_name if @file + end + + def to_s # :nodoc: + "#{self.class.name.downcase} #@name in: #{parent}" + end + +end diff --git a/lib/rdoc/code_object/normal_class.rb b/lib/rdoc/code_object/normal_class.rb new file mode 100644 index 0000000000..aa340b5d15 --- /dev/null +++ b/lib/rdoc/code_object/normal_class.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +## +# A normal class, neither singleton nor anonymous + +class RDoc::NormalClass < RDoc::ClassModule + + ## + # The ancestors of this class including modules. Unlike Module#ancestors, + # this class is not included in the result. The result will contain both + # RDoc::ClassModules and Strings. + + def ancestors + if String === superclass then + super << superclass + elsif superclass then + ancestors = super + ancestors << superclass + ancestors.concat superclass.ancestors + else + super + end + end + + def aref_prefix # :nodoc: + 'class' + end + + ## + # The definition of this class, <tt>class MyClassName</tt> + + def definition + "class #{full_name}" + end + + def direct_ancestors + superclass ? super + [superclass] : super + end + + def inspect # :nodoc: + superclass = @superclass ? " < #{@superclass}" : nil + "<%s:0x%x class %s%s includes: %p extends: %p attributes: %p methods: %p aliases: %p>" % [ + self.class, object_id, + full_name, superclass, @includes, @extends, @attributes, @method_list, @aliases + ] + end + + def to_s # :nodoc: + display = "#{self.class.name} #{self.full_name}" + if superclass + display += ' < ' + (superclass.is_a?(String) ? superclass : superclass.full_name) + end + display += ' -> ' + is_alias_for.to_s if is_alias_for + display + end + + def pretty_print q # :nodoc: + superclass = @superclass ? " < #{@superclass}" : nil + + q.group 2, "[class #{full_name}#{superclass}", "]" do + q.breakable + q.text "includes:" + q.breakable + q.seplist @includes do |inc| q.pp inc end + + q.breakable + q.text "constants:" + q.breakable + q.seplist @constants do |const| q.pp const end + + q.breakable + q.text "attributes:" + q.breakable + q.seplist @attributes do |attr| q.pp attr end + + q.breakable + q.text "methods:" + q.breakable + q.seplist @method_list do |meth| q.pp meth end + + q.breakable + q.text "aliases:" + q.breakable + q.seplist @aliases do |aliaz| q.pp aliaz end + + q.breakable + q.text "comment:" + q.breakable + q.pp comment + end + end + +end diff --git a/lib/rdoc/code_object/normal_module.rb b/lib/rdoc/code_object/normal_module.rb new file mode 100644 index 0000000000..498ec4dde2 --- /dev/null +++ b/lib/rdoc/code_object/normal_module.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true +## +# A normal module, like NormalClass + +class RDoc::NormalModule < RDoc::ClassModule + + def aref_prefix # :nodoc: + 'module' + end + + def inspect # :nodoc: + "#<%s:0x%x module %s includes: %p extends: %p attributes: %p methods: %p aliases: %p>" % [ + self.class, object_id, + full_name, @includes, @extends, @attributes, @method_list, @aliases + ] + end + + ## + # The definition of this module, <tt>module MyModuleName</tt> + + def definition + "module #{full_name}" + end + + ## + # This is a module, returns true + + def module? + true + end + + def pretty_print q # :nodoc: + q.group 2, "[module #{full_name}:", "]" do + q.breakable + q.text "includes:" + q.breakable + q.seplist @includes do |inc| q.pp inc end + q.breakable + + q.breakable + q.text "constants:" + q.breakable + q.seplist @constants do |const| q.pp const end + + q.text "attributes:" + q.breakable + q.seplist @attributes do |attr| q.pp attr end + q.breakable + + q.text "methods:" + q.breakable + q.seplist @method_list do |meth| q.pp meth end + q.breakable + + q.text "aliases:" + q.breakable + q.seplist @aliases do |aliaz| q.pp aliaz end + q.breakable + + q.text "comment:" + q.breakable + q.pp comment + end + end + + ## + # Modules don't have one, raises NoMethodError + + def superclass + raise NoMethodError, "#{full_name} is a module" + end + +end diff --git a/lib/rdoc/code_object/require.rb b/lib/rdoc/code_object/require.rb new file mode 100644 index 0000000000..05e26b84b0 --- /dev/null +++ b/lib/rdoc/code_object/require.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +## +# A file loaded by \#require + +class RDoc::Require < RDoc::CodeObject + + ## + # Name of the required file + + attr_accessor :name + + ## + # Creates a new Require that loads +name+ with +comment+ + + def initialize(name, comment) + super() + @name = name.gsub(/'|"/, "") #' + @top_level = nil + self.comment = comment + end + + def inspect # :nodoc: + "#<%s:0x%x require '%s' in %s>" % [ + self.class, + object_id, + @name, + parent_file_name, + ] + end + + def to_s # :nodoc: + "require #{name} in: #{parent}" + end + + ## + # The RDoc::TopLevel corresponding to this require, or +nil+ if not found. + + def top_level + @top_level ||= begin + tl = RDoc::TopLevel.all_files_hash[name + '.rb'] + + if tl.nil? and RDoc::TopLevel.all_files.first.full_name =~ %r(^lib/) then + # second chance + tl = RDoc::TopLevel.all_files_hash['lib/' + name + '.rb'] + end + + tl + end + end + +end diff --git a/lib/rdoc/code_object/single_class.rb b/lib/rdoc/code_object/single_class.rb new file mode 100644 index 0000000000..dd16529648 --- /dev/null +++ b/lib/rdoc/code_object/single_class.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +## +# A singleton class + +class RDoc::SingleClass < RDoc::ClassModule + + ## + # Adds the superclass to the included modules. + + def ancestors + superclass ? super + [superclass] : super + end + + def aref_prefix # :nodoc: + 'sclass' + end + + ## + # The definition of this singleton class, <tt>class << MyClassName</tt> + + def definition + "class << #{full_name}" + end + + def pretty_print q # :nodoc: + q.group 2, "[class << #{full_name}", "]" do + next + end + end +end diff --git a/lib/rdoc/code_object/top_level.rb b/lib/rdoc/code_object/top_level.rb new file mode 100644 index 0000000000..3864f66431 --- /dev/null +++ b/lib/rdoc/code_object/top_level.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: true +## +# A TopLevel context is a representation of the contents of a single file + +class RDoc::TopLevel < RDoc::Context + + MARSHAL_VERSION = 0 # :nodoc: + + ## + # This TopLevel's File::Stat struct + + attr_accessor :file_stat + + ## + # Relative name of this file + + attr_accessor :relative_name + + ## + # Absolute name of this file + + attr_accessor :absolute_name + + ## + # All the classes or modules that were declared in + # this file. These are assigned to either +#classes_hash+ + # or +#modules_hash+ once we know what they really are. + + attr_reader :classes_or_modules + + attr_accessor :diagram # :nodoc: + + ## + # The parser class that processed this file + + attr_reader :parser + + ## + # Creates a new TopLevel for the file at +absolute_name+. If documentation + # is being generated outside the source dir +relative_name+ is relative to + # the source directory. + + def initialize absolute_name, relative_name = absolute_name + super() + @name = nil + @absolute_name = absolute_name + @relative_name = relative_name + @file_stat = File.stat(absolute_name) rescue nil # HACK for testing + @diagram = nil + @parser = nil + + @classes_or_modules = [] + end + + ## + # Sets the parser for this toplevel context, also the store. + + def parser=(val) + @parser = val + @store.update_parser_of_file(absolute_name, val) if @store + @parser + end + + ## + # An RDoc::TopLevel is equal to another with the same relative_name + + def == other + self.class === other and @relative_name == other.relative_name + end + + alias eql? == + + ## + # Adds +an_alias+ to +Object+ instead of +self+. + + def add_alias(an_alias) + object_class.record_location self + return an_alias unless @document_self + object_class.add_alias an_alias + end + + ## + # Adds +constant+ to +Object+ instead of +self+. + + def add_constant constant + object_class.record_location self + return constant unless @document_self + object_class.add_constant constant + end + + ## + # Adds +include+ to +Object+ instead of +self+. + + def add_include(include) + object_class.record_location self + return include unless @document_self + object_class.add_include include + end + + ## + # Adds +method+ to +Object+ instead of +self+. + + def add_method(method) + object_class.record_location self + return method unless @document_self + object_class.add_method method + end + + ## + # Adds class or module +mod+. Used in the building phase + # by the Ruby parser. + + def add_to_classes_or_modules mod + @classes_or_modules << mod + end + + ## + # Base name of this file + + def base_name + File.basename @relative_name + end + + alias name base_name + + ## + # Only a TopLevel that contains text file) will be displayed. See also + # RDoc::CodeObject#display? + + def display? + text? and super + end + + ## + # See RDoc::TopLevel::find_class_or_module + #-- + # TODO Why do we search through all classes/modules found, not just the + # ones of this instance? + + def find_class_or_module name + @store.find_class_or_module name + end + + ## + # Finds a class or module named +symbol+ + + def find_local_symbol(symbol) + find_class_or_module(symbol) || super + end + + ## + # Finds a module or class with +name+ + + def find_module_named(name) + find_class_or_module(name) + end + + ## + # Returns the relative name of this file + + def full_name + @relative_name + end + + ## + # An RDoc::TopLevel has the same hash as another with the same + # relative_name + + def hash + @relative_name.hash + end + + ## + # URL for this with a +prefix+ + + def http_url(prefix) + path = [prefix, @relative_name.tr('.', '_')] + + File.join(*path.compact) + '.html' + end + + def inspect # :nodoc: + "#<%s:0x%x %p modules: %p classes: %p>" % [ + self.class, object_id, + base_name, + @modules.map { |n,m| m }, + @classes.map { |n,c| c } + ] + end + + ## + # Time this file was last modified, if known + + def last_modified + @file_stat ? file_stat.mtime : nil + end + + ## + # Dumps this TopLevel for use by ri. See also #marshal_load + + def marshal_dump + [ + MARSHAL_VERSION, + @relative_name, + @parser, + parse(@comment), + ] + end + + ## + # Loads this TopLevel from +array+. + + def marshal_load array # :nodoc: + initialize array[1] + + @parser = array[2] + @comment = array[3] + + @file_stat = nil + end + + ## + # Returns the NormalClass "Object", creating it if not found. + # + # Records +self+ as a location in "Object". + + def object_class + @object_class ||= begin + oc = @store.find_class_named('Object') || add_class(RDoc::NormalClass, 'Object') + oc.record_location self + oc + end + end + + ## + # Base name of this file without the extension + + def page_name + basename = File.basename @relative_name + basename =~ /\.(rb|rdoc|txt|md)$/i + + $` || basename + end + + ## + # Path to this file for use with HTML generator output. + + def path + http_url @store.rdoc.generator.file_dir + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class}: ", "]" do + q.text "base name: #{base_name.inspect}" + q.breakable + + items = @modules.map { |n,m| m } + items.concat @modules.map { |n,c| c } + q.seplist items do |mod| q.pp mod end + end + end + + ## + # Search record used by RDoc::Generator::JsonIndex + + def search_record + return unless @parser < RDoc::Parser::Text + + [ + page_name, + '', + page_name, + '', + path, + '', + snippet(@comment), + ] + end + + ## + # Is this TopLevel from a text file instead of a source code file? + + def text? + @parser and @parser.include? RDoc::Parser::Text + end + + def to_s # :nodoc: + "file #{full_name}" + end + +end |