Update to RDoc r56
[rbx.git] / lib / rdoc / ri / driver.rb
blobecf1bf9f2797363ed6d9c11975aa77ffff584a70
1 require 'optparse'
2 require 'yaml'
4 require 'rdoc/ri'
5 require 'rdoc/ri/paths'
6 require 'rdoc/ri/formatter'
7 require 'rdoc/ri/display'
8 require 'fileutils'
9 require 'rdoc/markup'
10 require 'rdoc/markup/to_flow'
12 class RDoc::RI::Driver
14   class Hash < ::Hash
15     def self.convert(hash)
16       hash = new.update hash
18       hash.each do |key, value|
19         hash[key] = case value
20                     when ::Hash then
21                       convert value
22                     when Array then
23                       value = value.map do |v|
24                         ::Hash === v ? convert(v) : v
25                       end
26                       value
27                     else
28                       value
29                     end
30       end
32       hash
33     end
35     def method_missing method, *args
36       self[method.to_s]
37     end
39     def merge_enums(other)
40       other.each do |k, v|
41         if self[k] then
42           case v
43           when Array then
44             # HACK dunno
45             if String === self[k] and self[k].empty? then
46               self[k] = v
47             else
48               self[k] += v
49             end
50           when Hash then
51             self[k].update v
52           else
53             # do nothing
54           end
55         else
56           self[k] = v
57         end
58       end
59     end
60   end
62   class Error < RDoc::RI::Error; end
64   class NotFoundError < Error
65     def message
66       "Nothing known about #{super}"
67     end
68   end
70   attr_accessor :homepath # :nodoc:
72   def self.process_args(argv)
73     options = {}
74     options[:use_stdout] = !$stdout.tty?
75     options[:width] = 72
76     options[:formatter] = RDoc::RI::Formatter.for 'plain'
77     options[:list_classes] = false
78     options[:list_names] = false
80     # By default all paths are used.  If any of these are true, only those
81     # directories are used.
82     use_system = false
83     use_site = false
84     use_home = false
85     use_gems = false
86     doc_dirs = []
88     opts = OptionParser.new do |opt|
89       opt.program_name = File.basename $0
90       opt.version = RDoc::VERSION
91       opt.summary_indent = ' ' * 4
93       directories = [
94         RDoc::RI::Paths::SYSDIR,
95         RDoc::RI::Paths::SITEDIR,
96         RDoc::RI::Paths::HOMEDIR
97       ]
99       if RDoc::RI::Paths::GEMDIRS then
100         Gem.path.each do |dir|
101           directories << "#{dir}/doc/*/ri"
102         end
103       end
105       opt.banner = <<-EOT
106 Usage: #{opt.program_name} [options] [names...]
108 Where name can be:
110   Class | Class::method | Class#method | Class.method | method
112 All class names may be abbreviated to their minimum unambiguous form. If a name
113 is ambiguous, all valid options will be listed.
115 The form '.' method matches either class or instance methods, while #method
116 matches only instance and ::method matches only class methods.
118 For example:
120     #{opt.program_name} Fil
121     #{opt.program_name} File
122     #{opt.program_name} File.new
123     #{opt.program_name} zip
125 Note that shell quoting may be required for method names containing
126 punctuation:
128     #{opt.program_name} 'Array.[]'
129     #{opt.program_name} compact\\!
131 By default ri searches for documentation in the following directories:
133     #{directories.join "\n    "}
135 Specifying the --system, --site, --home, --gems or --doc-dir options will
136 limit ri to searching only the specified directories.
138 Options may also be set in the 'RI' environment variable.
139       EOT
141       opt.separator nil
142       opt.separator "Options:"
143       opt.separator nil
145       opt.on("--classes", "-c",
146              "Display the names of classes and modules we",
147              "know about.") do |value|
148         options[:list_classes] = value
149       end
151       opt.separator nil
153       opt.on("--doc-dir=DIRNAME", "-d", Array,
154              "List of directories to search for",
155              "documentation. If not specified, we search",
156              "the standard rdoc/ri directories. May be",
157              "repeated.") do |value|
158         value.each do |dir|
159           unless File.directory? dir then
160             raise OptionParser::InvalidArgument, "#{dir} is not a directory"
161           end
162         end
164         doc_dirs.concat value
165       end
167       opt.separator nil
169       opt.on("--fmt=FORMAT", "--format=FORMAT", "-f",
170              RDoc::RI::Formatter::FORMATTERS.keys,
171              "Format to use when displaying output:",
172              "   #{RDoc::RI::Formatter.list}",
173              "Use 'bs' (backspace) with most pager",
174              "programs. To use ANSI, either disable the",
175              "pager or tell the pager to allow control",
176              "characters.") do |value|
177         options[:formatter] = RDoc::RI::Formatter.for value
178       end
180       opt.separator nil
182       unless RDoc::RI::Paths::GEMDIRS.empty? then
183         opt.on("--[no-]gems",
184                "Include documentation from RubyGems.") do |value|
185           use_gems = value
186         end
187       end
189       opt.separator nil
191       opt.on("--[no-]home",
192              "Include documentation stored in ~/.rdoc.") do |value|
193         use_home = value
194       end
196       opt.separator nil
198       opt.on("--[no-]list-names", "-l",
199              "List all the names known to RDoc, one per",
200              "line.") do |value|
201         options[:list_names] = value
202       end
204       opt.separator nil
206       opt.on("--no-pager", "-T",
207              "Send output directly to stdout.") do |value|
208         options[:use_stdout] = !value
209       end
211       opt.separator nil
213       opt.on("--[no-]site",
214              "Include documentation from libraries",
215              "installed in site_lib.") do |value|
216         use_site = value
217       end
219       opt.separator nil
221       opt.on("--[no-]system",
222              "Include documentation from Ruby's standard",
223              "library.") do |value|
224         use_system = value
225       end
227       opt.separator nil
229       opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger,
230              "Set the width of the output.") do |value|
231         options[:width] = value
232       end
233     end
235     argv = ENV['RI'].to_s.split.concat argv
237     opts.parse! argv
239     options[:names] = argv
241     options[:path] = RDoc::RI::Paths.path(use_system, use_site, use_home,
242                                           use_gems, *doc_dirs)
243     options[:raw_path] = RDoc::RI::Paths.raw_path(use_system, use_site,
244                                                   use_home, use_gems, *doc_dirs)
246     options
248   rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
249     puts opts
250     puts
251     puts e
252     exit 1
253   end
255   def self.run(argv = ARGV)
256     options = process_args argv
257     ri = new options
258     ri.run
259   end
261   def initialize(options={})
262     options[:formatter] ||= RDoc::RI::Formatter.for('plain')
263     options[:use_stdout] ||= !$stdout.tty?
264     options[:width] ||= 72
265     @names = options[:names]
267     @class_cache_name = 'classes'
268     @all_dirs = RDoc::RI::Paths.path(true, true, true, true)
269     @homepath = RDoc::RI::Paths.raw_path(false, false, true, false).first
270     @homepath = @homepath.sub(/\.rdoc/, '.ri')
271     @sys_dirs = RDoc::RI::Paths.raw_path(true, false, false, false)
273     FileUtils.mkdir_p cache_file_path unless File.directory? cache_file_path
275     @class_cache = nil
277     @display = RDoc::RI::DefaultDisplay.new(options[:formatter],
278                                             options[:width],
279                                             options[:use_stdout])
280   end
282   def class_cache
283     return @class_cache if @class_cache
285     newest = map_dirs('created.rid', :all) do |f|
286       File.mtime f if test ?f, f
287     end.max
289     up_to_date = (File.exist?(class_cache_file_path) and
290                   newest and newest < File.mtime(class_cache_file_path))
292     @class_cache = if up_to_date then
293                      load_cache_for @class_cache_name
294                    else
295                      class_cache = RDoc::RI::Driver::Hash.new
297                      classes = map_dirs('**/cdesc*.yaml', :sys) { |f| Dir[f] }
298                      populate_class_cache class_cache, classes
300                      classes = map_dirs('**/cdesc*.yaml') { |f| Dir[f] }
301                      warn "Updating class cache with #{classes.size} classes..."
303                      populate_class_cache class_cache, classes, true
304                      write_cache class_cache, class_cache_file_path
305                    end
306   end
308   def class_cache_file_path
309     File.join cache_file_path, @class_cache_name
310   end
312   def cache_file_for(klassname)
313     File.join cache_file_path, klassname.gsub(/:+/, "-")
314   end
316   def cache_file_path
317     File.join @homepath, 'cache'
318   end
320   def display_class(name)
321     klass = class_cache[name]
322     klass = RDoc::RI::Driver::Hash.convert klass
323     @display.display_class_info klass, class_cache
324   end
326   def get_info_for(arg)
327     @names = [arg]
328     run
329   end
331   def load_cache_for(klassname)
332     path = cache_file_for klassname
334     cache = nil
336     if File.exist? path and
337        File.mtime(path) >= File.mtime(class_cache_file_path) then
338       File.open path, 'rb' do |fp|
339         cache = Marshal.load fp.read
340       end
341     else
342       class_cache = nil
344       File.open class_cache_file_path, 'rb' do |fp|
345         class_cache = Marshal.load fp.read
346       end
348       klass = class_cache[klassname]
349       return nil unless klass
351       method_files = klass["sources"]
352       cache = RDoc::RI::Driver::Hash.new
354       sys_dir = @sys_dirs.first
355       method_files.each do |f|
356         system_file = f.index(sys_dir) == 0
357         Dir[File.join(File.dirname(f), "*")].each do |yaml|
358           next unless yaml =~ /yaml$/
359           next if yaml =~ /cdesc-[^\/]+yaml$/
360           method = read_yaml yaml
361           name = method["full_name"]
362           ext_path = f
363           ext_path = "gem #{$1}" if f =~ %r%gems/[\d.]+/doc/([^/]+)%
364           method["source_path"] = ext_path unless system_file
365           cache[name] = RDoc::RI::Driver::Hash.convert method
366         end
367       end
369       write_cache cache, path
370     end
372     RDoc::RI::Driver::Hash.convert cache
373   end
375   ##
376   # Finds the method
378   def lookup_method(name, klass)
379     cache = load_cache_for klass
380     raise NotFoundError, name unless cache
382     method = cache[name.gsub('.', '#')]
383     method = cache[name.gsub('.', '::')] unless method
384     raise NotFoundError, name unless method
386     method
387   end
389   def map_dirs(file_name, system=false)
390     dirs = if system == :all then
391              @all_dirs
392            else
393              if system then
394                @sys_dirs
395              else
396                @all_dirs - @sys_dirs
397              end
398            end
400     dirs.map { |dir| yield File.join(dir, file_name) }.flatten.compact
401   end
403   ##
404   # Extract the class and method name parts from +name+ like Foo::Bar#baz
406   def parse_name(name)
407     parts = name.split(/(::|\#|\.)/)
409     if parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
410       meth = parts.pop
411       parts.pop
412     end
414     klass = parts.join
416     [klass, meth]
417   end
419   def populate_class_cache(class_cache, classes, extension = false)
420     classes.each do |cdesc|
421       desc = read_yaml cdesc
422       klassname = desc["full_name"]
424       unless class_cache.has_key? klassname then
425         desc["display_name"] = "Class"
426         desc["sources"] = [cdesc]
427         desc["instance_method_extensions"] = []
428         desc["class_method_extensions"] = []
429         class_cache[klassname] = desc
430       else
431         klass = class_cache[klassname]
433         if extension then
434           desc["instance_method_extensions"] = desc.delete "instance_methods"
435           desc["class_method_extensions"] = desc.delete "class_methods"
436         end
438         klass.merge_enums desc
439         klass["sources"] << cdesc
440       end
441     end
442   end
444   def read_yaml(path)
445     data = File.read path
446     data = data.gsub(/ \!ruby\/(object|struct):(RDoc::RI|RI).*/, '')
447     data = data.gsub(/ \!ruby\/(object|struct):SM::(\S+)/,
448                      ' !ruby/\1:RDoc::Markup::\2')
449     YAML.load data
450   end
452   def run
453     if @names.empty? then
454       @display.list_known_classes class_cache.keys.sort
455     else
456       @names.each do |name|
457         case name
458         when /::|\#|\./ then
459           if class_cache.key? name then
460             display_class name
461           else
462             meth = nil
464             klass, meth = parse_name name
466             method = lookup_method name, klass
468             @display.display_method_info method
469           end
470         else
471           if class_cache.key? name then
472             display_class name
473           else
474             methods = select_methods(/^#{name}/)
475             if methods.size == 0
476               raise NotFoundError, name
477             elsif methods.size == 1
478               @display.display_method_info methods.first
479             else
480               @display.display_method_list methods
481             end
482           end
483         end
484       end
485     end
486   rescue NotFoundError => e
487     abort e.message
488   end
490   def select_methods(pattern)
491     methods = []
492     class_cache.keys.sort.each do |klass|
493       class_cache[klass]["instance_methods"].map{|h|h["name"]}.grep(pattern) do |name|
494         method = load_cache_for(klass)[klass+'#'+name]
495         methods << method if method
496       end
497       class_cache[klass]["class_methods"].map{|h|h["name"]}.grep(pattern) do |name|
498         method = load_cache_for(klass)[klass+'::'+name]
499         methods << method if method
500       end
501     end
502     methods
503   end
505   def write_cache(cache, path)
506     File.open path, "wb" do |cache_file|
507       Marshal.dump cache, cache_file
508     end
510     cache
511   end