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 follows1.
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.