RuboCopファミリーと周辺の更新 (GW 2021編)

GW に更新のあった RuboCop 周辺のアップデートについてざっくりまとめておく。

Parser 3.0.1.1

github.com

RuboCop が依存する Ruby構文解析の Gem で、Ruby 3.0 で入った endless method definition のバグ修正と Ruby 3.1 (dev) のサポート追加をしている。

Ruby 3.0 で導入された endless method definition は以下のように = で終わる writer メソッドは定義できない。

% ruby -cve 'def foo=(foo) = @foo = foo'
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin19]
-e:1: setter method cannot be defined in an endless method definition
def foo=(foo) = @foo = foo

それについて method_name.end_with?('=') でマッチさせていたので、受け入れられるべき比較演算子 (e.g. ==) となるメソッド定義までエラーにしていたという Parser gem のバグだった。

Ruby 3.1 (dev) のサポートについては、パターンマッチの新構文となる pin オペレータをサポートするようになった。大元のサンプルから持ってくると ^ を使った式をこんな感じでパターンマッチ中で取れるようになります。夢がありそうな拡張。

Prime.each_cons(2).lazy.find_all { _1 in [n, ^(n + 2)] }.take(3).to_a
#=> [[3, 5], [5, 7], [11, 13]]

RuboCop 1.4

github.com

Ruby 3.1 (dev) をサポートしました。また、Bundler/GemVersion cop と Layout/SingleLineBlockChain cop がデフォルト無効で追加されています。

Bundler/GemVersion は Gemfile に gem 指定するときにバージョンを必須にする cop だけれど、理由がなければバージョン指定するべきではないという見解でデフォルト無効です。個人的にはデフォルト無効ならまあ (コアに入れて) いいか。。。という気持ちです。 Layout/SingleLineBlockChain cop は、example.select { |item| item.cond? }.join('-') のようなチェーンを許可しない cop で、ふつうに厳しすぎるのでデフォルト無効は妥当だと思って見ていました。余談だけれど blockchain という用語は暗号資産を連想したという bbatsov のコメントがおもしろかった。

あと、RuboCop AST とそれが依存する Parser の依存バージョンを上げているので、bundle update --conservative rubocop でアップグレードしているようであれば、bundle update --conservative rubocop rubocop-ast parser あたりの依存 gem 指定をしないと 1.14 に上がらないかもしれません。

RuboCop AST 1.5

github.com

Ruby 3.1 (dev) をサポートしています。

RuboCop Performance 1.11.3

github.com

Performance/MapCompact cop の不具合修正をして v1.11.3 まで上がりました。あと互換のない compact.mapfilter_map に手動で置換されようとしたケースを rails/rails で見かけたので、それは検出しない旨をドキュメントとテストに追記しています。

RuboCop Rails 2.10

github.com

RuboCop Rails は半年くらいぶりのリリース。古めのイシューなんかもさらって、フィードバックにもとづいた cop の安全性などの見直しを入れており、2.9 系を調整した better バージョンな感じになっていると思います。RuboCop Performance 1.11 系と同様に、Ruby 2.4 のサポートを打ち切ったのと、今後のメジャーアップデートに向けて RuboCop 1.7.0 以上への依存に更新しています。古めのバージョンを使っているようであれば、あわせてアップデートが必要です。

ご活用ください。

RuboCop Performance 1.11 をリリースした

RuboCop Performance 1.11 をリリースした。主な変更点は以下。

  • Ruby 2.4 のサポートを終了した
  • Ruby 2.7 で追加された Enumerable#filter_map への Cop を追加した

github.com

前者はそのままなので、後者について記す。

Enumerable#filter_map に対応づけられるのは、以下の2つの Cop となる。

これは当初 Performance/FilterMap cop として提案していたものだが、対象となる bad ケースのコンテキストや good ケースとの互換性に違いがあるため、別々の Cop として分割した。

Performance/SelectMap cop

github.com

この Cop は select.map のチェーンへのケースを filter_map に置き換えることを提示する。

# bad
ary.select(&:foo).map(&:bar)
ary.filter(&:foo).map(&:bar)

# good
ary.filter_map { |o| o.bar if o.foo }

