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 class Container::Item RDoc assumes +Container+ is a module
# unless it later sees class Container. 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