Using introspection to get method arguments and other info
I just wrote a small script that uses introspection to tell you the methods defined in a file plus their argument names and default values. It's a quick hack, but it works surprisingly well.
UPDATE: works with more .rb files now (interface also improved somewhat).
Here are some examples:
$ method_args csv CSV::Cell#initialize (data = "", is_null = false) CSV::Cell#data () CSV.open (path, mode, fs = nil, rs = nil) CSV.foreach (path, rs = nil) CSV.read (path, length = nil, offset = nil) [...]
method_args.rb can detect arguments with default values and "splat args":
$ method_args benchmark Benchmark#benchmark (caption = "", label_width = nil, fmtstr = nil, *labels) Benchmark#bm (label_width = 0, *labels) Benchmark#bmbm (width = 0) Benchmark#measure (label = "") [...]
The nice thing is that method_args.rb actually requires your library code and will take into account its dependencies too. So it'll tell you all the methods you'd pull into your runtime if you required some file.
At the time being, all this script does is writing the methods it found on stdout, but it could for instance send them to another application through a socket to provide fairly accurate intellisense information, or generate rich tags, or...
It chokes on some of the files from the stdlib (like e2mmap.rb), but works for most.
Here's the code: method_args.rb
#!/usr/bin/env ruby # Copyright (c) 2006 Mauricio Fernandez <mfp@acm.org> # http://eigenclass.org # Use and distribution subject to the same conditions as Ruby. $VERBOSE = nil $__method_args_off = true if ARGV.empty? puts <<EOF ruby method_args.rb [-i] [-m] [-c] <file> [<file> ...] -i omit instance methods defined in classes -m omit instance methods defined in modules -c omit class methods The given files will be #require()d in order. Examples: ruby method_args.rb complex ruby method_args.rb thread ruby method_args.rb -c rubygems EOF exit end module MethodArgs MAX_ARGS = 20 def output_method_info(klass, object, meth, is_singleton = false) return if $__method_args_off file = line = params = values = nil unless %w[initialize].include?(meth.to_s) if is_singleton return if class << klass; private_instance_methods(true) end.include?(meth.to_s) else return if class << object; private_instance_methods(true) end.include?(meth.to_s) end end arity = is_singleton ? object.method(meth).arity : klass.instance_method(meth).arity set_trace_func lambda{|event, file, line, id, binding, classname| begin #puts "!EVENT: #{event} #{classname}##{id}, #{file} #{line}" #puts "(#{self} #{meth})" if event[/call/] && classname == self && id == meth params = eval("local_variables", binding) values = eval("local_variables.map{|x| eval(x)}", binding) #puts "EVENT: #{event} #{classname}##{id}" throw :done end rescue Exception end } variadic_with_block = false if arity >= 0 num_args = arity catch(:done){ object.send(meth, *(0...arity)) } else num_args = 0 # determine number of args (including splat & block) MAX_ARGS.downto(arity.abs - 1) do |i| catch(:done) do begin object.send(meth, *(0...i)) rescue Exception end end # all nils if there's no splat and we gave too many args next if !values || values.compact.empty? k = nil values.each_with_index{|x,j| break (k = j) if Array === x} if k num_args = k+1 else num_args = i end break end # determine if it's got a block arg =begin 30.downto(arity.abs - 1) do |i| catch(:done) { object.send(meth, *(0...i)) } next if values.compact.empty? variadic_with_block = true if values[-1] == nil end =end args = (0...arity.abs-1).to_a catch(:done) do args.empty? ? object.send(meth) : object.send(meth, *args) end end set_trace_func(nil) if local_variables == params puts "#{klass}#{is_singleton ? "." : "#"}#{meth} (...)" return end fmt_params = lambda do |arr, arity| arr.inject([[], 0]) do |(a, i), x| if Array === values[i] [a << "*#{x}", i+1] else if arity < 0 && i >= arity.abs - 1 [a << "#{x} = #{values[i].inspect}", i + 1] else [a << x, i+1] end end end.first.join(", ") end params ||= [] params = params[0,num_args] #unfortunately, there's no way to tell the block arg from the first local #since its value will be nil even if we pass a block #if arity >= 0 && params[arity] # or variadic_with_block # arg_desc = "(#{fmt_params.call(params[0..-2], arity)}, &#{params.last})" #else arg_desc = "(#{fmt_params.call(params, arity)})" #end puts "#{klass}#{is_singleton ? "." : "#" }#{meth} #{arg_desc}" rescue Exception #puts "GOT EXCEPTION while processing #{klass} #{meth}" #puts $!.message #puts $!.backtrace puts "#{klass}#{is_singleton ? "." : "#"}#{meth} (...)" ensure set_trace_func(nil) end end new_args = [] omissions = {} ARGV.each do |arg| case arg when /-./; omissions[$&] = true else new_args << arg end end ARGV.replace new_args class Object include MethodArgs def self.method_added(meth) if [Integer, Fixnum, Bignum, Float].include? self or defined?(Digest::Base) && Digest::Base == self puts "#{self}##{meth} (...)" return end begin o = self.allocate rescue Exception p $! end output_method_info(self, o, meth, false) end end unless omissions["-i"] class Module method_added = instance_method(:method_added) define_method(:method_added) do |meth| begin if instance_of? Module o = Object.new o.extend(self) output_method_info(self, o, meth, false) end method_added.bind(self).call(meth) rescue Exception puts "#{self}##{meth} (...)" end end end unless omissions["-m"] class Class def singleton_method_added(meth) output_method_info(self, self, meth, true) rescue Exception puts "#{self}.#{meth} (...)" end end unless omissions["-c"] $__method_args_off = false ARGV.each{|x| require x} END { puts "zzzz OK" }
No Title - Kent (2006-09-08 (Fri) 10:20:33)
Very nice!!!
Steph. 2006-09-08 (Fre) 12:23:44
Does this script require ruby 1.8.5 or 1.9?
I get an error in line 95:
method_args.rb:95: in 'allocate': allocator undefined in for Binding (TypeError)
(with ruby 1.8.4)
mfp 2006-09-08 (Fri) 13:01:29
It works with Ruby 1.8.5. However, it will choke on some files. In your case, it saw a method being defined in class Binding (which cannot be instantiated) and the exception wasn't rescued correctly.
You can try to ignore exceptions and keep processing with this:
--- method_args.rb.orig 2006-09-08 20:53:00.000000000 +0200
+++ method_args.rb 2006-09-08 20:54:10.000000000 +0200
@@ -96,7 +96,7 @@
o = allocate
output_method_info(self, o, meth, false)
super
- rescue NotImplementedError
+ rescue Exception
# not instantiable
puts "#{self}##{meth} (...)"
end
@@ -104,18 +104,24 @@
class Module
method_added = instance_method(:method_added)
define_method(:method_added) do |meth|
- if instance_of? Module
- o = Object.new
- o.extend(self)
- output_method_info(self, o, meth, false)
+ begin
+ if instance_of? Module
+ o = Object.new
+ o.extend(self)
+ output_method_info(self, o, meth, false)
+ end
+ method_added.bind(self).call(meth)
+ rescue Exception
+ puts "#{self}##{meth} (...)"
end
- method_added.bind(self).call(meth)
end
end
class Class
def singleton_method_added(meth)
output_method_info(self, self, meth, true)
super
+ rescue Exception
+ puts "#{self}##{meth} (...)"
end
end
- 100 http://soc.jayunit.net/articles/2006/09/12/ruby-introspection
- 79 http://soc.jayunit.net
- 68 http://www.ruby-forum.com/topic/85312
- 47 http://www.artima.com/forums/flat.jsp?forum=123&thread=175559
- 36 http://anarchaia.org
- 16 http://planetruby.0x42.net
- 10 http://www.artima.com/buzz/community.jsp?forum=123
- 9 http://www.anarchaia.org
- 9 http://www.rubycorner.com
- 7 http://anarchaia.org/archive/2006/09/08.html
Keyword(s):[blog] [ruby] [frontpage] [method] [arguments] [introspection] [snippet]
References: