pockestrap

Programmer's memo

RuboCop 0.51.0 のCHANGELOGを読む

RuboCop のバージョン 0.51.0 がリリースされました。

https://github.com/bbatsov/rubocop/releases/tag/v0.51.0

CHANGELOG から変更を見ていこうと思います。

破壊的変更

古い Ruby のサポート終了

RuboCop 0.51.0 から、古いバージョンのRubyのサポートが終了しました。

RuboCopに関係するRubyのバージョンには、2つの概念があります。 一つは、RuboCopを実行するのに使うRubyのバージョンです。これは、rubocopコマンドを実行した時に走るRubyインタプリタのバージョンになります。

もう一つは、RuboCopが解析を行う際に想定するRubyのバージョンです。 これは、例えばWebアプリケーションであればそのアプリケーションを実行するRubyのバージョンになるでしょうし、ライブラリであればそのライブラリがサポートする中で一番低いRubyのバージョンになります。
このバージョンは、.rubocop.yml内でTargetRubyVersionを使用して設定することが可能です。

AllCops:
  TargetRubyVersion: 2.4

今回はこの両方のバージョンが、2.1まで引き上げられました。 つまり、RuboCop 0.51.0からはバージョン2.1未満のRubyを使用してRuboCopを実行することが出来ません。
また、アプリケーションがRuby 2.1未満を使用している or ライブラリがRuby 2.1未満をサポートしている場合、古いバージョンに合わせた解析が行えなくなってしまいます。

後者の例で具体的にどのような問題が起きるのか解説します。 RuboCopにはStyle/SymbolArrayというCopが存在します。このCopは[:foo, :bar, :baz]のようなコードに対して%i[foo bar baz]と書きましょう、という指摘を出します。 この%i構文はRuby 2.0から追加されました。そのためこれまでRuboCopではこのCopはTargetRubyVersionに2.0以上が指定されている時のみ有効になっていました。
しかし、今回のアップデートからはTargetRubyVersionには2.1以上しか指定できなくなってしまったため、このCopは常に有効になります。 つまり、Ruby 1.9を対象にしているプロジェクトでもRuboCop 0.51.0を使用するとこのCopが有効になってしまいます。

このケースはStyle/SymbolArrayを無効にすれば良いだけですが、このようなCopは多く存在するため、全てをいい感じに動かすのは難しいでしょう。

Ruby 2.1未満でRuboCopを使いたい場合、Rubyをアップデートするか、RuboCop 0.50.0を使用する必要があります。 これらRuboCopがサポートしていないバージョンのRubyは、もう既に公式にサポートが終了しているので、アップデートをしたほうが良いでしょう。

新規Cop追加

Copとは、RuboCopにおいてひとつのルールを指す言葉です。例えば、「インデントが正しいかチェックする」「非推奨メソッドを使っていないかチェックする」などが1つのCopの単位になります。

この章では、0.51.0で新たに追加されたCopをひとつずつ紹介します。

Rails/UnknownEnv

このCopは、RailsアプリケーションでのRails.envtypoを検出します。

Railsアプリケーションでは、以下のようにしてproduction環境でのみ有効になるコードを書くことが出来ます。

if Rails.env.production?
  do_something_for_production
end

ですが、このproduction?メソッドをtypoしていたとしても、例外や警告は一切発生せず、常にfalseを返します。

# `production?`をtypoしているため、常にfalseに評価されてしまっている!
if Rails.env.prodcution?
  do_something_for_production
end

このようなコードは非常に発見しづらいバグを生んでしまいます。

このCopを使用すると、上記のような未知のenvironmentを検出することが出来ます。

また、stagingなどの独自のenvironmentを定義している場合、以下のようにして.rubocop.ymlで定義を追加することが出来ます。

Rails/UnknownEnv:
  Environments:
    - development
    - test
    - production
    - staging

この際、development, test, productionも書く必要があることに注意してください。

Style/StderrPuts

Rubyではメッセージを標準エラー出力に出すwarnというメソッドが定義されています。

# 標準エラー出力にメッセージが出る
warn '`foobar` is deprecated. Use `foobaz` instead of `foobar`'

上記のコードは、$stderr.putsとほぼ同等の動きをします。

このCopは$stderr.putsを使用している場合にwarnメソッドを使用するよう警告を出します。

# warn で書き換えることが出来る
$stderr.puts '`foobar` is deprecated. Use `foobaz` instead of `foobar`'

ただし、warnメソッドは、$VERBOSEフラグがnilの場合(例えば、-W0オプション付きでRubyが実行された場合)にはメッセージを出力しません。 そのため、必ずメッセージを出力したい場合にはwarnではなく$stderr.putsを使うほうが良いでしょう。

Lint/UnneededRequireStatement

threadrationalなどいくつかのライブラリは、requireをしなくても使用することが出来ます。

$ ruby -e "puts Thread" 
Thread
$ ruby -e "puts Rational" 
Rational
$ ruby -e 'p require "thread"'
false
$ ruby -e 'p require "rational"'
false

そのため、これらのライブラリへのrequireは意味のないコードです。

このCopはこのようなrequireする必要のないライブラリをrequireしているコードに警告を出します。

Lint/RedundantWithObject

Enumerable, Enumeratorにはそれぞれ#each_with_object, #with_objectが定義されています。

これらのメソッドを使うことで、任意のオブジェクトをeachに渡して実行することが出来ます。

例:

hash = [1, 2, 3].each.with_object({}) do |item, hash|
  hash[item] = item.to_s
end

p hash # => {1=>"1", 2=>"2", 3=>"3"}

このように#with_objectは便利なのですが、開発やリファクタリングの過程でコードが変更され、#with_objectが必要なくなることがあります。

# 引数でhashを受け取るようになったので、`with_object`は必要ない
def f(hash)
  [1, 2, 3].each.with_object({}) do |item|
    hash[item] = item.to_s
  end
end


hash = {}
f(hash)
p hash # => {1=>"1", 2=>"2", 3=>"3"}

ですがこのような時にwith_objectを消し忘れてしまうことがあります。

このCopは、そのようなwith_objectの消し忘れに対して警告します。

Style/CommentedKeyword

TBD

Lint/RegexpAsCondition

Rubyではif文などの条件文全体が正規表現リテラルである場合、その正規表現を暗黙的に$_と比較する、という仕様があります。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#if

$_ = 'b'
if /a/
  puts 'これは表示されない'
end

$_ = 'a'
if /a/
  puts 'これは表示される'
end

この仕様はワンライナーを書く際に便利です。

https://docs.ruby-lang.org/ja/latest/doc/spec=2frubycmd.html

$ cat animal.txt
cat
dog
cow
$ cat animal.txt | ruby -nle 'puts $_ if /^c/'
cat
cow

ですが、通常のアプリケーションを使う上ではあまり使うことのない機能でしょう。 意図しない挙動になってしまうかも知れません。

このCopは、このような条件文に正規表現リテラルが入っているコードに対して警告を出します。

Style/MixinUsage

このCopは、トップレベルに書かれたincludeに対して警告を出します。

# 警告が出る
include FooModule

class Object
  include FooModule
end

トップレベルに書かれたincludeは暗黙的なObjectに対するincludeです。 このCopでは明示的にObjectに対するincludeを書くように指摘を出します。

また、このCopは将来的に暗黙的なincludeを書くようなスタイルもサポートする予定です。

Style/DateTime

DateTimeではなくTimeDateを使うことを推奨するCopです。理由はちゃんと調べてないので、自分で考えてください。

Gemspec/OrderedDependencies

gemspec内で定義されている依存Gemの順番を揃えるCopです。

