# We represent the various high-level code constructs that appear # in Ruby programs: classes, modules, methods, and so on. require 'rdoc/tokenstream' module RDoc # We contain the common stuff for contexts (which are containers) # and other elements (methods, attributes and so on) # class CodeObject attr_accessor :parent # We are the model of the code, but we know that at some point # we will be worked on by viewers. By implementing the Viewable # protocol, viewers can associated themselves with these objects. attr_accessor :viewer # are we done documenting (ie, did we come across a :enddoc:)? attr_accessor :done_documenting # Which section are we in attr_accessor :section # do we document ourselves? attr_reader :document_self def document_self=(val) @document_self = val if !val remove_methods_etc end end # set and cleared by :startdoc: and :enddoc:, this is used to toggle # the capturing of documentation def start_doc @document_self = true @document_children = true end def stop_doc @document_self = false @document_children = false end # do we document ourselves and our children attr_reader :document_children def document_children=(val) @document_children = val if !val remove_classes_and_modules end end # Do we _force_ documentation, even is we wouldn't normally show the entity attr_accessor :force_documentation # Default callbacks to nothing, but this is overridden for classes # and modules def remove_classes_and_modules end def remove_methods_etc end def initialize @document_self = true @document_children = true @force_documentation = false @done_documenting = false end # Access the code object's comment attr_reader :comment # Update the comment, but don't overwrite a real comment # with an empty one def comment=(comment) @comment = comment unless comment.empty? end # There's a wee trick we pull. Comment blocks can have directives that # override the stuff we extract during the parse. So, we have a special # class method, attr_overridable, that lets code objects list # those directives. Wehn a comment is assigned, we then extract # out any matching directives and update our object def CodeObject.attr_overridable(name, *aliases) @overridables ||= {} attr_accessor name aliases.unshift name aliases.each do |directive_name| @overridables[directive_name.to_s] = name end end end # A Context is something that can hold modules, classes, methods, # attributes, aliases, requires, and includes. Classes, modules, and # files are all Contexts. class Context < CodeObject attr_reader :name, :method_list, :attributes, :aliases, :constants attr_reader :requires, :includes, :in_files, :visibility attr_reader :sections class Section attr_reader :title, :comment, :sequence @@sequence = "SEC00000" def initialize(title, comment) @title = title @@sequence.succ! @sequence = @@sequence.dup set_comment(comment) end private # 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 # # # --------------------- # # :SECTION: The title # # The body # # --------------------- def set_comment(comment) return unless comment if comment =~ /^.*?:section:.*$/ 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 def initialize super() @in_files = [] @name ||= "unknown" @comment ||= "" @parent = nil @visibility = :public @current_section = Section.new(nil, nil) @sections = [ @current_section ] initialize_methods_etc initialize_classes_and_modules end # map the class hash to an array externally def classes @classes.values end # map the module hash to an array externally def modules @modules.values end # Change the default visibility for new methods def ongoing_visibility=(vis) @visibility = vis end # Given an array +methods+ of method names, set the # visibility of the corresponding AnyMethod object def set_visibility_for(methods, vis, singleton=false) count = 0 @method_list.each do |m| if methods.include?(m.name) && m.singleton == singleton m.visibility = vis count += 1 end end return if count == methods.size || singleton # perhaps we need to look at attributes @attributes.each do |a| if methods.include?(a.name) a.visibility = vis count += 1 end end end # Record the file that we happen to find it in def record_location(toplevel) @in_files << toplevel unless @in_files.include?(toplevel) end # Return true if at least part of this thing was defined in +file+ def defined_in?(file) @in_files.include?(file) end def add_class(class_type, name, superclass) add_class_or_module(@classes, class_type, name, superclass) end def add_module(class_type, name) add_class_or_module(@modules, class_type, name, nil) end def add_method(a_method) puts "Adding #@visibility method #{a_method.name} to #@name" if $DEBUG a_method.visibility = @visibility add_to(@method_list, a_method) end ######################################## # moved to parse_f95.rb # #!# #!# def add_method(a_method) #!# if !(a_method.visibility == :public) && #!# !(a_method.visibility == :private) && #!# !(a_method.visibility == :protected) #!# a_method.visibility = @visibility #!# end #!# puts "Adding #{a_method.visibility} method #{a_method.name} to #@name" if $DEBUG #!# add_to(@method_list, a_method) #!# end #!# # moved to parse_f95.rb # ######################################## def add_attribute(an_attribute) add_to(@attributes, an_attribute) end def add_alias(an_alias) meth = find_instance_method_named(an_alias.old_name) if meth new_meth = 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 = "Alias for \##{meth.name}" meth.add_alias(new_meth) add_method(new_meth) else add_to(@aliases, an_alias) end end ######################################## # moved to parse_f95.rb # #!# #!# def add_alias(an_alias, ignore_case=nil) #!# meth = find_instance_method_named(an_alias.old_name, ignore_case) #!# if meth #!# new_meth = 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 = "Alias for \##{meth.name}" #!# meth.add_alias(new_meth) #!# add_method(new_meth) #!# else #!# add_to(@aliases, an_alias) #!# end #!# end #!# # moved to parse_f95.rb # ######################################## def add_include(an_include) add_to(@includes, an_include) end def add_constant(const) add_to(@constants, const) end # Requires always get added to the top-level (file) context def add_require(a_require) if self.kind_of? TopLevel add_to(@requires, a_require) else parent.add_require(a_require) end end def add_class_or_module(collection, class_type, name, superclass=nil) cls = collection[name] if cls puts "Reusing class/module #{name}" if $DEBUG else cls = class_type.new(name, superclass) puts "Adding class/module #{name} to #@name" if $DEBUG # collection[name] = cls if @document_self && !@done_documenting collection[name] = cls if !@done_documenting cls.parent = self cls.section = @current_section end cls end def add_to(array, thing) array << thing if @document_self && !@done_documenting thing.parent = self thing.section = @current_section 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 def initialize_methods_etc @method_list = [] @attributes = [] @aliases = [] @requires = [] @includes = [] @constants = [] end # and remove classes and modules when we see a :nodoc: all def remove_classes_and_modules initialize_classes_and_modules end def initialize_classes_and_modules @classes = {} @modules = {} end # Find a named module def find_module_named(name) return self if self.name == name res = @modules[name] || @classes[name] return res if res find_enclosing_module_named(name) end # find a module at a higher scope def find_enclosing_module_named(name) parent && parent.find_module_named(name) end ######################################## # moved to parse_f95.rb # #!# #!# # Find a named module #!# def find_module_named(name, ignore_case=nil) #!# res = nil #!# if !ignore_case #!# return self if self.name == name #!# else #!# return self if self.name.upcase == name.upcase #!# end #!# if !ignore_case #!# res = @modules[name] || @classes[name] #!# else #!# @modules.each{ |n, v| #!# if n.upcase == name.upcase #!# res = v ; break #!# end #!# } #!# @classes.each{ |n, v| #!# if n.upcase == name.upcase #!# res = v ; break #!# end #!# } if !res #!# end #!# return res if res #!# find_enclosing_module_named(name, ignore_case) #!# end #!# #!# # find a module at a higher scope #!# def find_enclosing_module_named(name, ignore_case=nil) #!# parent && parent.find_module_named(name, ignore_case) #!# end #!# # moved to parse_f95.rb # ######################################## # Iterate over all the classes and modules in # this object def each_classmodule @modules.each_value {|m| yield m} @classes.each_value {|c| yield c} end def each_method @method_list.each {|m| yield m} end def each_attribute @attributes.each {|a| yield a} end def each_constant @constants.each {|c| yield c} end ######################################## # moved to parse_f95.rb # #!# #!# def each_includes #!# @includes.each {|i| yield i} #!# end #!# # moved to parse_f95.rb # ######################################## # Return the toplevel that owns us def toplevel return @toplevel if defined? @toplevel @toplevel = self @toplevel = @toplevel.parent until TopLevel === @toplevel @toplevel end # allow us to sort modules by name def <=>(other) name <=> other.name end # Look up the given 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 /^::(.*)/ result = toplevel.find_symbol($1) when /::/ modules = symbol.split(/::/) unless modules.empty? module_name = modules.shift result = find_module_named(module_name) if result modules.each do |module_name| result = result.find_module_named(module_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 result = find_module_named(symbol) else result = find_local_symbol(symbol) if result.nil? if symbol =~ /^[A-Z]/ result = parent while result && result.name != symbol result = result.parent end end end end end if result && method if !result.respond_to?(:find_local_symbol) p result.name p method fail end result = result.find_local_symbol(method) end result end def find_local_symbol(symbol) res = find_method_named(symbol) || find_constant_named(symbol) || find_attribute_named(symbol) || find_module_named(symbol) end ######################################## # moved to parse_f95.rb # #!# #!# # Look up the given filename. #!# def find_file(file, method=nil, ignore_case=nil) #!# find_file_named(file, method, ignore_case) #!# end #!# #!# # Look up the given symbol. If method is non-nil, then #!# # we assume the symbol references a module that #!# # contains that method #!# def find_symbol(symbol, method=nil, ignore_case=nil) #!# result = nil #!# case symbol #!# when /^::(.*)/ #!# result = toplevel.find_symbol($1, nil, ignore_case) #!# when /::/ #!# modules = symbol.split(/::/) #!# unless modules.empty? #!# module_name = modules.shift #!# result = find_module_named(module_name, ignore_case) #!# if result #!# modules.each do |module_name| #!# result = result.find_module_named(module_name, ignore_case) #!# 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 #!# result = find_module_named(symbol, ignore_case) #!# else #!# result = find_local_symbol(symbol, ignore_case) #!# if result.nil? #!# if symbol =~ /^[A-Z]/ || #!# symbol =~ /^[A-Za-z]/ && ignore_case #!# result = parent #!# while result && result.name != symbol #!# result = result.parent #!# end #!# end #!# end #!# end #!# end #!# if result && method #!# if !result.respond_to?(:find_local_symbol) #!# p result.name #!# p method #!# fail #!# end #!# result = result.find_local_symbol(method, ignore_case) #!# end #!# result #!# end #!# #!# def find_local_symbol(symbol, ignore_case=nil) #!# res = find_method_named(symbol, ignore_case) || #!# find_constant_named(symbol, ignore_case) || #!# find_attribute_named(symbol, ignore_case) || #!# find_module_named(symbol, ignore_case) #!# end #!# #!# def include_requires?(name, ignore_case=nil) #!# if self.kind_of? TopLevel #!# self.requires.each{|r| #!# if r.name == name || #!# r.name.upcase == name.upcase && ignore_case #!# return true #!# end #!# } #!# return false #!# else #!# parent.include_requires?(name) #!# end #!# end #!# #!# def include_includes?(name, ignore_case=nil) #!# self.includes.each{|i| #!# if i.name == name || #!# i.name.upcase == name.upcase && ignore_case #!# return true #!# end #!# } #!# return false #!# end #!# # moved to parse_f95.rb # ######################################## # Handle sections def set_current_section(title, comment) @current_section = Section.new(title, comment) @sections << @current_section end private # Find a named method, or return nil def find_method_named(name) @method_list.find {|meth| meth.name == name} end # Find a named instance method, or return nil def find_instance_method_named(name) @method_list.find {|meth| meth.name == name && !meth.singleton} end # Find a named constant, or return nil def find_constant_named(name) @constants.find {|m| m.name == name} end # Find a named attribute, or return nil def find_attribute_named(name) @attributes.find {|m| m.name == name} end ######################################## # moved to parse_f95.rb # #!# #!# # Find a named method, or return nil #!# def find_method_named(name, ignore_case=nil) #!# if !ignore_case #!# @method_list.find {|meth| meth.name == name} #!# else #!# @method_list.find {|meth| meth.name.upcase == name.upcase} #!# end #!# end #!# #!# # Find a named instance method, or return nil #!# def find_instance_method_named(name, ignore_case=nil) #!# if !ignore_case #!# @method_list.find {|meth| meth.name == name && !meth.singleton} #!# else #!# @method_list.find {|meth| #!# meth.name.upcase == name.upcase && !meth.singleton #!# } #!# end #!# end #!# #!# # Find a named constant, or return nil #!# def find_constant_named(name, ignore_case=nil) #!# if !ignore_case #!# @constants.find {|m| m.name == name} #!# else #!# @constants.find {|m| m.name.upcase == name.upcase} #!# end #!# end #!# #!# # Find a named attribute, or return nil #!# def find_attribute_named(name, ignore_case=nil) #!# if !ignore_case #!# @attributes.find {|m| m.name == name} #!# else #!# @attributes.find {|m| m.name.upcase == name.upcase} #!# end #!# end #!# # moved to parse_f95.rb # ######################################## end # A TopLevel context is a source file class TopLevel < Context attr_accessor :file_stat attr_accessor :file_relative_name attr_accessor :file_absolute_name attr_accessor :diagram @@all_classes = {} @@all_modules = {} #!# @@all_files = {} def TopLevel::reset @@all_classes = {} @@all_modules = {} #!# @@all_files = {} end def initialize(file_name) super() @name = "TopLevel" @file_relative_name = file_name @file_absolute_name = file_name @file_stat = File.stat(file_name) @diagram = nil #!# @@all_files[file_name] = self end def full_name nil end # Adding a class or module to a TopLevel is special, as we only # want one copy of a particular top-level class. For example, # if both file A and file B implement class C, we only want one # ClassModule object for C. This code arranges to share # classes and modules between files. def add_class_or_module(collection, class_type, name, superclass) cls = collection[name] if cls puts "Reusing class/module #{name}" if $DEBUG else if class_type == NormalModule all = @@all_modules else all = @@all_classes end cls = all[name] if !cls cls = class_type.new(name, superclass) all[name] = cls unless @done_documenting end puts "Adding class/module #{name} to #@name" if $DEBUG collection[name] = cls unless @done_documenting cls.parent = self end cls end def TopLevel.all_classes_and_modules @@all_classes.values + @@all_modules.values end ######################################## # moved to parse_f95.rb # #!# #!# def TopLevel.all_files #!# @@all_files #!# end #!# # moved to parse_f95.rb # ######################################## def TopLevel.find_class_named(name) @@all_classes.each_value do |c| res = c.find_class_named(name) return res if res end nil end def find_local_symbol(symbol) find_class_or_module_named(symbol) || super end def find_class_or_module_named(symbol) @@all_classes.each_value {|c| return c if c.name == symbol} @@all_modules.each_value {|m| return m if m.name == symbol} nil end # Find a named module def find_module_named(name) find_class_or_module_named(name) || find_enclosing_module_named(name) end ######################################## # moved to parse_f95.rb # #!# #!# def find_local_symbol(symbol, ignore_case=nil) #!# find_class_or_module_named(symbol, ignore_case) || super #!# end #!# #!# def find_class_or_module_named(symbol, ignore_case=nil) #!# if !ignore_case #!# @@all_classes.each_value {|c| return c if c.name == symbol} #!# @@all_modules.each_value {|m| return m if m.name == symbol} #!# else #!# @@all_classes.each_value {|c| return c if c.name.upcase == symbol.upcase} #!# @@all_modules.each_value {|m| return m if m.name.upcase == symbol.upcase} #!# end #!# nil #!# end #!# #!# # Find a named module #!# def find_module_named(name, ignore_case=nil) #!# find_class_or_module_named(name, ignore_case) || find_enclosing_module_named(name, ignore_case) #!# end #!# #!# # Find a named file #!# def find_file_named(name, method=nil, ignore_case=nil) #!# return nil unless name #!# result = nil #!# @@all_files.each{|file_name, toplevel| #!# result = toplevel if file_name == name #!# } #!# dir = File.dirname(@file_relative_name) #!# @@all_files.each{|file_name, toplevel| #!# if /^#{dir}\/(.*)/ =~ file_name #!# result = toplevel if $1 == name #!# end #!# } #!# if result #!# if method #!# result_method = result.find_local_symbol(method, ignore_case) #!# return result_method #!# else #!# return result #!# end #!# else #!# return nil #!# end #!# end #!# # moved to parse_f95.rb # ######################################## end # ClassModule is the base class for objects representing either a # class or a module. class ClassModule < Context attr_reader :superclass attr_accessor :diagram def initialize(name, superclass = nil) @name = name @diagram = nil @superclass = superclass @comment = "" super() end # Return the fully qualified name of this class or module def full_name if @parent && @parent.full_name @parent.full_name + "::" + @name else @name end end def http_url(prefix) path = full_name.split("::") File.join(prefix, *path) + ".html" end # Return +true+ if this object represents a module def is_module? false end # to_s is simply for debugging def to_s res = self.class.name + ": " + @name res << @comment.to_s res << super res end def find_class_named(name) return self if full_name == name @classes.each_value {|c| return c if c.find_class_named(name) } nil end ######################################## # moved to parse_f95.rb # #!# #!# def find_file_named(name, method=nil, ignore_case=nil) #!# parent.find_file_named(name, method, ignore_case) #!# end #!# # moved to parse_f95.rb # ######################################## end # Anonymous classes class AnonClass < ClassModule end # Normal classes class NormalClass < ClassModule end # Singleton classes class SingleClass < ClassModule end # Module class NormalModule < ClassModule def is_module? true end end # AnyMethod is the base class for objects representing methods class AnyMethod < CodeObject attr_accessor :name attr_accessor :visibility attr_accessor :block_params attr_accessor :dont_rename_initialize attr_accessor :singleton attr_reader :aliases # list of other names for this method attr_accessor :is_alias_for # or a method we're aliasing attr_overridable :params, :param, :parameters, :parameter attr_accessor :call_seq include TokenStream def initialize(text, name) super() @text = text @name = name @token_stream = nil @visibility = :public @dont_rename_initialize = false @block_params = nil @aliases = [] @is_alias_for = nil @comment = "" @call_seq = nil end def <=>(other) @name <=> other.name end ######################################## # moved to parse_f95.rb # #!# #!# def <=>(other) #!# t = @name <=> other.name #!# return t if t != 0 #!# t = @params <=> other.params #!# return t if t != 0 #!# t = @comment <=> other.comment #!# end #!# # moved to parse_f95.rb # ######################################## def to_s res = self.class.name + ": " + @name + " (" + @text + ")\n" res << @comment.to_s res end def param_seq p = params.gsub(/\s*\#.*/, '') p = p.tr("\n", " ").squeeze(" ") p = "(" + p + ")" unless p[0] == ?( if (block = block_params) # If this method has explicit block parameters, remove any # explicit &block $stderr.puts p p.sub!(/,?\s*&\w+/) $stderr.puts p block.gsub!(/\s*\#.*/, '') block = block.tr("\n", " ").squeeze(" ") if block[0] == ?( block.sub!(/^\(/, '').sub!(/\)/, '') end p << " {|#{block}| ...}" end p end def add_alias(method) @aliases << method end end # Represent an alias, which is an old_name/ new_name pair associated # with a particular context class Alias < CodeObject attr_accessor :text, :old_name, :new_name, :comment def initialize(text, old_name, new_name, comment) super() @text = text @old_name = old_name @new_name = new_name self.comment = comment end def to_s "alias: #{self.old_name} -> #{self.new_name}\n#{self.comment}" end end # Represent a constant class Constant < CodeObject attr_accessor :name, :value def initialize(name, value, comment) super() @name = name @value = value self.comment = comment end end # Represent attributes class Attr < CodeObject attr_accessor :text, :name, :rw, :visibility def initialize(text, name, rw, comment) super() @text = text @name = name @rw = rw @visibility = :public self.comment = comment end def to_s "attr: #{self.name} #{self.rw}\n#{self.comment}" end def <=>(other) self.name <=> other.name end end # a required file class Require < CodeObject attr_accessor :name def initialize(name, comment) super() @name = name.gsub(/'|"/, "") #' self.comment = comment end end # an included module class Include < CodeObject attr_accessor :name def initialize(name, comment) super() @name = name self.comment = comment end end end