eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

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.

/hiki/methodargumentsviaintrospection/update.png 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


Last modified:2006/09/08 08:22:46
Keyword(s):[blog] [ruby] [frontpage] [method] [arguments] [introspection] [snippet]
References: