在给定ruby文件的情况下获取Ruby方法的开头和结尾的行号

Get line number of beginning and end of Ruby method given a ruby file

如何在给定ruby文件的情况下找到Ruby方法的开头和结尾的行?

比如说:

1
2
3
4
5
1 class Home
2   def initialize(color)
3     @color = color
4   end
5 end

给定文件home.rb和方法名称initialize我希望接收(2,4)这些是开始和结束行。


找到end很棘手。我能想到的最好的方法是使用解析器gem。基本上,您将Ruby代码解析为AST,然后递归遍历其节点,直到找到类型为:def且第一个子节点为:initialize的节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require"parser/current"

def recursive_find(node, &block)
  return node if block.call(node)
  return nil unless node.respond_to?(:children) && !node.children.empty?
  node.children.each do |child_node|
    found = recursive_find(child_node, &block)
    return found if found
  end
  nil
end

src = <<END
  class Home
    def initialize(color)
      @color = color
    end
  end
END

ast = Parser::CurrentRuby.parse(src)

found = recursive_find(ast) do |node|
  node.respond_to?(:type) && node.type == :def && node.children[0] == :initialize
end

puts"Start: #{found.loc.first_line}"
puts"End: #{found.loc.last_line}"

# => Start: 2
#    End: 4

附:我会推荐标准库中的Ripper模块,但据我所知,没有办法从它中获得结束。


Ruby有一个source_location方法,它为您提供文件和起始行:

1
2
3
4
5
6
7
8
class Home
  def initialize(color)
    @color = color
  end
end

p Home.new(1).method(:initialize).source_location
# => ["test2.rb", 2]

要找到结束,也许要寻找下一个def或EOF。


我假设该文件包含最多一个initialize方法的定义(尽管将方法概括为搜索其他方法并不困难)并且该方法的定义不包含语法错误。任何方法都可能需要后一个假设来提取正确的行范围。

唯一棘手的部分是找到包含end的行,它是initialize方法定义的最后一行。我已经使用Kernel#eval来定位该行。每当要执行该方法时,必须谨慎行事,尽管这里eval只是试图编译(而不是执行)一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_start_end_offsets(fname)
  start = nil
  str = ''
  File.foreach(fname).with_index do |line, i|
    if start.nil?
      next unless line.lstrip.start_with?('def initialize')
      start = i
      str << line.lstrip.insert(4,'_')
    else
      str << line
      if line.strip =="end"
        begin
          rv = eval(str)
        rescue SyntaxError
          nil
        end
        return [start, i] unless rv.nil?
      end
    end
  end
  nil
end

假设我们正在搜索如下创建的文件1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
str = <<-_
class C
  def self.feline
   "cat"
  end
  def initialize(arr)
    @row_sums = arr.map do |row|
      row.reduce do |t,x|
        t+x
      end
    end
  end
  def speak(sound)
    puts sound
  end
end
_

FName = 'temp'
File.write(FName, str)
  #=> 203

我们首先搜索开始的行(在去除前导空格之后)"def initialize"。这是索引4的行。完成该方法定义的end位于索引10。因此,我们希望该方法返回[4, 10]

让我们看看这是否是我们得到的。

1
2
p get_start_end_offsets(FName)
  #=> [4, 10]

说明

变量start等于从def initialize开始的行的索引(在删除前导空格之后)。 start最初nil并且仍然nil直到找到"def initialize"行。然后将start设置为该行的索引。

我们现在寻找lineline.strip #=>"end"。这可能是也可能不是终止该方法的end。要确定它是否是eval一个字符串,其中包含从def initialize开始到刚刚找到的end行的所有行。如果eval引发SyntaxError异常,则end不会终止该方法。该例外被拯救并返回nil。如果end终止方法,eval将返回:_initialize(这是真实的)。在这种情况下,该方法返回[start, i],其中i是该行的索引。如果在文件中找不到initialize方法,则返回nil

我已将"initialize"转换为"_initialize"以取消警告(eval):1: warning: redefining Object#initialize may cause infinite loop)

查看此SO问题的两个答案,以了解为什么SyntaxError正在获救。

比较缩进

如果已知"def initialize..."总是缩进与终止方法定义的行"end"相同的量(并且两者之间没有其他行"end"缩进相同),我们可以使用该事实来获取开始和结束的行。有很多方法可以做到这一点;我将使用Ruby有点模糊的触发器操作符。这种方法可以容忍语法错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_start_end_offsets(fname)
  indent = -1
  lines = File.foreach(fname).with_index.select do |line, i|
    cond1 = line.lstrip.start_with?('def initialize')
    indent = line.size - line.lstrip.size if cond1
    cond2 = line.strip =="end" && line.size - line.lstrip.size == indent
    cond1 .. cond2 ? true : false
  end
  return nil if lines.nil?
  lines.map(&:last).minmax
end

get_start_end_offsets(FName)
  #=> [4, 10]

1文件不必仅包含代码。


Ruby源只是一个文本文件。您可以使用linux命令查找方法行号

1
grep -nrw 'def initialize' home.rb | grep -oE '[0-9]+'