1

在我的 Ruby (on Rails) 项目中,我想禁止或限制使用标准库提供的某些方法。示例:我想禁止调用Float#to_d,因为当有人在 Float 文字上使用该方法时出现舍入错误。我想限制String#to_d只使用完全有效的字符串,因为'string'.to_d返回0.0.

在全球范围内修补/覆盖这些方法当然是个坏主意。它可能会破坏一些依赖性。

添加一个扫描代码以不调用任何#to_d方法的 linter 规则的问题是,它错误地限制了调用合法方法,如Integer#to_d. 当然,所有合法方法都可以以不同的名称添加到相应的类中。但这需要添加很多样板(用于方法)并更改这些方法的所有调用。

我还考虑过使用改进。这类似于猴子补丁,但仅适用于使用细化的范围。但是,必须using向每个文件添加语句会很丑陋且容易出错。是否可以为我的项目中的每个文件自动激活优化,但不能为依赖项激活?

4

1 回答 1

0

如果你小心的话,你可以monkeypatch。我试图实现它,看看它是否有效。显然,您会受到很大的性能影响,因此我不建议在生产中使用它:P 可以通过记住测试位置而不是每次调用该方法时都向上爬目录树来加快速度。

这个想法是如果在类上定义了原始方法,则将其移开,然后替换一个方法来检查您是否从代码中调用。我正在使用包含目录的.git目录作为您的项目目录;如果您vendor的项目目录中直接有一个目录,则它是豁免的,项目目录之外的所有内容也是如此。如果您位于豁免的位置,只需将内容传递给保存的旧方法或继承链;如果不是,请尖叫犯规。

require 'pathname'

PROJECT_CODE = Pathname.new(__dir__).ascend.find { |loc| (loc / '.git').directory? }
VENDOR_CODE = PROJECT_CODE / 'vendor'

class ForbiddenMethodError < StandardError; end

def forbid_method(klass, meth, &block)
  case
  when klass.instance_methods(false).include?(meth)
    old_meth = :"forbid_method_old_#{meth}"
    klass.alias_method old_meth, meth
  when klass.respond_to?(meth)
    old_meth = nil
  else
    raise ArgumentError, "No such method: #{klass}##{meth}"
  end

  klass.define_method(meth) do |*args|
    if !block || instance_exec(*args, &block)
      caller_loc = Pathname.new(caller_locations.first.path).expand_path
      caller_loc.ascend do |ancestor|
        case ancestor
        when PROJECT_CODE
          raise ForbiddenMethodError, "#{klass}##{meth}", caller[2..]
        when VENDOR_CODE
          break
        end
      end
    end

    if old_meth
      send(old_meth, *args)
    else
      super(*args)
    end
  end
end

有了这个,您可以在项目代码中创建Float#to_d和(有条件地)String#to_d失败:

require 'bigdecimal/util'
forbid_method(Float, :to_d)
forbid_method(String, :to_d) { !BigDecimal(self) rescue true }

如果您传递一个块,则该功能仅在该块条件为真时才被禁止。(该块将传递所有方法的参数,并将与禁止方法的接收者一起执行self。)

于 2019-07-30T10:16:08.003 回答