Parser gem のバージョニングと RuboCop の TargetRubyVersion

Ruby 2.7.2 にあわせた Parser 2.7.2.0 がリリースされた。

Parser と RuboCop の繋がりは大きいので、この機会に RuboCop で解析する Ruby のバージョン指定との関係も書いておく。

Parser のバージョニング

Parser のバージョニングは、最新の安定版 Ruby のパッチバージョンに Parser としてのリリースバージョンを加えたものになる。例えば現在の最新の安定版 Ruby は 2.7 系であり、今回リリースされた Parser 2.7.2.0 は 2.7.2 に対応した最初のリリース (0オリジン) という意味。

Ruby では安定版のパッチリリースで構文へのバックポートが入る場合があるため、安定版のパッチ版リリースごとに Parser のバージョンを揃えておくと丁寧。参考としては Ruby 2.5.5 から 2.5.6 への変更で Parser への構文バックポートを入れていることがある。

github.com

この Parser のバージョニングから察せられると思うが、Parser 2.7.1.5 から Parser 2.7.2.0 にすると Ruby 2.7.1 から Ruby 2.7.2 への構文サポートに切り替わる。つまり単一の Parser のバージョンで複数の Ruby のパッチバージョン (2.7.0, 2.7.1 と 2.7.2 など) はサポート (梱包) していない。単一の Parser のバージョンがサポートしているのは複数のパッチバージョン (2.7 系, 2.6 系, 2.5 系, 2.4 系, 2.3 系...) までとなる。

RuboCop の TargetRubyVersion との関係

RuboCop との関係だが、.rubocop.yml で構文解析する Ruby のバージョンを指定する TargetRubyVersion に指定するバージョンはマイナーバージョンまでが意味を持つ。

TargetRubyVersion: 2.7

たまにパッチバージョンまで指定されているのを見るが意味がない (警告を出すようにしても良いかもしれない) 。

どういうことかというと、パッチバージョンの指定は依存している Parser のバージョンに依存する。いまどきなら Gemfile (Gemfile.lock) から依存している Parser のバージョンが 2.7.1.5 なら Ruby 2.7.1 での構文解析をし、2.7.2.0 なら Ruby 2.7.2 系での構文解析をする。

つまり RuboCop が解析する Ruby のパッチバージョンは .rubocop.yml ではなく、Gemfile (Gemfile.lock もっというとランタイムの Parser のバージョン) からのバージョンが意味を持つ。

最新でない過去の Ruby バージョンとの関係

Parser 自体は Ruby 1.8 からの 2.7 (experimental を含めると 3.0) までの構文を梱包している。つまり Parser 2.7.2.0 をインストールすると Ruby 2.7.2, 2.6.6, 2.5.8, 2.4.10, 2.3.10, さらに 2.2 以下の構文サポートが入る (実装的には MacRuby や RubyMotion の構文解析も入るようだがよく知らない) 。

執筆時点での次の Ruby 2.6 系となる Ruby 2.6.7 が将来リリースされたらリリースされる Parser はどうなるかというと、その時の最新安定版 Ruby が 2.7.2 であれば Parser 2.7.2.x となる (x の部分が繰り上がり) 。リリースされるのは Parser 2.6 系ではない点に注意。このようにちょっと複雑だったりするので、面倒だなと思う人は常に Ruby と Parser のバージョンを上げておくと概ね足並みがそろっていくと思う。

この記事を書きつつ Parser のリリースバージョンがサポートしている Rubyマトリックスのドキュメントがあると便利だと思う人がいるかもしれないと思ったけれど、自分の時間を使うには他にやることが無限にあるので、過去を紐解く時間を捻出したドキュメント化とその提案なんかが好きな人がいればお任せしたい。

ちなみに以下の PR に書いたように、今回の Ruby 2.7.1 から 2.7.2 への Parser gem としての構文解析ポーティングはないが、Ruby ランタイムバージョンと Parser の解析バージョンの不一致警告を防ぐ意味合いでもバージョンを揃えておくと気持ち的に平穏だと思う。

github.com

Kaigi on Rails STAY HOME Edition に登壇した

Kaigi on Rails STAY HOME Edition に『TDD with git. Long live engineering』というタイトルで登壇した。

kaigionrails.org

発表スライドは以下です。

Zoom での発表だったけれど、ふだん使いは Google Meets や Slack Call がメインだったりするので、メジャーなツールと知っているのとは裏腹に Zoom は個人的には不慣れなツールというあたりにバタついてしまっていた。それもあって当日、発表者のリアルタイム配信もしようとしていたけれど、そこは断念した (たぶん自分の発表風景は表示されていなかったと思う) 。Kaigi on Rails チームからは事前の接続リハーサルが別日に用意されていたので、そこで接続試験しておくべきだったようだ。オンライン時代登壇への準備不足よ、、、このあたりはいろいろと反省。

発表中はデスクトップに出ている Zoom でスタッフの yuki3738 が相槌をうってくれたりしていて、オンライン発表に付いてくる寂寞感みたいなものを払拭してくれていてありがたかったです。どうもありがとう。

本編ですが、タイトルは DHH の "TDD is dead. Long live testing." が元ネタ。

yattom.hatenablog.com

発表ネタのあたためについて少し触れると、私はふだんメモレベルでのパッチネタや CFP ネタなんかを Basecamp で管理しているのですが、どうも今年の7月2日にはストックしていたネタだったので3ヶ月越しで世に出せたようです (当時はちょっと違ったタイトルで考えていたらしい) 。世に出す良い機会をありがとうございました。

f:id:koic:20201005140817p:plain

本編裏のチャットで流れていたコメントについて、割と重要な点をスキップしてしまっていた部分をこちらの日記の方でひとつ補足。この講演で話していた git のコミット編集とは Pull Request 中のもので、master (main) に対しては編集しないのは前提。特にあとでスライドの編集などは予定していないので、本コメントでパッチに代えます。

また、本編中で「良ければご参照ください」としていた以下のページについては画像として貼っておきます。あくまで一例なのでご参考程度に。

f:id:koic:20201005132442j:plain

スケジュールの関係で懇親会への参加はできなかったのですが、kakutani さんからは以下のフィードバックをもらってました。ありがとうございます。

Kaigi on Rails はまた来年も開催されるそうで楽しみにしています。ありがとうございました。

Kaigi on Rails STAY HOME Edition に登壇します

今週末 2020年10月3日(土) に開催される Kaigi on Rails STAY HOME Edition に『TDD with git.
Long live engineering.』というタイトルで登壇します。

kaigionrails.org

私の登壇は 16:20-16:40 です。

概ね話はできていて、今回はアジャイルソフトウェア開発に寄った話です。

また、今回は同僚の yucao24hours9sako6 もそれぞれのセッションで登壇します (9sako6 は今年の新卒氏です) 。登壇3人分について詳しくは yucao24hours が勤務先のブログの方の方に書いてくれているのでどうぞ。

blog.agile.esm.co.jp

私の登壇はライブ配信を予定しています。オンラインでお会いしましょう。

RubyKaigi Takeout 2020 に登壇した

RubyKaigi Takeout 2020 に登壇した。

本編登壇は RubyKaigi 2018 以来 2 回目。去年 RubyKaigi 2019 では LT 登壇で TracePoint を使うことによって仕込んでしまったバグの話をしていた

今回のスライドは以下です。

RuboCop 1.0 に向けた話

本編から RuboCop 1.0 に向けた話をいくつかピックアップしておきます。後述の収録への問題があったため、特に終盤テンポが上がっていることからテキストとして少し補足しておきます。

拡張 API の変更

拡張 Cop の開発者向けの API は以下のように変更になります。

これまでの API:

class CustomCop < Cop
  def on_send(node)
  end

  def autocorrect(node)
  end
end

これからの API:

class CustomCop < Base
  extend AutoCorrector

  def on_send(node)
    add_offense(node) do |corrector|
    end
  end
end

従来の RuboCop::Cop::Cop はすぐに削除されるものではない soft deprecated なものですが、今後は RuboCop::Cop::Base を使うことをすすめます。RuboCop 自体もまだ移行中です。依存している Parser 実装から変わっているため、まだ予期していない不具合を持っている可能性があります。何か問題を見つけたら rubocop-hq までご連絡ください。

Safe / SafeAutoCorrect

RuboCop には SafeSafeAutoCorrect という概念があります。

Safe偽陽性を起こすことなく検出できるかどうかによって、避けることが難しい偽陽性が目立ったものは安全でない Safe: false がマークされています。

SafeAutoCorrect は振る舞いの互換性を維持できるかどうかです。こちらも RuboCop の提案による変更で非互換を起こすものは SafeAutoCorrect: false が設定されています。また偽陽性を持ったままの変更したことで壊れることを防ぐため Safe: false とされているものは SafeAutoCorrect: false です。

