Моей первой мыслью было выполнение каждого интересующего файла с добавлением дополнительного кода на лету для создания каталога методов и их местоположений. Однако это явно не сработает, поскольку можно ожидать, что исключения будут подняты практически сразу. Даже если исключений избежать, не будет никакой гарантии, что этот добавленный код будет выполнен. Кроме того, могут быть непреднамеренные неблагоприятные последствия слепого запуска кода.
Я думаю, что единственный разумный подход - это анализировать файлы, представляющие интерес. Там могут быть даже драгоценные камни, которые делают именно это. Это, безусловно, стоит искать.
Я создал метод, который анализирует файлы, чтобы создать хеш, содержащий требуемую информацию. Основное требование к его использованию - правильное форматирование файлов; в частности, ключевые слова class
, module
и def
должны иметь одинаковое количество пробелов с соответствующими им ключевыми словами end
. Поэтому он пропустит модули, классы и методы, которые определены в строке, такие как следующие.
module M; end
class C; end
def im(n) 2*n end
def self.cm(n) 2*n end
Если вертикальное выравнивание является проблемой, безусловно, есть гемы, которые правильно форматируют код.
Я выбрал конкретную хеш-структуру, но после того, как этот хеш был создан, его можно изменить по желанию. Например, я принял иерархию «методы экземпляра-> файлы-> контейнеры» («контейнеры» - это модули, классы и верхний уровень). Можно легко изменить этот хеш, изменив иерархию, скажем, «контейнер-> методы модуля-> файлы». Кроме того, можно ввести информацию в базу данных, чтобы сохранить гибкость в использовании.
Код
Следующее регулярное выражение используется для анализа каждой строки каждого интересующего файла.
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
* * Пример тысячи двадцать-шести * 1 028 *
Интересующими файлами могут быть, например, все файлы в определенных каталогах. В этом примере у нас всего два файла.
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
.