Home > Net >  How to get a detailed list of methods defined in a Ruby class via the command line?
How to get a detailed list of methods defined in a Ruby class via the command line?

Time:11-18

I am looking for a way to generate a list of all methods within a Ruby Class, similar to a C Header File. I want this so that I can see an overview of the class without using IDE code folding to collapse everything.

Preferably this would be a *nix command line function so that I can further process this using other command line tools.

Output would be something like

def foo( a, b, c )
def bar
def baz( d )

Similar to this question except I am looking for a list that includes parameters and I would like to output to the command line so that it can be further processed. The goal is to have this list viewable while I am working so I want it to be readable at a glance.

CodePudding user response:

Use instance_methods for that class and then query the parameters

e.g.

class A 
  def foo
  end
  def bar(a,b,c)
  end
end


A.instance_methods(false).each do |s|
   print "def #{s}(#{A.instance_method(s).parameters})\n"
end

Output:

def foo([])
def bar([[:req, :a], [:req, :b], [:req, :c]])

You might need to get subprocess the parameters array to get the name only.

As for command line, just save it into a ruby script.

CodePudding user response:

If you want to get all methods defined in a Module, you can use one of the Module#instance_methods family of methods, depending on what, exactly, you are looking for:

Each of these has an optional boolean parameter include_super=true, which lets you decide whether to include inherited methods (the default) or only return methods from the exact module you are sending the message to (when passing false).

If you want to get the parameters of those methods, you first need to obtain an UnboundMethod reflective proxy object which represents the method you are interested in. You can do this by using the Module#instance_method.

Once you have an UnboundMethod, you can use UnboundMethod#parameters to get a description of the method's parameters. Note, however, that you do not get the default argument of an optional parameter. That is actually impossible.

With these building blocks, you can build something like this:

class MethodHeaderFormatter
  private

  attr_accessor :name, :parameter_list

  def initialize(name, parameter_list)
    self.name = name
    self.parameter_list = MethodParameterListFormatter.new(parameter_list)
  end

  public

  def to_s = "def #{name}"   if parameter_list.empty? then '' else "(#{parameter_list})" end

  class MethodParameterListFormatter
    private

    attr_accessor :parameter_list

    def initialize(parameter_list)
      self.parameter_list = parameter_list.map(&MethodParameterFormatter.method(:[]))
    end

    public

    def empty? = parameter_list.empty?

    def to_s = parameter_list.join(', ')

    module MethodParameterFormatter
      private

      attr_accessor :name, :prefix, :suffix

      def initialize(name) = self.name = name

      public

      def self.[]((type, name)) = const_get(:"#{type.capitalize}MethodParameterFormatter").new(name)

      def to_s = "#{prefix}#{name}#{suffix}"

      class ReqMethodParameterFormatter; include MethodParameterFormatter end

      class OptMethodParameterFormatter
        include MethodParameterFormatter

        def initialize(name)
          super
          self.suffix = '=unknown'
        end
      end

      class RestMethodParameterFormatter
        include MethodParameterFormatter

        def initialize(name)
          super
          self.prefix = '*'
        end
      end

      class KeyreqMethodParameterFormatter
        include MethodParameterFormatter

        def initialize(name)
          super
          self.suffix = ':'
        end
      end

      class KeyMethodParameterFormatter
        include MethodParameterFormatter

        def initialize(name)
          super
          self.suffix = ': unknown'
        end
      end

      class KeyrestMethodParameterFormatter
        include MethodParameterFormatter

        def initialize(name)
          super
          self.prefix = '**'
        end
      end

      class BlockMethodParameterFormatter
        include MethodParameterFormatter

        def initialize(name)
          super
          self.prefix = '&'
        end
      end

      private_constant *constants
    end

    private_constant *constants
  end

  private_constant *constants
end

And you can use it like this:

module Test
  def foo(a, b, c) end
  def bar; end
  def baz(d) end
  def quux(m, o = 23, *r, k:, ok: 42, **kr, &b) end
  alias_method :blarf, :quux
  attr_accessor :frotz
end

puts Test.public_instance_methods(false).map { |meth| MethodHeaderFormatter.new(meth, Test.instance_method(meth).parameters) }
# def baz(d)
# def quux(m, o=unknown, *r, k:, ok: unknown, **kr, &b)
# def frotz=()
# def blarf(m, o=unknown, *r, k:, ok: unknown, **kr, &b)
# def frotz
# def foo(a, b, c)
# def bar

HOWEVER, please note that listing the methods of some module does not give you the protocol (i.e. the set of messages that are understood) of that module!

Here are two simple examples where the set of methods defined in a module does not correspond to the set of messages understood by instances of that module:

class Foo
  def bar = raise(NoMethodError)
  def respond_to?(meth) = meth != :bar && super
end

foo = Foo.new
foo.respond_to?(:bar) #=> false
foo.bar               # NoMethodError

While this is a stupid example, and code that hopefully nobody would write for real, it clearly shows that while Foo has a method named bar, its instances do not respond to a bar message the way you would expect.

Here is a more realistic example:

class Bar
  def method_missing(meth, *) = if meth == :foo then 'Fooooo!' else super end
  def respond_to_missing?(meth, *) = meth == :foo || super
end

bar = Bar.new
bar.respond_to?(:foo) #=> true
bar.foo               #=> 'Fooooo!'

And finally, just in case you get your hopes up that you can find some insane meta-programming abstract interpretation trick that actually lets you list out the entire protocol of a module, let me disabuse you of that notion:

class Quux
  def method_missing(*); end
  def respond_to_missing?(*) = true
end

Voilà: a class whose instances respond to an infinite number of messages, in fact, they respond to every possible message. And if you think this is unrealistic, well, actually, something like this is what one of the most widely-used libraries in the Ruby universe does: ActiveRecord.

CodePudding user response:

Use TypeProf to Create RBS Files

With Ruby >= 2.7.1, the correct answer is generally to use RBS or TypeProf to create the (very rough) equivalent of header files. Since your posted code isn't even a class at this point, and doesn't contain any inferrable type information, most of your types will all likely be "untyped," and it will be up to you to fill in the types.

Type checking is not handled by Ruby natively. For that, you'll need to use Steep, Sorbet, or something similar. That said, for documentation purposes, TypeProf is probably your best bet if you don't already have good YARD documentation, with RBS prototyping as a reasonable fallback. For example, given the following Ruby source file:

class Example
  def foo(a, b, c); end
  def bar; end
  def baz(d); end
end

running typeprof example.rb would yield:

# TypeProf 0.20.2

# Classes
class Example
  def foo: (untyped a, untyped b, untyped c) -> nil
  def bar: -> nil
  def baz: (untyped d) -> nil
end

On real code bases where an AST can be built, parsed, and code paths run via TypeProf, it does a fairly reasonable job of inferring common types, although there are a few exceptions and it doesn't do well with certain metaprogramming constructs. Still, it delivers what you're asking for most of the time.

To be honest though, unless you're planning to do type checking, using YARD tags for @param and @return will often yield more useful results from a documentation standpoint. The problem with documentation-based typing is that documentation has to be actively maintained; otherwise, the documentation can lie based on programmer error or neglect. That's where RBS and TypeProf have an advantage: they're based on the actual code, not on comments the programmer edits into the file. So, your mileage will vary based on on your use case.

  • Related