とりわけ最近の重要な変更点として、rubocop -a オプションの挙動変更と rubocop -A オプションの導入です。前者は安全な Cop でのみ自動修正を行うもので、後者は安全でない Cop も含めて自動修正を行うものです。

RuboCop 0.86 以前では、frozen stirng magic comment を追加する場合は rubocop -a でしたが、frozen string になったことで破壊的メソッドへの呼び出しがあるような場合など非互換の変更になる可能性があります。このような安全でない Cop での自動修正を実行したい場合は rubocop -A コマンドを使うことになります。

以下のエントリも参考にしてください。

koic.hatenablog.com

pending ステータス

もしいま使っている RuboCop がちょっと古くてアップグレードしづらいといった問題は、Cop への pending ステータスの導入での軽減を図っています。

ここ何バージョンかで RuboCop に追加された新しい Cop は 1.0 までは pending というステータスで、デフォルトでは有効になっていません。ユーザーが明示的に .rubocop.yml への Enabled: trueEnabled: false を指定してどうするかを決めるようになっています。

pending ステータスのものは RuboCop 1.0 で有効になりますので、もしそれまでにデフォルトで有効になるべきでない Cop を見かけたら RuboCop HQ までお知らせください。

本編収録にあたって

音楽 CD でライブ盤とスタジオ盤があるように、これまでふだんをライブだったとすると、今回はじめてのスタジオ盤の収録となってなかなか思うように行かなかった。その点を書き残しておく。

スライドと録音を同期させる方法として RubyKaigi チームから紹介があり、普段使いのプレゼンテーションツールが Keynote なので、Keynote での録音が手速かろうと進めていた。

実はそこでもいろいろとあって、手元の環境が古すぎて録音機能がない Keynote 6.0 (2013年) からのアップグレードというのが最初に必要だった。そしてそもそもの macOS が古いため OS のアップグレードも必要でとなり、そのあたりのセットアップに時間が使われたりした。

そのあとはスライドへの音録りになるけれど Keynote の録音機能の使い方を調べつつ覚えていった。ページ切り替えのタイミングによってやや音質が変わるのは、うまく録音できていた箇所まで戻してやり直しかひと呼吸取るためのものだった。

そしてインターネットの向こうにも人がいない PC に向かって話すということができなくて、全編テキスト起こしをするということをしていた。筋肉少女帯の『詩人オウムの世界』のギターソロはふだんアドリブで弾いているところ、譜面に起こしたという逸話をもとにしたものだったけれど、以下のようにうまくいった部分とそうでない部分があった。

  • 話し始めるにあたり、何から話していくかの流れができているのは良い
  • 急いでテキスト起こしをしたツケで、バグっているテキストをそのまま読み上げてしまっている箇所がある (Opal への説明ミスとか)

はじめての試みっぽいバグとしてテキストを読みつつもライブ感を作りたくて、流れに乗ったタイミングでテキストから外れるというものだった。どっちつかずの弊害でした。

こういったことをやっていると、タイムキーピングが疎かになって終盤、畳み掛けるようなスピードで収めに行くことになった。結果として 25分程度というオファーのところ 25分54秒だったので、最後は編集で少しカットできないかやってみようとしたけれど、ソフトの選定や使い方を覚えるまで辿り着けず断念。これは時間の中でこなせるスキルを予め獲得しているかという経験がものをいう世界線だった (そしてそのスキルは現状持ちあわせていなかった) 。 いちおう覚えたこととして、あとで編集しやすいようにまとまりのあるスライドごとに音声を収めておくと良いという知識を得ることができた。ページ切り替えのタイミングで話しているとそのパートだけを削除したいという編集ができなくなるのが理由。

あと MacBook Pro の内蔵マイクで収録していたけれど、途中音質がガラッと変わる部分があった。機材自体は変えていないので姿勢やマイク距離が変わったなどで、音の当て方が変わっていたのではと思う。このあたり音質を担保するのであればそれようの機材を使った方が良さそう (今現在は持っていないのですが) 。

以上のような失敗経験やノウハウを得る良い機会にもなったのが今回のスライド作成だった。次回はもう少しうまくできるんじゃないかなと思っている。

RubyKaigi Takeout 自体は、リモートながら RubyKaigi 感があって良かった。収録済みの配信であるもののタイムテーブルがあったのが良い舞台装置になっていたのではと思っている。勤務先ではタイムテーブルを見る会を実施したり、当日はどちらのトラックを聞こうか悩んだりできていつもながらのアクティビティを起こすきっかけにできたのは大きかったと思う。コンテンツとしては Parser を使ったトランスパイルの話がいくつかあったり、それに伴って見慣れた S 式が出てきたりと、普段の OSS 活動で備えている特異技術とリンクしたトピックなんかはとりわけ興味深く聞いていました。

