ということで RubyKaigi 2020 に pocke さんと登壇します。去年は LT での登壇だったので、本編登壇は 2018 年以来 2 年ぶり 2 回目です。よろしくお願いします。
Code Climate Test ReporterとSimpleCov 0.18で起きるエラーを回避する
昨日あたりから RuboCop の master で CI が落ちていて、見てみたら Code Climate の cc-test-reporter でエラーが起きていることが原因だった。
$ #!/bin/bash -eo pipefail ./tmp/cc-test-reporter before-build COVERAGE=true bundle exec rake spec ./tmp/cc-test-reporter format-coverage --output tmp/codeclimate.$CIRCLE_JOB.json Starting test-queue master (/tmp/test_queue_157_5140.sock) ==> Summary (2 workers in 36.1115s) [ 1] 8753 examples, 0 failures, 9 pending 254 suites in 36.1034s (pid 160 exit 0 ) [ 2] 6772 examples, 0 failures, 2 pending 239 suites in 36.1054s (pid 161 exit 0 ) Coverage report generated for RSpec, rspec-1, rspec-2, rspec-fork-163, rspec-fork-164, rspec-fork-165 to /home/circleci/project/coverage. 21266 / 21459 LOC (99.1%) covered. Error: json: cannot unmarshal object into Go struct field input.coverage of type []formatters.NullInt Usage: cc-test-reporter format-coverage [coverage file] [flags] Flags: --add-prefix string add this prefix to file paths -t, --input-type string type of input source to use [clover, cobertura, coverage.py, excoveralls, gcov, gocov, jacoco, lcov, simplecov, xccov] -o, --output string output path (default "coverage/codeclimate.json") -p, --prefix string the root directory where the coverage analysis was performed (default "/home/circleci/project") Global Flags: -d, --debug run in debug mode Exited with code exit status 255
https://circleci.com/gh/rubocop-hq/rubocop/83619
調べたところ SimpleCov 0.18 のリリースがリリースされており、cc-test-reporter がうまく組み合っていないというイシューが開かれていた。
PR がパスしているかどうかが分からないのは不便なので、対応までのワークアラウンドとして Gemfile に SimpleCov 0.18 未満を指定して乗り切ることにした。
-gem 'simplecov', '~> 0.10' +# Workaround for cc-test-reporter with SimpleCov 0.18. +# Stop upgrading SimpleCov until the following issue will be resolved. +# https://github.com/codeclimate/test-reporter/issues/418 +gem 'simplecov', '~> 0.10', '< 0.18'
キーワード引数の分離への対応にRuby 2.8.0-devを使う
先日のパッチ会で kamipo さんにもらったアドバイスを書き残しておく。
TL;DR としては表題そのまま。キーワード引数の分離への対応にRuby 2.8.0-devを使うというもの。
2.8.0-devを使えばイージーモードだけど2.7.0縛りプレイだと常人にはクリア不能のむずかしさ https://t.co/tpJGTARwAc
— Ryuta Kamizono (@kamipo) 2020年1月24日
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 method
create_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みてもどこ直せばいいかまったくといっていいほどわからんからな" の一例である。
warningみてもどこ直せばいいかまったくといっていいほどわからんからな
— Ryuta Kamizono (@kamipo) 2020年1月24日
次に 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 が以下です。
このように、キーワード引数の分離への対応には Ruby 2.8.0-dev を使った方がよりヒントを得られるケースがあるので、困った時は rbenv install 2.8.0-dev
などで master の Ruby を使ってみることを考慮してみると良いでしょう。
この問題に2ヶ月間取り組んだ kamipo さんからの知見に感謝します。
この問題に取り組んだ2ヶ月間が僕をここまで強くした
— Ryuta Kamizono (@kamipo) 2020年1月24日
次回のパッチ会は 2020年2月27日(木)です。
Ginza.rb 第79回
Ginza.rb 第79回 Rails6の新しい定数自動読み込みZeitwerkのコードを読もう!に参加した。会場はメドピアさん。
表題のとおり今回は Zeitwerk がテーマ。余談だが自分は発音できないのでカタカナ読みでツァイトヴェルクと読んでいたが、それでも読みづらいので Rails コミュニティで略称として使われている zloader (ゼットローダー) の方で読んでいたりする。
オートローダーとは Rails で require
を書かなくてもクラスが使える機構を支えているものだが、Ruby 標準の autoload を Rails が拡張した autoload が Rails 5.2 以前に択一だったもので、Rails 6 で追加されたオートローダーが Zeitwerk となる。従来のオートローダーは classic というもので Active Support が提供している。なお、Rails では Active Support が Zeitwerk に依存してる。
autoload はクラスが見つからなかったときにファイルをロードするが、Zeitwerk はその逆でファイル名をもとにクラスをロードする。なのでファイル名とクラス名のマッピングが不確かだと NameError
が起きるのでそのあたりはひとつの注意点としましょうとなっている。例としては html_parser.rb は HtmlParser
とマッピングされるため HTMLParser
としたい場合は infrection を使ったマッピングを行う。
また Zeitwerk はスレッドセーフな実装になっているので、autoload を使っていたときにレースコンディションでエラーになるようなケースが解消されるなどのメリットがあるとのこと。
実装として面白かったのはクラスの reload だが、自分だったら愚直に load
メソッド (require
と異なり同じファイルを読み直す) を使うのが第一感となるが、require
済みのファイルパスを保持する $LOADED_FEATURES
から対象のパスを reject!
で破壊的に除いて 再び setup を実行するとかマジかと思って面白かった。このコードは面白かった。印象的なことだったので2度言った。
あと TracePoint
が実装として使われていたりもするライブラリなので、TracePoint
ファンの人は読んでみると面白いと思う。
今回 willnet さんの主導で README を読んで実装のコード解説に入っていたけれど、時間内にしゅっと本筋のコードを眺めてどういったものかを解説する様はまさに匠の技だった。Zeitwerk への理解が深まりました。ありがとうございました。
Gem 開発における gemspec と Gemfile への開発 Gem の指定について
昨日の Asakusa.rb 第547回で、hsbt さんに聞いて amatsuda さんたちを交えて話していた表題について書き残しておく。先に記しておくと決定的な結論はない。
Gem を開発する際に、開発時のみに使う Gem を指定する先として gemspec を使った spec.add_development_dependency
での指定と Gemfile を使った gem
での指定がある。補足しておくとそれぞれ前者は RubyGem での指定で、後者は Bundler での指定となる。このあたりどちらが推奨されているとかあれば聞いてみようというのが質問の発端だった。
RubyGems 3.1.2 と Bundler 2.1.4 時点で、それぞれでできることは以下となる。
RubyGems
メタデータとして rubygems.org に公開できる。ただしそれを使ったアナリティクスなどをとっているわけではないみたい。
Gemfile
Gemfile に gem
指定する際にできることとして path
オプションや github
オプションに加え、group
指定などできる。加えて gemfiles に複数バージョンそれぞれ用の Gemfile を配置することで、例えば複数バージョンの Rails についてバージョンごとに Gemfile を切り替えたテストができる。Appraisals を使った複数の Gemfile 切り替えなんかもこれがベースになる。これらは gemspec ではできないこと。
結局のところ?
基本的にできることが多い Gemfile に書けば良いというスタイルもある一方で amatsuda さんのスタイルは面白くて、複数環境の Gemfile に共通するものは gemspec に指定して、環境差分がある分についてはそれぞれの Gemfile に指定することで開発依存 gem の指定を DRY にするというもの。好きな言葉を地で行っていて流石だと思った。
結論という結論は特になく、現状だと Gemfile でしか指定できないことは Gemfile をもちいて行うしかなく、あとは考え方次第のスタイルという感じではないだろうかと理解している。
ちなみに GitHub の Used by
はそれぞれが参照されていそうという感じだった。
Asakusa.rb 第547回
Asakusa.rb 第547回だった。
ちょうど RubyKaigi 2020 のプロポーザルの提出期限日に Asakusa.rb というタイミングだったので、yahonda さんや joker1007 さんにフィードバックをもらいつつ、締め切り3時間ちょっと前くらいに提出していた。提出が終わったらだいたい燃え尽きていた (レビューありがとうございました!) 。
あとは RubyGems と Bundler のメンテナーでもある hsbt さんに、gem 開発リポジトリにおける gemspec への add_development_dependency
指定と Gemfile への指定の違いについて疑問に思っていたことを聞いたりしていた。有用な話を聞けたので、別エントリとして書くと思う。
Asakusa.rb べんり。
Ruby 2.7.0 で導入された Arguments Forwarding
Ruby 2.7.0 で導入された Arguments Forwarding について、RuboCop でスタイル部署の新たな Cop として開発中なので、Cop 開発の過程で得ている知見やらを進捗を記しておきます。
Arguments Forwarding とは
def foo(...)
といった形で引数への構文拡張がされています。以下のコードを動かしてみるとイメージがつくかもしれません。
% cat arguments_forwarding.rb def foo(...) bar(...) end def bar(str) puts "hello, #{str}" end foo('world') # => hello, world
...
でイメージがつくかもしれませんが、サンプルではひとつの引数のみですが、実際は複数の引数をそのままフォワードします。
効能
pocke さんによる RuboCop への Feature Request に詳しいです。
キーワード引数の分離への警告 (Ruby 3.0 ではエラー) の回避の側面を提案に含めているのが興味深かったです。実際に検知できるケースがアプリケーションレイヤーでどの程度あるかはわかりませんが、Ruby 3.0 へのマイグレーターのひとつになるかと思い着手することにしました。
構文上の注意
Arguments Forwarding を使ったメソッド定義では引数の括弧は省略できません。
def foo ... end
以下のような構文エラーになります。
% ruby -vc foo.rb ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin17] foo.rb:1: warning: ... at EOL, should be parenthesized? foo.rb:1: syntax error, unexpected ..., expecting ';' or '\n' def foo ...
つまり以下のようなコードを Arguments Forwarding に置き換える場合は括弧が必須になります。
# Before def foo *args, **kwargs, &block end # After def foo(...) end
一方でフォワーディング先のメソッド呼び出しは括弧を省略できます。
def foo(...) bar ... end
ただし以下のような警告が発生するため括弧をつけることが好ましいでしょう。
ただし以下のような警告が発生します。追記した内容を踏まえて括弧をつけることを一考するとよいでしょう。
% ruby foo.rb foo.rb:2: warning: ... at EOL, should be parenthesized?
2020年1月15日追記
こちらについて pocke さんから括弧の有無で意味合いが変わるというフィードバックをいただく。
Parser gem でも試して見たところ以下のように結果が変わりました。pocke さん、いつもありがとうございます!
% ruby-parse -e 'def foo(...); bar ...; end' (def :foo (forward-args) (erange (send nil :bar) nil)) % ruby-parse -e 'def foo(...); bar(...); end' (def :foo (forward-args) (send nil :bar (forwarded-args)))
(追記ここまで)
また想像にかたくないですが、メソッド定義が Arguments Forwarding でない場合に、フォワード先のみ ...
にしていると構文エラーです。
def foo(*args, **kwargs, &block) bar(...) end def bar(*args, **kwargs, &block) end foo('world', num: 42)
実行した内容は以下です。
% ruby foo.rb foo.rb:2: unexpected ...
Style/ArgumentsForwarding
cop
以下で開発中の Ruby 2.7 以上を対象とした現在開発中の新しい Cop です。
pocke さんのイシューにあるとおり以下のケースを Arguments Forwarding にするとブロックまで引き渡されるという振る舞いの破壊が入ります。
# We can replace the parameter with `...`, but it will change the behavior. # Because `...` forwards block also. def foo(*args) bar(*args) end
そのためこのケースは Strict
オプションとして設定できるようにして、デフォルトでは Safe な振る舞いにするため無効にしています。
オプションを含めてここまでの説明について概ね実装ができています。何が不足してるかというとメソッド定義の (Arguments Forwarding ではない) 通常の引数がメソッド内で使われているケースで誤検知がおきます。
例えば以下のようなケースです。
def foo(*args, **kwargs, &block) bar(*args, **kwargs, &block) args.first.do_something end
式展開で使われているケースや、以下のようなコーナーケースもアウトです。
# Array リテラルでもちいたケース def foo(*args, **kwargs, &block) bar(*args, **kwargs, &block) var = [args] do_something(var) end # Hash リテラルでもちいたケース def foo(*args, **kwargs, &block) bar(*args, **kwargs, &block) h = {key: args} # key: value 両方とも do_something(h) end
以下のようにフォワーディングだけしているケースを検出するのであれば容易ですが、フォワーディングの前後でデコレーションあるいはフィルタリング処理をいれているような実践ケースが検知できなくなります。
def foo(*args) bar(*args) end
このあたりをどのように実装するかというのが現状で、variable_table
あたりを使ったなにか良い方法があるのか、地道に例外ケースを潰すのかといった調査と設計をしているが現状のステータスです。
おわりに - OSS の開発、メンテナンスについて
このような OSS 開発への活動を支援していただけるスポンサーを募集しています。
実際のところ GitHub Sponsors は想像していた以上に OSS 活動のモチベーション継続の一要因になっています。これを通じて RailsConf 2019 (at ミネアポリス) にて DHH が講演した OSS の社会構図が変わるきっかけになるといいなと思っています。