diff options
Diffstat (limited to 'lib/rdoc/context.rb')
-rw-r--r-- | lib/rdoc/context.rb | 755 |
1 files changed, 755 insertions, 0 deletions
diff --git a/lib/rdoc/context.rb b/lib/rdoc/context.rb new file mode 100644 index 0000000000..4cf0c1914f --- /dev/null +++ b/lib/rdoc/context.rb @@ -0,0 +1,755 @@ +require 'rdoc/code_object' + +## +# 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] + + ## + # Method visibilities + + VISIBILITIES = [:public, :protected, :private] + + ## + # Aliased methods + + attr_reader :aliases + + ## + # attr* methods + + attr_reader :attributes + + ## + # Constants defined + + attr_reader :constants + + ## + # Current section of documentation + + attr_reader :current_section + + ## + # Files this context is found in + + attr_reader :in_files + + ## + # Modules this context includes + + attr_reader :includes + + ## + # 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 + + ## + # Sections in this context + + attr_reader :sections + + ## + # Aliases that haven't been resolved to a method + + attr_accessor :unmatched_alias_lists + + ## + # Current visibility of this context + + attr_reader :visibility + + ## + # A per-comment section of documentation like: + # + # # :SECTION: The title + # # The body + + class Section + + ## + # Section comment + + attr_reader :comment + + ## + # Context this Section lives in + + attr_reader :parent + + ## + # Section sequence number (for linking) + + attr_reader :sequence + + ## + # Section title + + attr_reader :title + + @@sequence = "SEC00000" + + ## + # Creates a new section with +title+ and +comment+ + + def initialize(parent, title, comment) + @parent = parent + @title = title + + @@sequence.succ! + @sequence = @@sequence.dup + + set_comment comment + end + + ## + # Sections are equal when they have the same #sequence + + def ==(other) + self.class === other and @sequence == other.sequence + end + + def inspect # :nodoc: + "#<%s:0x%x %s %p>" % [ + self.class, object_id, + @sequence, title + ] + end + + ## + # Set 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 + # + # # blah blah blah + # # + # # :SECTION: The title + # # The body + + def set_comment(comment) + return unless comment + + if comment =~ /^#[ \t]*:section:.*\n/ then + start = $` + rest = $' + + if start.empty? + @comment = rest + else + @comment = rest.sub(/#{start.chomp}\Z/, '') + end + else + @comment = comment + end + + @comment = nil if @comment.empty? + end + + end + + ## + # Creates an unnamed empty context with public visibility + + def initialize + super + + @in_files = [] + + @name ||= "unknown" + @comment ||= "" + @parent = nil + @visibility = :public + + @current_section = Section.new self, nil, nil + @sections = [@current_section] + + initialize_methods_etc + initialize_classes_and_modules + end + + ## + # Sets the defaults for classes and modules + + def initialize_classes_and_modules + @classes = {} + @modules = {} + end + + ## + # Sets the defaults for methods and so-forth + + def initialize_methods_etc + @method_list = [] + @attributes = [] + @aliases = [] + @requires = [] + @includes = [] + @constants = [] + + # This Hash maps a method name to a list of unmatched aliases (aliases of + # a method not yet encountered). + @unmatched_alias_lists = {} + end + + ## + # Contexts are sorted by full_name + + def <=>(other) + full_name <=> other.full_name + end + + ## + # Adds +an_alias+ that is automatically resolved + + def add_alias(an_alias) + meth = find_instance_method_named(an_alias.old_name) + + if meth then + add_alias_impl an_alias, meth + else + add_to @aliases, an_alias + unmatched_alias_list = @unmatched_alias_lists[an_alias.old_name] ||= [] + unmatched_alias_list.push an_alias + end + + an_alias + end + + ## + # Turns +an_alias+ into an AnyMethod that points to +meth+ + + def add_alias_impl(an_alias, meth) + new_meth = RDoc::AnyMethod.new an_alias.text, an_alias.new_name + new_meth.is_alias_for = meth + new_meth.singleton = meth.singleton + new_meth.params = meth.params + + new_meth.comment = an_alias.comment + + meth.add_alias new_meth + + add_method new_meth + + # aliases don't use ongoing visibility + new_meth.visibility = meth.visibility + + new_meth + end + + ## + # Adds +attribute+ + + def add_attribute(attribute) + add_to @attributes, attribute + end + + ## + # Adds a class named +name+ with +superclass+. + # + # Given <tt>class Container::Item</tt> RDoc assumes +Container+ is a module + # unless it later sees <tt>class Container</tt>. add_class automatically + # upgrades +name+ to a class in this case. + + def add_class(class_type, name, superclass = 'Object') + klass = add_class_or_module @classes, class_type, name, superclass + + existing = klass.superclass + existing = existing.name if existing and not String === existing + + if superclass != existing and superclass != 'Object' then + klass.superclass = superclass + end + + # If the parser encounters Container::Item before encountering + # Container, then it assumes that Container is a module. This may not + # be the case, so remove Container from the module list if present and + # transfer any contained classes and modules to the new class. + + mod = RDoc::TopLevel.modules_hash.delete klass.full_name + + if mod then + klass.classes_hash.update mod.classes_hash + klass.modules_hash.update mod.modules_hash + klass.method_list.concat mod.method_list + + @modules.delete klass.name + end + + RDoc::TopLevel.classes_hash[klass.full_name] = klass + + klass + end + + ## + # Instantiates a +class_type+ named +name+ and adds it the modules or + # classes Hash +collection+. + + def add_class_or_module(collection, class_type, name, superclass = nil) + full_name = child_name name + + mod = collection[name] + + if mod then + mod.superclass = superclass unless mod.module? + else + all = if class_type == RDoc::NormalModule then + RDoc::TopLevel.modules_hash + else + RDoc::TopLevel.classes_hash + end + + mod = all[full_name] + + unless mod then + mod = class_type.new name, superclass + else + # If the class has been encountered already, check that its + # superclass has been set (it may not have been, depending on the + # context in which it was encountered). + if class_type == RDoc::NormalClass then + mod.superclass = superclass unless mod.superclass + end + end + + unless @done_documenting then + all[full_name] = mod + collection[name] = mod + end + + mod.section = @current_section + mod.parent = self + end + + mod + end + + ## + # Adds +constant+ + + def add_constant(constant) + add_to @constants, constant + end + + ## + # Adds included module +include+ + + def add_include(include) + add_to @includes, include + end + + ## + # Adds +method+ + + def add_method(method) + method.visibility = @visibility + add_to @method_list, method + + unmatched_alias_list = @unmatched_alias_lists[method.name] + if unmatched_alias_list then + unmatched_alias_list.each do |unmatched_alias| + add_alias_impl unmatched_alias, method + @aliases.delete unmatched_alias + end + + @unmatched_alias_lists.delete method.name + end + 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) + return @classes[name] if @classes.key? name + + add_class_or_module @modules, class_type, name, nil + end + + ## + # Adds an alias from +from+ to +name+ + + def add_module_alias from, name + to_name = child_name name + + unless @done_documenting then + if from.module? then + RDoc::TopLevel.modules_hash + else + RDoc::TopLevel.classes_hash + end[to_name] = from + + if from.module? then + @modules + else + @classes + end[name] = from + end + + from + end + + ## + # Adds +require+ to this context's top level + + def add_require(require) + if RDoc::TopLevel === self then + add_to @requires, require + else + parent.add_require require + end + end + + ## + # Adds +thing+ to the collection +array+ + + def add_to(array, thing) + array << thing if @document_self and not @done_documenting + thing.parent = self + thing.section = @current_section + end + + ## + # Creates the full name for a child with +name+ + + def child_name name + if RDoc::TopLevel === self then + name + else + "#{self.full_name}::#{name}" + end + 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 + + ## + # Is part of this thing was defined in +file+? + + def defined_in?(file) + @in_files.include?(file) + 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 methods + + def each_method # :yields: method + @method_list.sort.each {|m| yield m} + end + + ## + # Finds an attribute with +name+ in this context + + def find_attribute_named(name) + @attributes.find { |m| m.name == name } + end + + ## + # Finds a constant with +name+ in this context + + def find_constant_named(name) + @constants.find {|m| m.name == name} + end + + ## + # Find a module at a higher scope + + def find_enclosing_module_named(name) + parent && parent.find_module_named(name) + end + + ## + # Finds a file with +name+ in this context + + def find_file_named(name) + top_level.class.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.name == name && !meth.singleton } + end + + ## + # Finds a method, constant, attribute, module or files 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_module_named(symbol) or + find_file_named(symbol) + end + + ## + # Finds a instance or module method with +name+ in this context + + def find_method_named(name) + @method_list.find { |meth| meth.name == name } + 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+. If +method+ is non-nil, then we assume the symbol + # references a module that contains that method. + + def find_symbol(symbol, method = nil) + result = nil + + case symbol + when /^::(.*)/ then + result = top_level.find_symbol($1) + when /::/ then + modules = symbol.split(/::/) + + unless modules.empty? then + module_name = modules.shift + result = find_module_named(module_name) + + if result then + modules.each do |name| + result = result.find_module_named name + break unless result + end + end + end + + else + # if a method is specified, then we're definitely looking for + # a module, otherwise it could be any symbol + if method then + result = find_module_named symbol + else + result = find_local_symbol symbol + if result.nil? then + if symbol =~ /^[A-Z]/ then + result = parent + while result && result.name != symbol do + result = result.parent + end + end + end + end + end + + if result and method then + fail unless result.respond_to? :find_local_symbol + result = result.find_local_symbol(method) + end + + result + end + + ## + # The full name for this context. This method is overridden by subclasses. + + def full_name + '(unknown)' + end + + ## + # URL for this with a +prefix+ + + def http_url(prefix) + path = full_name + path = path.gsub(/<<\s*(\w*)/, 'from-\1') if path =~ /<</ + path = [prefix] + path.split('::') + + File.join(*path.compact) + '.html' + end + + ## + # Breaks method_list into a nested hash by type (class or instance) and + # visibility (public, protected private) + + def methods_by_type + methods = {} + + TYPES.each do |type| + visibilities = {} + VISIBILITIES.each do |vis| + visibilities[vis] = [] + end + + methods[type] = visibilities + end + + each_method do |method| + methods[method.type][method.visibility] << method + end + + methods + end + + ## + # Yields Method and Attr entries matching the list of names in +methods+. + # Attributes are only returned when +singleton+ is false. + + def methods_matching(methods, singleton = false) + count = 0 + + @method_list.each do |m| + if methods.include? m.name and m.singleton == singleton then + yield m + count += 1 + end + end + + return if count == methods.size || singleton + + @attributes.each do |a| + yield a if methods.include? a.name + 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 + + ## + # Changes the visibility for new methods to +visibility+ + + def ongoing_visibility=(visibility) + @visibility = visibility + end + + ## + # Record which file +top_level+ is in + + def record_location(top_level) + @in_files << top_level unless @in_files.include?(top_level) + end + + ## + # If a class's documentation is turned off after we've started collecting + # methods etc., we need to remove the ones we have + + def remove_methods_etc + initialize_methods_etc + 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 + + ## + # Removes classes and modules when we see a :nodoc: all + + def remove_classes_and_modules + initialize_classes_and_modules + end + + ## + # Creates a new section with +title+ and +comment+ + + def set_current_section(title, comment) + @current_section = Section.new self, title, comment + @sections << @current_section + end + + ## + # Return the TopLevel that owns us + + 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 + +end + |