简体   繁体   中英

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?

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.

Finding the end is tricky. The best way I can think of is to use the parser 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 :

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.

Ruby has a source_location method which gives you the file and the beginning line:

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.

Ruby source is nothing but a text file. You can use linux commands to find the method line number

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. 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. I've used Kernel#eval to locate that line. Naturally caution must be exercised whenever that method is to be executed, though here eval is merely attempting to compile (not execute) a method.

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 .

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" . That is the line at index 4 . The end that completes the definition of that method is at index 10 . We therefore expect the method to return [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 is initially nil and remains nil until the "def initialize" line is found. start is then set to the index of that line.

We now look for a line line such that line.strip #=> "end" . This may or may not be the end that terminates the method. 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. If eval raises a SyntaxError exception that end does not terminate the method. That exception is rescued and nil is returned. eval will return :_initialize (which is truthy) if that end terminates the method. In that case the method returns [start, i] , where i is the index of that line. nil is returned if no initialize method is found in the file.

I've converted "initialize" to "_initialize" to suppress the warning (eval):1: warning: redefining Object#initialize may cause infinite loop )

See both answers to this SO question to understand why SyntaxError is being rescued.

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. There are many ways to do that; I will use Ruby's somewhat obscure flip-flop operator. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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