valid,invalid

関心を持てる事柄について

ActiveRecord Association Extensionとwith_optionsを併用するとrubocop-railsのRails/HasManyOrHasOneDependent警告が出るので修正してみた

ActiveRecord Association extensionsでメソッドを追加する - valid,invalid で書いたActiveRecord Association Extensionだが、with_optionsと併用するとrubocop-railsRails/HasManyOrHasOneDependent copに警告されることがわかった。

with_options dependent: :destroy do
  has_many :foo do
  end
end
with_options dependent: :destroy do
  has_many :foo do
  ^^^^^^^^ Specify a `:dependent` option.
  end
end

原因調査

disableしてもよいのだがどのような原理で警告が出るのか気になったので調べて修正してみた。

github.com

対象のcopの実装を読みつつprint debugすると、Association Extensionのblockの存在有無によってS式の構造が変わることが原因のようだった。

まず、警告が出ないblockなしのケース。

with_options dependent: :destroy do
  has_many :foo
end
s(:block,                      # <= node.parent
  s(:send, nil, :with_options,
    s(:hash,
      s(:pair,
        s(:sym, :dependent),
        s(:sym, :destroy)))),
  s(:args),
  s(:send, nil, :has_many,     # <= node
    s(:sym, :foo))))

has_manyに対して:dependent optionが渡されるかどうかはnode.parentのnodeからwith_optionsを辿れば判断できる。

一方、blockありのケース。

with_options dependent: :destroy do
  has_many :foo do
  end
end

node.parentのnode下にはwith_options blockがない。

s(:block,                      # <= node.parent.parent
  s(:send, nil, :with_options,
    s(:hash,
      s(:pair,
        s(:sym, :dependent),
        s(:sym, :destroy)))),
  s(:args),
  s(:block,                    # <= node.parent
    s(:send, nil, :has_many,   # <= node
      s(:sym, :foo)),
    s(:args), nil)))

with_optionsの引数を参照するにはnode.parent.parentから辿らなければいけないのだが、node.parentが検査対象となってしまう。

# https://github.com/rubocop-hq/rubocop-rails/blob/9808efdb80078f1382da7cab8fe0b6a1917af047/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb#L64-L70
def valid_options_in_with_options_block?(node)
  return true unless node.parent

  n = node.parent.begin_type? ? node.parent.parent : node.parent

  contain_valid_options_in_with_options_block?(n)
end

修正

block付きの場合のS式にmatchしたらnode.parent.parentを参照して検証させるようにした。

+       def_node_matcher :association_extension_block?, <<~PATTERN
+         (block
+           (send nil? :has_many _)
+           (args) ...)
+       PATTERN

        def valid_options_in_with_options_block?(node)
          return true unless node.parent

-         n = node.parent.begin_type? ? node.parent.parent : node.parent
+         n = node.parent.begin_type? || association_extension_block?(node.parent) ? node.parent.parent : node.parent

          contain_valid_options_in_with_options_block?(n)
        end

rubocop-railsへの初pull requestであり雰囲気で書いているのでacceptされるかはわからない。

(2020-01-13 追記) mergeされた!

これまでrubocopの中身をちゃんと見たことがなかったので学びがあってよかった。

環境

  • rubocop 1.8.0
  • rubocop-rails 2.9.1

This article is for ohbarye Advent Calendar 2020.