【问题标题】:How to find duplicated Ruby methods with the same name but different code?如何查找具有相同名称但代码不同的重复 Ruby 方法?
【发布时间】:2018-07-02 18:07:08
【问题描述】:

我正在使用的非常大的 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

感谢您的帮助!

【问题讨论】:

  • 这是一个相当模糊的问题,特别是因为您提到了“竞争条件”。方法是否定义在不同的文件中?不同的类/模块?为什么加载文件的顺序会影响“优先”的方法?你能提供一个minimal reproducible example 的问题吗?您可以尝试各种事情,但我不知道该建议什么,因为信息太少了。
  • 嘿@TomLord,重复的方法分布在数百个不同的文件中,并包含在一个类中。这些本质上是辅助方法,本应集中在一个文件中,但却一遍又一遍地重复并经常更改,但保持相同的方法名称。现在有了这个问题,我只需要一个好的方法来定位这些方法已被复制并且与方法 应该 不同的所有实例。因此,这更像是处理竞争条件的研究和规划阶段。
  • 237个同名方法在同一个类中?由于多种原因,许多方法具有相同的名称但实现不同,例如Integer#+String#+ 应该明显不同,但我不确定你为什么要在同一个类中一遍又一遍地重新定义它们。
  • 根据您的上述评论,您应该做的是编写一个包含“控制”方法的模块,然后将其包含在类中并删除所有其他引用,无论它们的实现如何
  • 你应该通过编辑你的问题来澄清,而不是试图在 cmets 中解释。

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


【解决方案1】:

我的第一个想法是使用动态添加的附加代码来执行每个感兴趣的文件,以构建方法及其位置的目录。然而,这显然行不通,因为预计几乎会立即提出例外情况。即使避免了异常,也不能保证添加的代码会被执行。此外,盲目运行代码可能会产生意想不到的不利后果。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-01-28
    • 1970-01-01
    • 2021-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-17
    • 1970-01-01
    相关资源
    最近更新 更多