Thor x Rails 6.0での注意点

Rails アプリケーションを構築する際に、バッチ処理や便利タスクなどで Thor (現在の最新バージョンは 1.0.1) を使っている機会があると思うのですが、Rails 5.2 から Rails 6.0 で挙動が変わっていた点とその解決方法をシェアしておきます。

対象

Thor タスクのなかで options.except(:key) を使っているコードが対象です。

class Foo < Thor
  option ...
  def foo
    options.except(:key) # これ
  end
end

言い換えると Thor で options.except を使いその引数がシンボルでなければ、この問題はありません。

現象

options のクラスである Thor::CoreExt::HashWithIndifferentAccessexcept メソッドの挙動が変わっています。

Rails 5.2 まで

h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"bar"=>2}

Rails 6.0

h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"foo"=>1, "bar"=>2}

なので options.except の引数にシンボルでキー指定しても except されていない結果が返ります。

原因

https://github.com/rails/rails/pull/35771 のパフォーマンスチューニングの影響を受けてしまっていたようです。

解決

3つ例示しておきます。

1つめは場所が限られているようであれば、options.except('key') のように引数をシンボルから文字列にするのが分かりやすいワークアランドです。ただ upstream で予期されていないであろう挙動の影響でプロダクトコードに手入れするのがちょっとくやしい。

2つめは例えば config/initializers/thor.rb のようなファイルで以下のようなモンキーパッチを適用する手段です。

class Thor
  module CoreExt
    class HashWithIndifferentAccess
      def except(*keys)
        dup.tap do |hash|
          keys.each { |key| hash.delete(convert_key(key)) }
        end
      end
    end
  end
end

3つめ。以下のパッチを開いているのでマージされてリリースされるまで Gemfile に github 指定するなどできます。

github.com

昨晩直して先ほど PR を開いたばかりなので、これから動きがあることを期待しています。

Rails アップグレード時などの参考にどうぞ。