キーワード引数の分離への対応にRuby 2.8.0-devを使う

先日のパッチ会で kamipo さんにもらったアドバイスを書き残しておく。

TL;DR としては表題そのまま。キーワード引数の分離への対応にRuby 2.8.0-devを使うというもの。

Ruby 2.7.0 を使ってキーワード引数の分離への警告のみでそれを抑制しようとする場合は、スーパーハードモードルビーとパッチ会で呼ばれた変更箇所の特定が難しいケースになる場合がある。

スーパーハードモード (Ruby 2.7.0)

Ruby 3.0 に向けてキーワード引数の分離が必要になる場合は、Ruby 2.7.0 を使うと以下のような警告が表示される。

% ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]

% bundle exec rake
(略)

/home/vagrant/.rvm/gems/ruby-2.7.0/bundler/gems/rails-80e72c5eb7d4/activerecord/lib/active_record/migration.rb:907: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/home/vagrant/src/oracle-enhanced/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199: warning: The called method `create_table' is defined here

まず warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call となっている Active Record のコードの場所を示す。

    def method_missing(method, *arguments, &block)
      arg_list = arguments.map(&:inspect) * ", "

      say_with_time "#{method}(#{arg_list})" do
        unless connection.respond_to? :revert
          unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
            arguments[0] = proper_table_name(arguments.first, table_name_options)
            if [:rename_table, :add_foreign_key].include?(method) ||
              (method == :remove_foreign_key && !arguments.second.is_a?(Hash))
              arguments[1] = proper_table_name(arguments.second, table_name_options)
            end
          end
        end
        return super unless connection.respond_to?(method)
->      connection.send(method, *arguments, &block) # ここが `activerecord/lib/active_record/migration.rb:907`
      end
    end
    ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)

次に warning: The called methodcreate_table' is defined here` となっている Oracle enhanced adapter のコードの場所を示す。

->      def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options) # ここが `oracle_enhanced/schema_statements.rb:199`
          create_sequence = id != false
          td = create_table_definition(
            table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
          )

結論としてはどちらもキーワード引数の分離への変更対象ではない。メソッドの定義側も呼び出し側いずれのコードも期待から外れたものではなかった。

これが kamipo さんの "warningみてもどこ直せばいいかまったくといっていいほどわからんからな" の一例である。

次に Ruby 2.8.0-dev を使うと警告ではなくエラーになることでバックトレースが見えるイージーモードに切り替えてみよう。

イージーモード (Ruby 2.8.0-dev)

Ruby 2.8.0-dev を使った結果は以下となる。

Failures:
  1) OracleEnhancedAdapter schema dump tables should not include ignored
  table names in schema dump
     Failure/Error:
               def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
                 create_sequence = id != false
                 td = create_table_definition(
                   table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
                 )

                 if id && !td.as
                   pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)

                   if pk.is_a?(Array)

     ArgumentError:
            wrong number of arguments (given 2, expected 1)
     # ./lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199:in `create_table'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:907:in `block in method_missing'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:875:in `block in say_with_time'
     # /home/travis/.rvm/rubies/ruby-head/lib/ruby/2.8.0/benchmark.rb:293:in `measure'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:875:in `say_with_time'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:896:in `method_mi
     # ./spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb:24:in `block in create_test_posts_table'
     # ./spec/spec_helper.rb:121:in `instance_eval'
     # ./spec/spec_helper.rb:121:in `block (2 levels) in schema_define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:884:in `suppress_messages'
     # ./spec/spec_helper.rb:120:in `block in schema_define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:50:in `instance_eval'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:50:in `define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:46:in `define'
     # ./spec/spec_helper.rb:119:in `schema_define'
     (以下略)

Ruby 2.7.0 で表示されている警告は ArgumentError からのバックトレースの以下2行となる。そして、Ruby 2.8.0-dev では警告ではなくエラーになることでその先もバックトレースとして表示されている。

     ArgumentError:
            wrong number of arguments (given 2, expected 1)
     # ./lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199:in `create_table'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:907:in `block in method_missing'

これが示すことはバックトレースで問題の箇所を探索することができるようになるということ。この例だとバックトレース中に示されている spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb:24 が修正対象となるものだった。

これはテストコード中の以下を示している。

  def create_test_posts_table(options = {})
    options[:force] = true
    schema_define do
->    create_table :test_posts, options do |t| # ここが `oracle_enhanced/schema_dumper_spec.rb:24`
        t.string :title
        t.timestamps null: true
      end
      add_index :test_posts, :title
    end
  end

変更するべきはこのテストコードで次のようになる。

diff --git a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb b/spec/active_record/connec
index f3b1df0..8330a95 100644
--- a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb
+++ b/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb
@@ -21,7 +21,7 @@ describe "OracleEnhancedAdapter schema dump" do
   def create_test_posts_table(options = {})
     options[:force] = true
     schema_define do
-      create_table :test_posts, options do |t|
+      create_table :test_posts, **options do |t|
         t.string :title
         t.timestamps null: true
       end

これによって解決している PR が以下です。

github.com

このように、キーワード引数の分離への対応には Ruby 2.8.0-dev を使った方がよりヒントを得られるケースがあるので、困った時は rbenv install 2.8.0-dev などで master の Ruby を使ってみることを考慮してみると良いでしょう。

この問題に2ヶ月間取り組んだ kamipo さんからの知見に感謝します。


次回のパッチ会は 2020年2月27日(木)です。

blog.agile.esm.co.jp