我的第一个想法是使用动态添加的附加代码来执行每个感兴趣的文件,以构建方法及其位置的目录。然而,这显然行不通,因为预计几乎会立即提出例外情况。即使避免了异常,也不能保证添加的代码会被执行。此外,盲目运行代码可能会产生意想不到的不利后果。
我认为唯一合理的方法是解析感兴趣的文件。甚至可能有宝石可以做到这一点。这当然值得一试。
我已经构建了一个方法来解析文件以构建包含所需信息的哈希。使用它的主要要求是文件格式正确;具体来说,关键字class、module 和def 必须缩进与它们对应的end 关键字相同数量的空格。因此,它将错过内联定义的模块、类和方法,如下所示。
module M; end
class C; end
def im(n) 2*n end
def self.cm(n) 2*n end
如果垂直对齐有问题,肯定有 gem 可以正确格式化代码。
我选择了一个特定的哈希结构,但是一旦构建了该哈希,就可以根据需要对其进行修改。例如,我采用了层次结构“实例方法->文件->容器”(“容器”是模块、类和顶层)。可以轻松地修改该哈希以将层次结构更改为“容器->模块方法->文件”。或者,可以将信息输入数据库以保持使用方式的灵活性。
代码
以下正则表达式用于解析每个感兴趣文件的每一行。
R = /
\A # match beginning of string
(?<indent>[ ]*) # capture zero or more spaces, name 'indent'
(?: # begin non-capture group
(?<type>class|module) # capture keyword 'class' or 'module', name 'type'
[ ]+ # match one or more spaces
(?<name>\p{Upper}\p{Alnum}*) # capture an uppercase letter followed by
# >= alphanumeric chars, name 'name'
| # or
(?<type>def) # capture keyword 'def', name 'type'
[ ]+ # match one or more spaces
(?<name> # begin capture group named 'name'
(?:self\.)? # optionally match 'self.'
\p{Lower}\p{Alnum}* # match a lowercase letter followed by
# >= 0 zero alphanumeric chars, name 'name'
) # close capture group 'name'
| # or
(?<type>end) # capture keyword 'end', name 'type'
\b # match a word break
) # end non-capture group
/x # free-spacing regex definition mode
用于解析的方法如下。
def find_methods_by_name(files_of_interest)
files_of_interest.each_with_object({ imethod: {}, cmethod: {} }) do |fname, h|
stack = []
File.readlines(fname).each do |line|
m = line.match R
next if m.nil?
indent, type, name = m[:indent].size, m[:type], m[:name]
case type
when "module", "class"
name = stack.any? ? [stack.last[:name], name].join('::') : name
stack << { indent: indent, type: type, name: name }
when "def"
if name =~ /\Aself\./
stack << { indent: indent, type: :cmethod, name: name[5..-1] }
else
stack << { indent: indent, type: :imethod, name: name }
end
when "end"
next if stack.empty? || stack.last[:indent] != indent
type, name = stack.pop.values_at(:type, :name)
next if type == "module" or type == "class"
((h[type][name] ||= {})[fname] ||= []) << (stack.any? ?
[stack.last[:type], stack.last[:name]].join(' ') : :main)
end
end
raise StandardError, "stack = #{stack} after processing file '#{fname}'" if stack.any?
end
end
示例
例如,感兴趣的文件可能是某些目录中的所有文件。在这个例子中,我们只有两个文件。
files_of_interest = ['file1.rb', 'file2.rb']
这些文件如下。
File.write('file1.rb',
<<_)
def mm
end
module M
def m
end
module N
def self.nm
end
def n
end
def a2
end
end
end
class A
def self.a1c
end
def a1
end
def a2
end
end
class B
include M
def b
end
end
_
#=> 327
File.write('file2.rb',
<<_)
def mm
end
module M
def m
end
module N
def n
end
def a2
end
end
end
module P
def p
end
end
class A
include M::N
def self.a1c
end
def a1
end
end
class B
include P
def b
end
end
_
#=> 335
h = find_methods_by_name(files_of_interest)
#=> {
# :imethod=>{
# "mm"=>{
# "file1.rb"=>[:main],
# "file2.rb"=>[:main]
# },
# "m"=>{
# "file1.rb"=>["module M"],
# "file2.rb"=>["module M"]
# },
# "n"=>{
# "file1.rb"=>["module M::N"],
# "file2.rb"=>["module M::N"]
# },
# "a2"=>{
# "file1.rb"=>["module M::N", "class A"],
# "file2.rb"=>["module M::N"]
# },
# "a1"=>{
# "file1.rb"=>["class A"],
# "file2.rb"=>["class A"]
# },
# "b"=>{
# "file1.rb"=>["class B"],
# "file2.rb"=>["class B"]
# },
# "p"=>{
# "file2.rb"=>["module P"]
# }
# },
# :cmethod=>{
# "nm"=>{
# "file1.rb"=>["module M::N"]
# },
# "a1c"=>{
# "file1.rb"=>["class A"],
# "file2.rb"=>["class A"]
# }
# }
# }
为了消除只出现一次的文件,我们可以执行一个额外的步骤。
h.transform_values! { |g| g.reject { |k,v| v.size == 1 && v.values.first.size == 1 } }
这将删除实例方法p 和类方法nm。