如何通过命令行获取Ruby class中定义的方法的详细列表?

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

我正在寻找一种在 Ruby Class 中生成所有方法列表的方法,类似于 C 头文件。我想要这个,这样我就可以看到 class 的概览,而无需使用 IDE 代码折叠来折叠所有内容。

最好是 *nix 命令行函数,以便我可以使用其他命令行工具进一步处理它。

输出类似于

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

类似于this question,除了我正在寻找一个包含参数的列表,我想输出到命令行以便进一步处理。我的目标是让这个列表在我工作时可见,所以我希望它一目了然。

对class使用instance_methods然后查询参数

例如

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

输出:

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

您可能需要对参数数组进行子处理以仅获取名称。

至于命令行,只需将其保存到ruby脚本中即可。

如果你想获取Module中定义的所有方法,你可以使用Module#instance_methods系列方法之一,具体取决于什么,确切地说,您正在寻找:

其中每一个都有一个可选的布尔参数 include_super=true,它允许您决定是包含继承的方法(默认)还是只包含来自您要向其发送消息的确切模块的 return 方法(通过 false 时)。

想要获取那些方法的参数,首先需要获取一个UnboundMethod reflective proxy object which represents the method you are interested in. You can do this by using the Module#instance_method.

获得 UnboundMethod 后,您可以使用 UnboundMethod#parameters 获取方法参数的说明。但是请注意,您 而不是 获取可选参数的默认参数。那其实是不可能的。

使用这些构建块,您可以构建如下内容:

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

你可以这样使用它:

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

但是,请注意列出某些模块的方法不会给你协议(即一组消息是理解)那个模块!

这里有两个简单的例子,其中模块中定义的方法集与该模块实例理解的消息集不对应:

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

虽然这是一个愚蠢的例子,并且希望没有人会真正编写代码,但它清楚地表明虽然 Foo 有一个名为 bar 的方法,但它的实例不响应 bar 以您期望的方式发送消息。

这是一个更现实的例子:

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!'

最后,为了以防万一你抱有希望,你可以找到一些疯狂的元编程抽象解释技巧,实际上可以让你列出模块的整个协议,让我纠正你的想法:

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

Voilà:一个 class 实例响应 无限数量的消息 ,事实上,它们响应 每条可能的消息 .如果您认为这是不现实的,那么,实际上,Ruby 宇宙中使用最广泛的库之一就是这样做的:ActiveRecord。

使用 TypeProf 创建 RBS 文件

使用 Ruby >= 2.7.1,正确的答案通常是使用 RBS or TypeProf 创建(非常粗略的)头文件等价物。由于此时您发布的代码甚至还不是 class,并且不包含任何可推断的类型信息,因此您的大多数类型都可能是“未类型化的”,并且由您来填写类型。

Type checking 不由 Ruby 本机处理。为此,您需要使用 Steep、Sorbet 或类似的东西。也就是说,出于文档目的,如果您还没有良好的 YARD 文档,TypeProf 可能是您最好的选择,而 RBS 原型制作是一个合理的后备方案。例如,给定以下 Ruby 源文件:

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

运行ning typeprof example.rb 会产生:

# 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

在可以通过 TypeProf 构建、解析 AST 和代码路径 运行 的真实代码库上,它在推断常见类型方面做得相当合理,尽管有一些例外并且它不会不能很好地处理某些元编程结构。不过,它大多数时候都能满足您的要求。

老实说,除非您打算进行类型检查,否则从文档的角度来看,对 @param and @return 使用 YARD 标记通常会产生更有用的结果。基于文档的类型的问题在于必须积极维护文档;否则,文档可能基于程序员的错误或疏忽而存在。这就是 RBS 和 TypeProf 的优势所在:它们基于实际代码,而不是程序员编辑到文件中的注释。因此,您的里程会根据您的用例而有所不同。