如何找到具有相同名称但不同代码的重复Ruby方法?

时间:2018-07-02 18:07:08

标签: ruby methods grep race-condition code-duplication

我正在使用的非常大的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 :(得分:0)

我的第一个想法是执行每个感兴趣的文件,并在运行时添加其他代码,以建立方法及其位置的目录。但是,这显然是行不通的,因为预计几乎会立即引发例外情况。即使避免了异常,也无法保证会执行添加的代码。此外,盲目运行代码可能会带来意想不到的不利后果。

我认为唯一合理的方法是解析感兴趣的文件。甚至有宝石可以做到这一点。当然值得一搜。

我已经构造了一种方法,该方法可以分析文件以构建包含所需信息的哈希。使用它的主要要求是文件格式正确;具体来说,关键字classmoduledef必须与其相应的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

示例

例如,感兴趣的文件可能是某些目录中的所有文件。在此示例中,我们只有两个文件。

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