#!/usr/bin/env ruby require 'rdoc/ri/ri_reader' require 'rdoc/ri/ri_cache' require 'rdoc/ri/ri_paths' require 'stringio' require 'pp' require 'optparse' class ApplicationError < StandardError; end RDOC_DIRS = [ ['1.8.2', 'c:/tmp/ri-1.8.2-system'], ['1.8.3', 'c:/ruby-1.8.3/share/ri/1.8/system'], ['1.8.4', 'c:/ruby-1.8.4/share/ri/1.8/system'], ['1.8.5', 'c:/ruby-1.8.5/share/ri/1.8/system'], ['1.9.0', 'c:/usr/local/share/ri/1.9/system'], ] def main begin Signal.trap(:PIPE, 'EXIT') rescue ArgumentError # mswin32, mingw32 # unsupported signal SIGPIPE end Signal.trap(:INT, 'EXIT') parser = OptionParser.new parser.banner = "Usage: #{File.basename($0)} " unless ARGV.size == 1 $stderr.puts "Usage: #{$0} " exit 1 end parser.on('--help', 'Prints this message and quit.') { puts parser.help exit 0 } begin parser.parse! rescue OptionParser::ParseError => err $stderr.puts err.message $stderr.puts parser.help exit 1 end cname = ARGV[0] ri_structs = RDOC_DIRS.map do |version, rdoc_dir| RiStruct.new(version, rdoc_dir) end ri_structs.each do |st| st.lookup_class(cname) end st0 = ri_structs.shift for st1 in ri_structs puts "--- #{st0.version}" puts "+++ #{st1.version}" unless st0.ri_class puts "- no such class" end unless st1.ri_class puts "+ no such class" end if st0.ri_class && st1.ri_class win, lose = *diff_class(st0, st1) win.each do |m| puts "+ #{m.fullname}" end lose.each do |m| puts "- #{m.fullname}" end end st0 = st1 end end def diff_class(st0, st1) unzip(diff_entries(st1.s, st0.s), diff_entries(st1.i, st0.i))\ .map {|list| list.flatten } end def unzip(*tuples) [tuples.map {|s, i| s }, tuples.map {|s, i| i }] end def diff_entries(e1, e0) [e1 - e0, e0 - e1] end class RiStruct def initialize(version, rdoc_dir) @version = version @rdoc_dir = rdoc_dir @path = RI::Paths.path(false, false, false, false, @rdoc_dir) @reader = RI::RiReader.new(RI::RiCache.new(@path)) @ri_class = nil end attr_reader :version, :rdoc_dir, :path, :reader def lookup_class(cname) begin @ri_class = ri_lookup_class(cname) @singleton_entries = ri_entries(@reader.singleton_methods(@ri_class)) @instance_entries = ri_entries(@reader.instance_methods(@ri_class)) rescue ApplicationError # end end attr_reader :ri_class, :singleton_entries, :instance_entries alias s singleton_entries alias i instance_entries private def ri_lookup_class(name) ns = @reader.top_level_namespace.first name.split('::').each do |const| ns = ns.contained_class_named(const) or raise ApplicationError, "no such class in RDoc database: #{name}" end ns end def ri_entries(ri) ri.map {|m| [RiMethodEntry.new(m.name, m)] + m.aliases.map {|a| RiMethodEntry.new(a.name, m) } }.flatten.uniq.sort end end class Ent def initialize(name, ent) @name = name @entry = ent end attr_reader :name attr_reader :entry def ==(other) @name == other.name end alias eql? == def hash @name.hash end def <=>(other) @name <=> other.name end end class RiMethodEntry < Ent def bitclust? false end def inspect "\#" end def singleton_method? @entry.singleton_method? end def fullname c, t, m = @entry.fullname.split(/([\.\#])/, 2) "#{c}#{t}#{@name}" end end module RI class RiReader # reopen def singleton_methods(c) c.singleton_methods.map {|ent| get_method(ent) } end def instance_methods(c) c.instance_methods.map {|ent| get_method(ent) } end end class ClassEntry # reopen def singleton_methods @class_methods end attr_reader :instance_methods def method_entries @class_methods.sort_by {|m| m.name } + @instance_methods.sort_by {|m| m.name } end end class MethodEntry # reopen def fullname "#{@in_class.full_name}#{@is_class_method ? '.' : '#'}#{@name}" end def singleton_method? @is_class_method end end class MethodDescription # reopen def fullname name = full_name() unless /\#/ =~ name components = name.split('::') m = components.pop components.join('::') + '.' + m else name end end def singleton_method? @is_class_method end end end main