Как найти дублированные методы Ruby с тем же именем, но с другим кодом? - PullRequest
0 голосов
/ 02 июля 2018

Очень большая кодовая база Ruby, с которой я работаю, имеет много экземпляров дублированных методов, определенных с тем же именем, но некоторые из его кодов отличаются (что приводит к большой проблеме состояния гонки). Конечная цель - согласовать дубликаты и получить только одну версию метода с тем же именем. Сначала мне нужно найти все версии метода, которые отличаются от «контрольной» версии этого метода. Есть ли оптимальный способ поиска и поиска всех экземпляров дублированных методов с одинаковыми именами, которые отличаются от одной определенной версии?

Дублированные методы распределены по сотням разных файлов и содержатся в одном классе. По сути, это вспомогательные методы, которые должны были быть централизованы в одном файле, но вместо этого были продублированы и часто изменены, но с тем же именем метода. Сейчас мне просто нужен хороший способ найти все случаи, когда эти методы были продублированы и отличаются от того, каким должен быть метод.

Я думаю, что Rubocop ищет только дублированные имена методов, что является лишь умеренно полезным, поскольку он может найти 237 методов с одинаковыми именами, но я не знаю, сколько из этих методов являются отклонениями от моего "контрольного" метода без ручного поиска и сравнение.

Некоторые примеры метода, переопределенного в файлах в нескольких подкаталогах:

def get_field(field_name)
  return nil unless field = @global_vars.business.fields.find_by_identifier(field_name)
  field.value.present? ? field.value : nil
end

def get_field(field_name)
  @global_vars.business.fields.find_by_identifier(field_name).try(:value)
end

def get_field(field_name)
  return nil unless field @company.fields.find_by_identifier(field_name)
  field.value.present? ? field.value : nil
end

def get_field(field_name)
  @property.fields.find_by_identifier(field_name).try(:value)
end

Спасибо за вашу помощь!

1 Ответ

0 голосов
/ 06 июля 2018

Моей первой мыслью было выполнение каждого интересующего файла с добавлением дополнительного кода на лету для создания каталога методов и их местоположений. Однако это явно не сработает, поскольку можно ожидать, что исключения будут подняты практически сразу. Даже если исключений избежать, не будет никакой гарантии, что этот добавленный код будет выполнен. Кроме того, могут быть непреднамеренные неблагоприятные последствия слепого запуска кода.

Я думаю, что единственный разумный подход - это анализировать файлы, представляющие интерес. Там могут быть даже драгоценные камни, которые делают именно это. Это, безусловно, стоит искать.

Я создал метод, который анализирует файлы, чтобы создать хеш, содержащий требуемую информацию. Основное требование к его использованию - правильное форматирование файлов; в частности, ключевые слова 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...