振る舞いに互換性はあるが、good ケースの可読性について賛否があるのと、複雑なブロック処理については自動修正が困難だと思い自動修正を提供しておらず、手修正も大変そうということからデフォルトで無効にしている。

Performance/MapCompact cop

github.com

この Cop は map.compact のチェーンへのケースを filter_map に置き換える。このケースは good ケースになることでコードもシンプルになると思う。

# bad
ary.map(&:foo).compact
ary.collect(&:foo).compact

# good
ary.filter_map(&:foo)

以下のいずれのケースも nil を除外するが、filter_map は加えて false も除外する。このように compactfilter_map は振る舞いに互換性がないため、SafeAutoCorrect: false (rubocop -A で適用) としている。

[true, false, nil].compact              #=> [true, false]
[true, false, nil].filter_map(&:itself) #=> [true]

なお、Enumerable#filter_map! というメソッドは存在しないので、以下のケースは受け入れている。

ary.map(&:foo).compact!

Performance/SelectMap cop は好みが分かれるところだと思うが、Performance/MapCompact cop は適用可能であれば適用しておくとコードとしてもシンプルになると思う (ただし振る舞いの違いに気をつけて適材適所で) 。

いつもの新規 Cop のようにデフォルトで pending ステータスなので、NewCops: enable にしているわけでなければ、以下のように有効化が必要となる。

# .rubocop.yml
Performance/MapCompact:
  Enabled: true

今日はここまでです。ハックを続けましょう。

RuboCop Oracleをリリースした

RuboCop Oracleを実装してリリースした。

github.com

長年 RDBMSOracle (Active Record Oracle enhanced adapter) を使った運用をしているが、これは無停止リリースを行うにあたってのマイグレーションの tip を cop にして含んでおいたもの。

最初の v0.1.0 リリースで含んでいるのは Oracle/OnlineIndex cop 単品。これは CREATE INDEX の際に ONLINE オプションをついていないマイグレーションファイルを検知する cop です。

docs.oracle.com

Oracle/OnlineIndex cop

データベースのインデックス追加を行う際は ONLINE オプションを付けないと OLTP (オンライントランザクション処理) が有効にならず、オンライン稼働のままインデックス作成を行おうとするとオンライン処理とそれぞれ処理待ちになりうる (結果としてオンラインサービス影響が起きうる) 。

この Oracle/OnlineIndex cop の bad と good は以下。options: :online を付与することで OLTP が有効になる。

# bad
add_index :table_name, :column_name

# good
add_index :table_name, :column_name, options: :online

マイナス面としては OLTP を有効にしたときよりも処理が遅くなるといったことがありうるが、無停止でサービス影響なしで進めるのを第一に ONLINE オプションを付けておこうという判断をするプロジェクトには有効だと思う。

また Oracle/OnlineIndex cop のオプションとして MigratedSchemaVersion というものがある。たとえば、RuboCop Oracle 導入以前に db/migrate/202104130150_add_title_index_to_articles.rb まで本番環境に適用済みであれば、それ以前のマイグレーションファイルへの ONLINE オプションの付与を検知したくない場合に、適用済みのマイグレーションバージョンを記すことで、適用済みのマイグレーションファイルへの検出を防ぐことができる。

Oracle/OnlineIndex:
  MigratedSchemaVersion: '202104130150'

git による並列開発で、必ずしもマイグレーションバージョンのタイムスタンプが過去から未来へと流れているわけではないが、だいたいはうまくいくだろう。bin/rails db:migrate:status あるいはその内部 API を使えばさらに誤検知を防げるだろうが、静的解析の域を超えるので実装はしないことにしている。そのあたりはコードレビュー掛け合わせになるだろう。

開発雑記

RuboCop Oracle の開発構想自体はずいぶんと前からあって、一番の悩みは名前だった。Active Record Oracle enhanced adapter をもう少し前面に出した名前にするか悩んだが、gem で公開するときの名前をシンプルにしたかったのと、gem + RuboCop の世界で Oracle を冠して困るようなことはなかろうとやや大きめの名前だが RuboCop Oracle にした。 また、RuboCop Rails に含めた cop にすることも少しだけ考えたが、Oracle adapter 特化なので独自の gem にした。