# gemがアルファベット順でソートされている。
s.add_runtime_dependency('parallel', '~> 1.10')
s.add_runtime_dependency('parser', '>= 2.3.3.1', '< 3.0')
s.add_runtime_dependency('powerpack', '~> 0.1')
s.add_runtime_dependency('rainbow', '>= 2.2.2', '< 3.0')
s.add_runtime_dependency('ruby-progressbar', '~> 1.7')
s.add_runtime_dependency('unicode-display_width', '~> 1.0', '>= 1.0.1')

なお、このCopはAuto-Correctをサポートしているため、-aオプションをつけてRuboCopを実行することで自動的にソートを行うことが可能です。

RuboCop プラグイン開発者向け

また、このリリースではRuboCop内部のAPIにも変更があります。 そのため、rubocop-rspecのようなRuboCopプラグインを作っている人は、以下の対応をする必要があります。

なお、RuboCopの設定ファイルGem(onkcopなど)に対しては特に対応は必要ありません。

NodePattern

まず、NodePatternというRuboCop内部で使用しているASTに対するパターンマッチャに破壊的な変更がありました。

RuboCop 0.50.0までは、(send _ :== nil)というパターンは以下のようなコードを生成していました。

node.send_type? &&
  children.size == 3 &&
  children[1] == :== &&
  children[2] == nil

ここで、最後の行のchildren[2] == nilに注目してください。これは、ASTの3番目(1-originで)の要素が"存在しない"ことを意味します。 この仕様が原因で、今までは"nilリテラルが渡された"ことを検査することが出来ませんでした(Hackな方法を使えば出来たのかも知れないけど)。

RuboCop 0.51.0 からは、先程のパターンは以下のようなコードに変換されます。

node.send_type? &&
  children.size == 3 &&
  children[1] == :== &&
  children[2].nil_type?

これは"nilリテラルが渡された"ことを意味します。

また、今までのようなASTの要素が存在しないことを示したい場合、(send _ :== nil?)のようにnil?メソッドの呼び出しとして実装する必要があります。

そのため、多くのケースでは既存のNodePatternを使っているコード内のnilnil?に書き換える必要があるでしょう。

例として、rubocop-rspecではこの変更が既に行われているため、参考にしてください。 https://github.com/backus/rubocop-rspec/pull/477

add_offense

また、RuboCop 0.51.0からはadd_offenseメソッドの書き方が変更されました。

0.50.0まではadd_offenseメソッドは次のように使用していました。

add_offense(node, :selector, message(node.children.first))

ですが、RuboCop 0.51.0からは通常の引数ではなくキーワード引数を使用する様に変更されました。

add_offense(node, location: :selector, message: message(node.children.first))

なお、従来のスタイルのadd_offenseも現状は使用することが出来ますが、次のリリースで削除される予定です。 そのため、RuboCopプラグインを作っている場合はなるべくはやく対応をした方が良いでしょう。

名前の変更

今回のアップデートでは、Lint/LiteralInConditionLint/LiteralAsConditionに変更されました。 もし.rubocop.ymlLint/LiteralInConditionの記載がある場合には名前の変更が必要です。 この変更は、mryを使うことで自動的に修正することが可能です。 https://github.com/pocke/mry

$ gem install mry
$ mry .rubocop.yml --target 0.51.0

また、最新のmryからは--fromオプションを使用することで、指定したリリースから追加されたCopの一覧を.rubocop.ymlに追記することが可能です。

# RuboCop 0.51.0でのリネームが反映され、0.51.0で新規追加されたCopが.rubocop.ymlに追記される
$ mry .rubocop.yml --target 0.51.0 --from 0.50.0

まとめ

この記事は以上になりますが、RuboCop 0.51.0ではこの他にも多くの機能追加、バグ修正が行われています。 より詳しい変更を知りたい方は、リリースノートをご覧ください。

Release RuboCop 0.51 · bbatsov/rubocop

過去のリリース