楽しかったです。RubyKaigi Takeout 2020 の関係者のみなさん、ありがとうございました。

f:id:koic:20200908145328j:plain

RubyKaigi Takeout 2020 に登壇します

RubyKaigi Takeout 2020 に『Road to RuboCop 1.0』というタイトルで登壇します。

rubykaigi.org

私の登壇は初日の 2020年9月4日(金) 14:00-14:25 です。

RuboCop 1.0 に向けた最新情報でまとめています。RuboCop のライトユーザーから拡張 Cop を作っている RuboCop マニアまで、よければテイクアウトしに来てください。

また、勤務先のブログの方で少しだけ講演について触れています。

blog.agile.esm.co.jp

本講演でのスライド作成にあたっては英文レビューに @yahonda さん に多大なるご協力をいただきました。オンラインで遅い時間まで本当にありがとうございました。

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 アップグレード時などの参考にどうぞ。

不正なバイトを置換文字で置き換えるCSV.openオプション

CSV 3.1.6 がリリースされました。

github.com

CSV 3.1.6 には、CSV.open に不正なバイトを置換文字で置き換えるオプションを追加するのに送ったパッチが取り込まれているのでその紹介です。

ユースケースとして、MS Excel 向けのエンコーディング (有名な CP932) に変換して CSV 出力するに際して、例えば Rails アプリケーションのバリデーション不足などで含まれてしまった不正なバイトをハンドリングしたいケースに活用できるオプションです。

CSV.open(..., invalid: :replace)

指定しているエンコーディング (ここでは CP932) へ変換できないバイトが含まれている場合、不正なバイトを置換文字で置き換えるという:invalid => :replace オプションが File.open にあります。CSV 3.1.5 までは CSV.open では受け付けないオプションだったため、例えば以下のように指定する必要がありました。

File.open(filename, 'w', encoding: Encoding::CP932, invalid: :replace, undef: :replace) do |file|
  csv = CSV.new(file, encoding: Encoding::CP932)

  csv << ...

  csv.close
end

CSV 3.1.6 から CSV.open:invalud オプションを受け付けるようになったため、以下のように書くことができます。

CSV.open(filename, 'w', encoding: Encoding::CP932, invalid: :replace) do |csv|
  csv << ...
end

対応したパッチは以下です。

github.com

また、Encoding::InvalidByteSequenceError が起きるようなエンコーディングに対して invalid: :replace オプション指定した場合、期待に反して ArgumentError が起きる以下のようなコードの問題についても解消されています。

require 'csv'

filename = 'foo.csv'

File.open(filename, 'w', encoding: Encoding::CP932, invalid: :replace) do |file|
  CSV.open(filename, encoding: Encoding::CP932) do |rows|
    rows << ["\x82\xa0"]
  end
end

これは上記のようなコードを書いていたとしても CSV 3.1.5 までは以下のエラーになっていたものです。

% ruby /tmp/csv.rb
Traceback (most recent call last):
        10: from /tmp/csv.rb:5:in `<main>'
         9: from /tmp/csv.rb:5:in `open'
         8: from /tmp/csv.rb:6:in `block in <main>'
         7: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv.rb:658:in `open'
         6: from /tmp/csv.rb:7:in `block (2 levels) in <main>'
         5: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv.rb:1230:in `<<'
         4: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:46:in `<<'
         3: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:46:in `collect'
         2: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:47:in `block in <<'
         1: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:159:in `quote'
/Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:159:in `match?': invalid byte sequence in UTF-8 (ArgumentError)

対応したパッチは以下です。

github.com

CSV.open(..., undef: :replace)

こちらも CSV 3.1.6 で以下のように File.openundef: :replace オプションを CSV.open に指定することができるようになったので、簡潔に対応できるようになりました。

CSV.open(filename, 'w', encoding: Encoding::CP932, undef: :replace) do |csv|
  csv << ...
end

対応したパッチは以下です。

github.com

CSV gem は Gemify されているため、Ruby のアップデートと独立して bundle update できます。詳しくは以下のエントリを参照してください。

koic.hatenablog.com

ruby/csv にパッチを送った際にスピーディーなレビューをしていただいた須藤さんありがとうございました。