そのほか、RuboCop Rails でもマイグレーションファイルへのルール検知をしたいという PR がいくつかあるが、マイグレーションファイルは適用済みかどうかの観点も重要なため取り込めていないものがある。今回の MigratedSchemaVersion はその課題に対するひとつの習作となっている。デフォルトで db/schema.rb のスキーマバージョンを活用するなど、もう少しリッチにするかもしれない (など、まだ改善の余地がある) 。

あとプロダクト自体のドキュメンテーションはもう少し整えておく予定。

Oracle x Rails でのサービス運用をしているプロジェクト向けですが、ご活用ください。

Ruby 3.0.1, 2.7.3, 2.6.7, 2.5.9 がリリースされた

Ruby 3.0.1, 2.7.3, 2.6.7, 2.5.9 がリリースされた。安定版へのメンテナンス、リリースありがとうございます。

脆弱性対応

Ruby 3.0 系から、このたび EOL になった 2.5 系まで含めると、以下3つの脆弱性への対応となるようです。

使っている Ruby のバージョンと環境によって対象となる脆弱性が異なるため、各自ひととおりのリリースと CVE アナウンスを見て対処すると良いと思います。

Ruby 2.7.3 への arguments forwarding 構文への拡張のバックポート

Ruby 2.7.3 には Ruby 3.0 から arguments forwarding 構文への拡張のバックポーティングがありました。以下のように ... の前の引数にも対応します。

% cat example.rb
def foo(arg1, arg2, ...)
  puts('## arg1')
  puts(arg1)

  puts('## arg2')
  puts(arg2)

  puts('## ...')
  puts(...)
end

foo('foo', 'bar', 'baz', 'qux')

% ruby example.rb
## arg1
foo
## arg2
bar
## ...
baz
qux

Parser gem に対応 PR を開いておいたので、近いうちに RuboCop でも解析可能になる予定です。

github.com

2021年4月6日同日追記

Parser gem 3.0.1.0 としてリリースされました。ご活用ください。

rubygems.org

RuboCop 1.12.1 がリリースされた

先月のパッチ会で @osyo-manga さんが、Fukuoka.rb 200回 LT 大会 (だったと思う) あたりで話題に上がっていたらしい RuboCop のバグに関するパッチ話を持ってきてくれて、その流れで後日 PR を開いてくれた以下の修正パッチが目玉。

github.com

問題としては、日本語のようなマルチバイト圏で、RuboCop の警告ハイライト範囲に誤差が出るというものだった。問題のサンプルとして、以下のコード例に対する修正される前と後の振る舞いです。

% cat example.rb
{
  "あいうえお" => value,
  "かきくけこ"   => value
}

Before

マルチバイト文字が関わった警告ハイライトがずれている。

% rubocop -v
1.12.0

% rubocop example.rb
Inspecting 1 file
C

Offenses:

example.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
{
^
example.rb:2:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "あいうえお" => value,
  ^^^^^^^
example.rb:3:3: C: [Correctable] Layout/HashAlignment: Align the keys of a hash literal if they span more than one line.
  "かきくけこ"   => value
  ^^^^^^^^^^^^^^^^^^
example.rb:3:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "かきくけこ"   => value
  ^^^^^^^
example.rb:3:13: C: [Correctable] Layout/SpaceAroundOperators: Operator => should be surrounded by a single space.
  "かきくけこ"   => value
            ^^

1 file inspected, 5 offenses detected, 5 offenses auto-correctable

After

マルチバイト文字を考慮した警告ハイライトになっている。 (注: デザインの関係でこの記事ではずれていますが、ターミナルで期待しているハイライトがされていることを確認できます。)

% rubocop -v
1.12.1

% rubocop example.rb
Inspecting 1 file
C

Offenses:

example.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
{
^
example.rb:2:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "あいうえお" => value,
  ^^^^^^^^^^^^
example.rb:3:3: C: [Correctable] Layout/HashAlignment: Align the keys of a hash literal if they span more than one line.
  "かきくけこ"   => value
  ^^^^^^^^^^^^^^^^^^^^^^^
example.rb:3:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "かきくけこ"   => value
  ^^^^^^^^^^^^
example.rb:3:13: C: [Correctable] Layout/SpaceAroundOperators: Operator => should be surrounded by a single space.
  "かきくけこ"   => value
                 ^^

1 file inspected, 5 offenses detected, 5 offenses auto-correctable

Unicode::DisplayWidth gem というキャラクターの文字表示幅を見てくれる gem があって、RuboCop ではそういったマルチバイト文字の処理を行う際に使っているのですが、ユーザーに表示するフォーマッター部分で使われていなかったことが原因でした。

rubygems.org

@osyo-manga さんパッチありがとうござました!

そのほか、RuboCop 1.12.1 の変更履歴は以下です。

github.com

それではハックを続けましょう。

社内向けに「達人プログラマーの基礎技術勉強会」を行った

5年くらい前にアジャイルジャパンの長崎サテライトで登壇した内容を、当時と組織メンバーの顔ぶれも変わっていることもあり社内向けに話した。

www.slideshare.net

参加メンバーからは「保守はバグ修正ではなく機能開発であり、テストがそれを支えることを (改めて) 知れた」「見積りによるブランチ戦略を知れた」など、「バージョン管理」「テスティング」「プロジェクト自動化」の三本柱を背景にした、見積りと計画づくりとブランチ戦略の繋がりを伝えることができて良かったのではと思う。

ネタ本を少し紹介。

『ソフトウェア開発 55の真実と10のウソ』は、ロバート・L・グラスが記したソフトウェア工学の本。書籍タイトルが若干トンデモ感あるかもしれないけれど、中身はきちんとしている。ソフトウェア工学を過去の歴史的背景を踏まえて学びたいような人におすすめ。

『達人プログラマー ソフトウェア開発に不可欠な基礎知識』は、Pragmatic Bookshelf から出版されていた「バージョン管理」「テスティング」「自動化」の3冊が一冊の訳本になったもの。git 以前のバージョン管理ツールだったり、取り扱われているツールの賞味期限は切れ気味だと思うけれど、行間のテキストは今でも通じるものがあると思う。当時の Pragmatic Bookshelf ファンたちを追体験したいような人には良いかもしれない。

『達人プログラマー』と聞いて思い浮かべるのは、こちらが多いと思うので載せておきます。

本編で話した、見積りと計画作りとブランチ戦略については密接に関連しているものの、そのあたりをリンクさせた話はあまり見かけないので、そういった実践知を共有する機会にはなったんじゃないかなと思う。

あと、相対見積りに使われるポイントには 1, 2, 3, 5, 8, 13 のフィボナッチ数を使いますが、指を立ててポイントを表現する際に「5pt より大きい見積りは手に余る」という真面目な駄洒落を言えたので満足です (実際 8pt のストーリーは手に余る) 。

Ruby のエイプリルフール新構文 2021

Downward assignments という名前で Ruby に提案された機能 (mame さん今年もありがとうございます) 。

p(2 * 3 * 7)  #=> 42
  ^^^^^var

p var         #=> 6

bugs.ruby-lang.org

エイプリルフールということを失念していて「これは!」とおもしろく思った一方で思ったのが、以下のような RuboCop のテスト機構とぶつかった場合に、テストの仕組みを考える必要があるかもしれないという点だった (がんばりましょう) 。

expect_offense(<<~RUBY)
  def func
    some_preceding_statements
    x = something
    ^^^^^^^^^^^^^ Redundant assignment before returning detected.
    x
  end
RUBY

github.com

また他の機能提案として、mrkn さんからはこんな提案が開かれていました。

irb(main):001:0> x = 3
=> 3
irb(main):002:0> 2x
=> 6
irb(main):003:0> def pi = Math::PI
=> :pi
irb(main):004:0> 2pi
=> 6.283185307179586

bugs.ruby-lang.org

エイプリルフールネタでいうと、Ruby 3.0 で正式採用された Endless Method Definition の例もあるので、気になる人はウォッチしてみましょう。

2021年4月5日追記

いずれの提案もエイプリルフール終了ということでクローズされたようです。