简体   繁体   English

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

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

How can I find the line of the beginning and end of a Ruby method given a ruby file? 给定一个ruby文件,如何找到Ruby方法的开始和结束行?

Say for example: 例如说:

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

Given the file home.rb and the method name initialize I would like to receive (2,4) which are the beginning and end lines. 给定文件home.rb和方法名initialize我希望接收(2,4)这是开始和结束行。

Finding the end is tricky. 寻找end是棘手的。 The best way I can think of is to use the parser gem . 我能想到的最好方法是使用解析器gem Basically you'll parse the Ruby code into an AST, then recursively traverse its nodes until you find a node with type :def whose first child is :initialize : 基本上,您将把Ruby代码解析为AST,然后递归遍历其节点,直到找到类型为:def的第一个子节点为:initialize的节点:

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

PS I would have recommended the Ripper module from the standard library, but as far as I can tell there's no way to get the end line out of it. PS我本来会推荐标准库中的Ripper模块,但是据我所知,还没有办法从中获得最终结果。

Ruby has a source_location method which gives you the file and the beginning line: Ruby有一个source_location方法,它为您提供文件和起始行:

class Home
  def initialize(color)
    @color = color
  end
end

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

To find the end, perhaps look for the next def or EOF. 要找到终点,也许要寻找下一个def或EOF。

Ruby source is nothing but a text file. Ruby源代码不过是一个文本文件。 You can use linux commands to find the method line number 您可以使用linux命令查找方法行号

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

I have assumed that the file contains the definition of at most one initialize method (though generalizing the method to search for others would not be difficult) and that the definition of that method contains no syntax errors. 我假设该文件最多包含一个initialize方法的定义(尽管将这种方法推广到其他方法并不困难),并且该方法的定义不包含语法错误。 The latter assumption is probably required for any method to extract the correct line range. 对于任何一种提取正确行范围的方法,可能都需要使用后一种假设。

The only tricky part is finding the line containing end that is the last line of the definition of the initialize method. 唯一棘手的部分是找到包含end的行,该行是initialize方法的定义的最后一行。 I've used Kernel#eval to locate that line. 我用过Kernel#eval来定位那条线。 Naturally caution must be exercised whenever that method is to be executed, though here eval is merely attempting to compile (not execute) a method. 自然地,每当要执行该方法时都必须谨慎,尽管在这里eval只是试图编译(而不执行)方法。

Code

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

Example

Suppose we are searching a file created as follows 1 . 假设我们正在搜索如下创建的文件1

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

We first search for the line that begins (after stripping leading spaces) "def initialize" . 我们首先搜索(去掉前导空格之后) "def initialize"开始的行。 That is the line at index 4 . 那就是索引4处的行。 The end that completes the definition of that method is at index 10 . 完成该方法定义的end一个索引是10 We therefore expect the method to return [4, 10] . 因此,我们希望该方法返回[4, 10]

Let's see if that's what we get. 让我们看看这是否就是我们得到的。

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

Explanation 说明

The variable start equals the index of the line beginning def initialize (after removing leading whitespace). 变量start等于开始def initialize的行的索引(在删除前导空白之后)。 start is initially nil and remains nil until the "def initialize" line is found. start最初为nil ,一直保持为nil直到找到"def initialize"行。 start is then set to the index of that line. 然后将start设置为该行的索引。

We now look for a line line such that line.strip #=> "end" . 现在,我们寻找一条line例如line.strip #=> "end" This may or may not be the end that terminates the method. 这可能是也可能不是方法终止的end To determine if it is we eval a string that contains all lines from the one that begins def initialize to the line equal to end just found. 要确定它是否是我们eval包含从一个开始的所有行字符串def initialize到线等于end才发现。 If eval raises a SyntaxError exception that end does not terminate the method. 如果eval引发SyntaxError异常,则end不会终止该方法。 That exception is rescued and nil is returned. 该异常被挽救,并且返回nil eval will return :_initialize (which is truthy) if that end terminates the method. 如果那end终止了该方法,则eval将返回:_initialize (这是事实)。 In that case the method returns [start, i] , where i is the index of that line. 在那种情况下,该方法返回[start, i] ,其中i是该行的索引。 nil is returned if no initialize method is found in the file. 如果在文件中未找到initialize方法,则返回nil

I've converted "initialize" to "_initialize" to suppress the warning (eval):1: warning: redefining Object#initialize may cause infinite loop ) 我已经将"initialize"转换为"_initialize"以禁止显示警告(eval):1: warning: redefining Object#initialize may cause infinite loop

See both answers to this SO question to understand why SyntaxError is being rescued. 查看此SO问题的两个答案,以了解为什么要挽救SyntaxError

Compare indentation 比较缩进

If it is known that "def initialize..." is always indented the same amount as the line "end" that terminates the method definition (and no other lines "end" between the two are indented the same), we can use that fact to obtain the beginning and ending lines. 如果知道"def initialize..."总是缩进与终止方法定义的"end"行相同的缩进量(并且两者之间没有其他"end"行缩进相同),我们可以使用事实,以获取开始和结束行。 There are many ways to do that; 有很多方法可以做到这一点。 I will use Ruby's somewhat obscure flip-flop operator. 我将使用Ruby有点晦涩的触发器运算符。 This approach will tolerate syntax errors. 这种方法将容忍语法错误。

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 The file need not contain only code. 1该文件不必只包含代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM