ruby/lib/rdoc/ri/driver.rb

670 строки
18 KiB
Ruby

require 'optparse'
require 'yaml'
require 'rdoc/ri'
require 'rdoc/ri/paths'
require 'rdoc/ri/formatter'
require 'rdoc/ri/display'
require 'fileutils'
require 'rdoc/markup'
require 'rdoc/markup/to_flow'
class RDoc::RI::Driver
#
# This class offers both Hash and OpenStruct functionality.
# We convert from the Core Hash to this before calling any of
# the display methods, in order to give the display methods
# a cleaner API for accessing the data.
#
class OpenStructHash < Hash
#
# This method converts from a Hash to an OpenStructHash.
#
def self.convert(object)
case object
when Hash then
new_hash = new # Convert Hash -> OpenStructHash
object.each do |key, value|
new_hash[key] = convert(value)
end
new_hash
when Array then
object.map do |element|
convert(element)
end
else
object
end
end
def merge_enums(other)
other.each do |k, v|
if self[k] then
case v
when Array then
# HACK dunno
if String === self[k] and self[k].empty? then
self[k] = v
else
self[k] += v
end
when Hash then
self[k].update v
else
# do nothing
end
else
self[k] = v
end
end
end
def method_missing method, *args
self[method.to_s]
end
end
class Error < RDoc::RI::Error; end
class NotFoundError < Error
def message
"Nothing known about #{super}"
end
end
attr_accessor :homepath # :nodoc:
def self.default_options
options = {}
options[:use_stdout] = !$stdout.tty?
options[:width] = 72
options[:formatter] = RDoc::RI::Formatter.for 'plain'
options[:interactive] = false
options[:use_cache] = true
# By default all standard paths are used.
options[:use_system] = true
options[:use_site] = true
options[:use_home] = true
options[:use_gems] = true
options[:extra_doc_dirs] = []
return options
end
def self.process_args(argv)
options = default_options
opts = OptionParser.new do |opt|
opt.program_name = File.basename $0
opt.version = RDoc::VERSION
opt.release = nil
opt.summary_indent = ' ' * 4
directories = [
RDoc::RI::Paths::SYSDIR,
RDoc::RI::Paths::SITEDIR,
RDoc::RI::Paths::HOMEDIR
]
if RDoc::RI::Paths::GEMDIRS then
Gem.path.each do |dir|
directories << "#{dir}/doc/*/ri"
end
end
opt.banner = <<-EOT
Usage: #{opt.program_name} [options] [names...]
Where name can be:
Class | Class::method | Class#method | Class.method | method
All class names may be abbreviated to their minimum unambiguous form. If a name
is ambiguous, all valid options will be listed.
The form '.' method matches either class or instance methods, while #method
matches only instance and ::method matches only class methods.
For example:
#{opt.program_name} Fil
#{opt.program_name} File
#{opt.program_name} File.new
#{opt.program_name} zip
Note that shell quoting may be required for method names containing
punctuation:
#{opt.program_name} 'Array.[]'
#{opt.program_name} compact\\!
By default ri searches for documentation in the following directories:
#{directories.join "\n "}
Specifying the --system, --site, --home, --gems or --doc-dir options will
limit ri to searching only the specified directories.
Options may also be set in the 'RI' environment variable.
EOT
opt.separator nil
opt.separator "Options:"
opt.separator nil
opt.on("--fmt=FORMAT", "--format=FORMAT", "-f",
RDoc::RI::Formatter::FORMATTERS.keys,
"Format to use when displaying output:",
" #{RDoc::RI::Formatter.list}",
"Use 'bs' (backspace) with most pager",
"programs. To use ANSI, either disable the",
"pager or tell the pager to allow control",
"characters.") do |value|
options[:formatter] = RDoc::RI::Formatter.for value
end
opt.separator nil
opt.on("--doc-dir=DIRNAME", "-d", Array,
"List of directories from which to source",
"documentation in addition to the standard",
"directories. May be repeated.") do |value|
value.each do |dir|
unless File.directory? dir then
raise OptionParser::InvalidArgument, "#{dir} is not a directory"
end
options[:extra_doc_dirs] << File.expand_path(dir)
end
end
opt.separator nil
opt.on("--[no-]use-cache",
"Whether or not to use ri's cache.",
"True by default.") do |value|
options[:use_cache] = value
end
opt.separator nil
opt.on("--no-standard-docs",
"Do not include documentation from",
"the Ruby standard library, site_lib,",
"installed gems, or ~/.rdoc.",
"Equivalent to specifying",
"the options --no-system, --no-site, --no-gems,",
"and --no-home") do
options[:use_system] = false
options[:use_site] = false
options[:use_gems] = false
options[:use_home] = false
end
opt.separator nil
opt.on("--[no-]system",
"Include documentation from Ruby's standard",
"library. Defaults to true.") do |value|
options[:use_system] = value
end
opt.separator nil
opt.on("--[no-]site",
"Include documentation from libraries",
"installed in site_lib.",
"Defaults to true.") do |value|
options[:use_site] = value
end
opt.separator nil
opt.on("--[no-]gems",
"Include documentation from RubyGems.",
"Defaults to true.") do |value|
options[:use_gems] = value
end
opt.separator nil
opt.on("--[no-]home",
"Include documentation stored in ~/.rdoc.",
"Defaults to true.") do |value|
options[:use_home] = value
end
opt.separator nil
opt.on("--list-doc-dirs",
"List the directories from which ri will",
"source documentation on stdout and exit.") do
options[:list_doc_dirs] = true
end
opt.separator nil
opt.on("--no-pager", "-T",
"Send output directly to stdout,",
"rather than to a pager.") do
options[:use_stdout] = true
end
opt.on("--interactive", "-i",
"This makes ri go into interactive mode.",
"When ri is in interactive mode it will",
"allow the user to disambiguate lists of",
"methods in case multiple methods match",
"against a method search string. It also",
"will allow the user to enter in a method",
"name (with auto-completion, if readline",
"is supported) when viewing a class.") do
options[:interactive] = true
end
opt.separator nil
opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger,
"Set the width of the output.") do |value|
options[:width] = value
end
end
argv = ENV['RI'].to_s.split.concat argv
opts.parse! argv
options[:names] = argv
options[:formatter] ||= RDoc::RI::Formatter.for('plain')
options[:use_stdout] ||= !$stdout.tty?
options[:use_stdout] ||= options[:interactive]
options[:width] ||= 72
options
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
puts opts
puts
puts e
exit 1
end
def self.run(argv = ARGV)
options = process_args argv
ri = new options
ri.run
end
def initialize(initial_options={})
options = self.class.default_options.update(initial_options)
@names = options[:names]
@class_cache_name = 'classes'
@doc_dirs = RDoc::RI::Paths.path(options[:use_system],
options[:use_site],
options[:use_home],
options[:use_gems],
options[:extra_doc_dirs])
@homepath = RDoc::RI::Paths.raw_path(false, false, true, false).first
@homepath = @homepath.sub(/\.rdoc/, '.ri')
@sys_dir = RDoc::RI::Paths.raw_path(true, false, false, false).first
@list_doc_dirs = options[:list_doc_dirs]
FileUtils.mkdir_p cache_file_path unless File.directory? cache_file_path
@cache_doc_dirs_path = File.join cache_file_path, ".doc_dirs"
@use_cache = options[:use_cache]
@class_cache = nil
@interactive = options[:interactive]
@display = RDoc::RI::DefaultDisplay.new(options[:formatter],
options[:width],
options[:use_stdout])
end
def class_cache
return @class_cache if @class_cache
# Get the documentation directories used to make the cache in order to see
# whether the cache is valid for the current ri instantiation.
if(File.readable?(@cache_doc_dirs_path))
cache_doc_dirs = IO.read(@cache_doc_dirs_path).split("\n")
else
cache_doc_dirs = []
end
newest = map_dirs('created.rid') do |f|
File.mtime f if test ?f, f
end.max
# An up to date cache file must have been created more recently than
# the last modification of any of the documentation directories. It also
# must have been created with the same documentation directories
# as those from which ri currently is sourcing documentation.
up_to_date = (File.exist?(class_cache_file_path) and
newest and newest < File.mtime(class_cache_file_path) and
(cache_doc_dirs == @doc_dirs))
if up_to_date and @use_cache then
open class_cache_file_path, 'rb' do |fp|
begin
@class_cache = Marshal.load fp.read
rescue
#
# This shouldn't be necessary, since the up_to_date logic above
# should force the cache to be recreated when a new version of
# rdoc is installed. This seems like a worthwhile enhancement
# to ri's robustness, however.
#
$stderr.puts "Error reading the class cache; recreating the class cache!"
@class_cache = create_class_cache
end
end
else
@class_cache = create_class_cache
end
@class_cache
end
def create_class_cache
class_cache = OpenStructHash.new
if(@use_cache)
# Dump the documentation directories to a file in the cache, so that
# we only will use the cache for future instantiations with identical
# documentation directories.
File.open @cache_doc_dirs_path, "wb" do |fp|
fp << @doc_dirs.join("\n")
end
end
classes = map_dirs('**/cdesc*.yaml') { |f| Dir[f] }
warn "Updating class cache with #{classes.size} classes..."
populate_class_cache class_cache, classes
write_cache class_cache, class_cache_file_path
class_cache
end
def populate_class_cache(class_cache, classes, extension = false)
classes.each do |cdesc|
desc = read_yaml cdesc
klassname = desc["full_name"]
unless class_cache.has_key? klassname then
desc["display_name"] = "Class"
desc["sources"] = [cdesc]
desc["instance_method_extensions"] = []
desc["class_method_extensions"] = []
class_cache[klassname] = desc
else
klass = class_cache[klassname]
if extension then
desc["instance_method_extensions"] = desc.delete "instance_methods"
desc["class_method_extensions"] = desc.delete "class_methods"
end
klass.merge_enums desc
klass["sources"] << cdesc
end
end
end
def class_cache_file_path
File.join cache_file_path, @class_cache_name
end
def cache_file_for(klassname)
File.join cache_file_path, klassname.gsub(/:+/, "-")
end
def cache_file_path
File.join @homepath, 'cache'
end
def display_class(name)
klass = class_cache[name]
@display.display_class_info klass
end
def display_method(method)
@display.display_method_info method
end
def get_info_for(arg)
@names = [arg]
run
end
def load_cache_for(klassname)
path = cache_file_for klassname
cache = nil
if File.exist? path and
File.mtime(path) >= File.mtime(class_cache_file_path) and
@use_cache then
open path, 'rb' do |fp|
begin
cache = Marshal.load fp.read
rescue
#
# The cache somehow is bad. Recreate the cache.
#
$stderr.puts "Error reading the cache for #{klassname}; recreating the cache!"
cache = create_cache_for klassname, path
end
end
else
cache = create_cache_for klassname, path
end
cache
end
def create_cache_for(klassname, path)
klass = class_cache[klassname]
return nil unless klass
method_files = klass["sources"]
cache = OpenStructHash.new
method_files.each do |f|
system_file = f.index(@sys_dir) == 0
Dir[File.join(File.dirname(f), "*")].each do |yaml|
next unless yaml =~ /yaml$/
next if yaml =~ /cdesc-[^\/]+yaml$/
method = read_yaml yaml
if system_file then
method["source_path"] = "Ruby #{RDoc::RI::Paths::VERSION}"
else
if(f =~ %r%gems/[\d.]+/doc/([^/]+)%) then
ext_path = "gem #{$1}"
else
ext_path = f
end
method["source_path"] = ext_path
end
name = method["full_name"]
cache[name] = method
end
end
write_cache cache, path
end
##
# Finds the next ancestor of +orig_klass+ after +klass+.
def lookup_ancestor(klass, orig_klass)
# This is a bit hacky, but ri will go into an infinite
# loop otherwise, since Object has an Object ancestor
# for some reason. Depending on the documentation state, I've seen
# Kernel as an ancestor of Object and not as an ancestor of Object.
if ((orig_klass == "Object") &&
((klass == "Kernel") || (klass == "Object")))
return nil
end
cache = class_cache[orig_klass]
return nil unless cache
ancestors = [orig_klass]
ancestors.push(*cache.includes.map { |inc| inc['name'] })
ancestors << cache.superclass
ancestor_index = ancestors.index(klass)
if ancestor_index
ancestor = ancestors[ancestors.index(klass) + 1]
return ancestor if ancestor
end
lookup_ancestor klass, cache.superclass
end
##
# Finds the method
def lookup_method(name, klass)
cache = load_cache_for klass
return nil unless cache
method = cache[name.gsub('.', '#')]
method = cache[name.gsub('.', '::')] unless method
method
end
def map_dirs(file_name)
@doc_dirs.map { |dir| yield File.join(dir, file_name) }.flatten.compact
end
##
# Extract the class and method name parts from +name+ like Foo::Bar#baz
def parse_name(name)
parts = name.split(/(::|\#|\.)/)
if parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
meth = parts.pop
parts.pop
end
klass = parts.join
[klass, meth]
end
def read_yaml(path)
data = File.read path
# Necessary to be backward-compatible with documentation generated
# by earliar RDoc versions.
data = data.gsub(/ \!ruby\/(object|struct):(RDoc::RI|RI).*/, '')
data = data.gsub(/ \!ruby\/(object|struct):SM::(\S+)/,
' !ruby/\1:RDoc::Markup::\2')
OpenStructHash.convert(YAML.load(data))
end
def run
if(@list_doc_dirs)
puts @doc_dirs.join("\n")
elsif @names.empty? then
@display.list_known_classes class_cache.keys.sort
else
@names.each do |name|
if class_cache.key? name then
method_map = display_class name
if(@interactive)
method_name = @display.get_class_method_choice(method_map)
if(method_name != nil)
method = lookup_method "#{name}#{method_name}", name
display_method method
end
end
elsif name =~ /::|\#|\./ then
klass, = parse_name name
orig_klass = klass
orig_name = name
loop do
method = lookup_method name, klass
break method if method
ancestor = lookup_ancestor klass, orig_klass
break unless ancestor
name = name.sub klass, ancestor
klass = ancestor
end
raise NotFoundError, orig_name unless method
display_method method
else
methods = select_methods(/#{name}/)
if methods.size == 0
raise NotFoundError, name
elsif methods.size == 1
display_method methods[0]
else
if(@interactive)
@display.display_method_list_choice methods
else
@display.display_method_list methods
end
end
end
end
end
rescue NotFoundError => e
abort e.message
end
def select_methods(pattern)
methods = []
class_cache.keys.sort.each do |klass|
class_cache[klass]["instance_methods"].map{|h|h["name"]}.grep(pattern) do |name|
method = load_cache_for(klass)[klass+'#'+name]
methods << method if method
end
class_cache[klass]["class_methods"].map{|h|h["name"]}.grep(pattern) do |name|
method = load_cache_for(klass)[klass+'::'+name]
methods << method if method
end
end
methods
end
def write_cache(cache, path)
if(@use_cache)
File.open path, "wb" do |cache_file|
Marshal.dump cache, cache_file
end
end
cache
end
end