Fix up Rubinius specific library specs.
[rbx.git] / lib / rdoc / ri / driver.rb
blobdfc5f2f98a10cec4dfff1fe0b84d5983f6fa2ffc
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
307     @class_cache = RDoc::RI::Driver::Hash.convert @class_cache
308     @class_cache
309   end
311   def class_cache_file_path
312     File.join cache_file_path, @class_cache_name
313   end
315   def cache_file_for(klassname)
316     File.join cache_file_path, klassname.gsub(/:+/, "-")
317   end
319   def cache_file_path
320     File.join @homepath, 'cache'
321   end
323   def display_class(name)
324     klass = class_cache[name]
325     klass = RDoc::RI::Driver::Hash.convert klass
326     @display.display_class_info klass, class_cache
327   end
329   def get_info_for(arg)
330     @names = [arg]
331     run
332   end
334   def load_cache_for(klassname)
335     path = cache_file_for klassname
337     cache = nil
339     if File.exist? path and
340        File.mtime(path) >= File.mtime(class_cache_file_path) then
341       open path, 'rb' do |fp|
342         cache = Marshal.load fp.read
343       end
344     else
345       class_cache = nil
347       open class_cache_file_path, 'rb' do |fp|
348         class_cache = Marshal.load fp.read
349       end
351       klass = class_cache[klassname]
352       return nil unless klass
354       method_files = klass["sources"]
355       cache = RDoc::RI::Driver::Hash.new
357       sys_dir = @sys_dirs.first
358       method_files.each do |f|
359         system_file = f.index(sys_dir) == 0
360         Dir[File.join(File.dirname(f), "*")].each do |yaml|
361           next unless yaml =~ /yaml$/
362           next if yaml =~ /cdesc-[^\/]+yaml$/
363           method = read_yaml yaml
364           name = method["full_name"]
365           ext_path = f
366           ext_path = "gem #{$1}" if f =~ %r%gems/[\d.]+/doc/([^/]+)%
367           method["source_path"] = ext_path unless system_file
368           cache[name] = RDoc::RI::Driver::Hash.convert method
369         end
370       end
372       write_cache cache, path
373     end
375     RDoc::RI::Driver::Hash.convert cache
376   end
378   ##
379   # Finds the next ancestor of +orig_klass+ after +klass+.
381   def lookup_ancestor(klass, orig_klass)
382     cache = class_cache[orig_klass]
384     return nil unless cache
386     ancestors = [orig_klass]
387     ancestors.push(*cache.includes.map { |inc| inc['name'] })
388     ancestors << cache.superclass
390     ancestor = ancestors[ancestors.index(klass) + 1]
392     return ancestor if ancestor
394     lookup_ancestor klass, cache.superclass
395   end
397   ##
398   # Finds the method
400   def lookup_method(name, klass)
401     cache = load_cache_for klass
402     return nil unless cache
404     method = cache[name.gsub('.', '#')]
405     method = cache[name.gsub('.', '::')] unless method
406     method
407   end
409   def map_dirs(file_name, system=false)
410     dirs = if system == :all then
411              @all_dirs
412            else
413              if system then
414                @sys_dirs
415              else
416                @all_dirs - @sys_dirs
417              end
418            end
420     dirs.map { |dir| yield File.join(dir, file_name) }.flatten.compact
421   end
423   ##
424   # Extract the class and method name parts from +name+ like Foo::Bar#baz
426   def parse_name(name)
427     parts = name.split(/(::|\#|\.)/)
429     if parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
430       meth = parts.pop
431       parts.pop
432     end
434     klass = parts.join
436     [klass, meth]
437   end
439   def populate_class_cache(class_cache, classes, extension = false)
440     classes.each do |cdesc|
441       desc = read_yaml cdesc
442       klassname = desc["full_name"]
444       unless class_cache.has_key? klassname then
445         desc["display_name"] = "Class"
446         desc["sources"] = [cdesc]
447         desc["instance_method_extensions"] = []
448         desc["class_method_extensions"] = []
449         class_cache[klassname] = desc
450       else
451         klass = class_cache[klassname]
453         if extension then
454           desc["instance_method_extensions"] = desc.delete "instance_methods"
455           desc["class_method_extensions"] = desc.delete "class_methods"
456         end
458         klass = RDoc::RI::Driver::Hash.convert klass
460         klass.merge_enums desc
461         klass["sources"] << cdesc
462       end
463     end
464   end
466   def read_yaml(path)
467     data = File.read path
468     data = data.gsub(/ \!ruby\/(object|struct):(RDoc::RI|RI).*/, '')
469     data = data.gsub(/ \!ruby\/(object|struct):SM::(\S+)/,
470                      ' !ruby/\1:RDoc::Markup::\2')
471     YAML.load data
472   end
474   def run
475     if @names.empty? then
476       @display.list_known_classes class_cache.keys.sort
477     else
478       @names.each do |name|
479         case name
480         when /::|\#|\./ then
481           if class_cache.key? name then
482             display_class name
483           else
484             klass, = parse_name name
486             orig_klass = klass
487             orig_name = name
489             until klass == 'Kernel' do
490               method = lookup_method name, klass
492               break method if method
494               ancestor = lookup_ancestor klass, orig_klass
496               break unless ancestor
498               name = name.sub klass, ancestor
499               klass = ancestor
500             end
502             raise NotFoundError, orig_name unless method
504             @display.display_method_info method
505           end
506         else
507           if class_cache.key? name then
508             display_class name
509           else
510             methods = select_methods(/^#{name}/)
512             if methods.size == 0
513               raise NotFoundError, name
514             elsif methods.size == 1
515               @display.display_method_info methods.first
516             else
517               @display.display_method_list methods
518             end
519           end
520         end
521       end
522     end
523   rescue NotFoundError => e
524     abort e.message
525   end
527   def select_methods(pattern)
528     methods = []
529     class_cache.keys.sort.each do |klass|
530       class_cache[klass]["instance_methods"].map{|h|h["name"]}.grep(pattern) do |name|
531         method = load_cache_for(klass)[klass+'#'+name]
532         methods << method if method
533       end
534       class_cache[klass]["class_methods"].map{|h|h["name"]}.grep(pattern) do |name|
535         method = load_cache_for(klass)[klass+'::'+name]
536         methods << method if method
537       end
538     end
539     methods
540   end
542   def write_cache(cache, path)
543     File.open path, "wb" do |cache_file|
544       Marshal.dump cache, cache_file
545     end
547     cache
548   end