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:
Module#public_instance_methods
for allpublic
methods,Module#protected_instance_methods
for allprotected
methods,Module#instance_methods
for allpublic
andprotected
methods, orModule#private_instance_methods
for allprivate
methods.
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.