Sevencop

カスタムCop集であるSevencopの最近の様子を、各Copの紹介という体裁で振り返る。

SevencopはOpinionatedなカスタムCopを集めたもので、実験的なCopを置いたり、公式には提案しにくいようなCopを置いたりしている。幾つかのCopについては、ここで公開して運用に耐えることを幾つかのアプリで試験してから、公式にPull Requestを出して取り込んだりもしている。

Sevencop/AutoloadOrdered

autoload を定数名の辞書順に定義させるやつ。

# bad
autoload :B, 'b'
autoload :A, 'a'

# good
autoload :A, 'a'
autoload :B, 'b'

Bundler/OrderedGems のように一応空行でセクションが分割されていた場合には、セクションごとに辞書順になっていればOKということにしているが、Gemfileと比べるとセクションが必要になる機会が乏しいので、この機能は無いほうが良いかもしれない。

このCopの他にも、このGemには辞書順系のCopが幾つか存在する。

Sevencop/HashElementOrdered

Hashの要素をキーの辞書順に定義させるやつ。キーが単純なやつの場合のみ有効。ちなみにメソッド呼び出し時のキーワード引数もparser gem的にはHashとして扱われるので、これも対象になる。

# bad
{
  b: 1,
  a: 1,
  c: 1
}

# good
{
  a: 1,
  b: 1,
  c: 1
}

RubyではHashの順序は保存されるし、値の評価順序も変わってくるが、その辺りの挙動に依存したコードを書きたいとは思わないので、自分は有効化することが多い。提案順序が安定するので、Copilotと若干相性が良い。

Sevencop/MethodDefinitionArgumentsMultiline

メソッド定義時、引数が複数個存在する場合は1行あたり1引数にしようというやつ。

# bad
def foo(a, b)
end

# good
def foo(
  a,
  b
)
end

エディタやdiff、例外情報の表示など、人間向けの各種ツールが行単位での操作を軸にしていることが多いので、それらとの相性を鑑みて行単位で記載する形式を好みがちである。

Sevencop/MethodDefinitionKeywordArgumentOrdered

キーワード引数の定義時は辞書順にしようというやつ。

# bad
def foo(b:, a:); end

# good
def foo(a:, b:); end

Sevencop/MethodDefinitionOrdered

メソッドの定義時は辞書順にしようというやつ。

# bad
def b; end
def a; end

# good
def a; end
def b; end

誰が書いても同じコードになるようにしようというか、機械が書いたときに書きやすいようにしようという考え方を優先しがちかもしれない。

Sevencop/RailsBelongsToOptional

これは他とは違い、一時的に有効化してautocorrectで機械的にコードを変換するためだけに用意したCop。すべての belongs_to について未指定の場合に optional: true を付けて回る。

# bad
belongs_to :group

# good
belongs_to :group, optional: true

古くから存在するRailsアプリの多くは、belongs_to のデフォルト設定を optional: true にしたままだと思うのだけど、これだと開発者が新たに belongs_to を追加するたびに手を抜かれてしまう危険性がある。そこで、既存の belongs_to のすべてをこのように変換した上でデフォルト設定を optional: false に切り替えよう、というのがこのCopの目的。

Rails関係のCopの名前には Rails- というPrefixを付けている。Cop名の辞書順に並ぶことが多いので、似たものが近い場所に配置されるようにしようという意図で、ある程度命名に気を遣っている。

Sevencop/RailsInferredSpecType

付けなくていいtype metadataは付けないようにするやつ。

# bad
# spec/models/user_spec.rb
RSpec.describe User, type: :model

# good
# spec/models/user_spec.rb
RSpec.describe User

付けなくていいtype metadataを付けないことで、付けないといけないtype metadataが目立つようにという目的。多分このrspec-railsのInferrenceの機能は将来廃止されそうなのだけど、その際の移行の労力を減らすためにも有用だと考えている。

次のPull Requestでrubocop-rspecに追加しようと提案している。

Sevencop/RailsOrderField

MySQLのfield関数をこういう感じで利用しているアプリにおいて、ActiveRecord 6頃から入ったSQL Injection対策のためにこのようなworkaroundを入れざるを得なくなったので、それを雑に検知・修正するために用意したCop。そんなに精度も良くないのであまり使うべきでもないかも。しかしこれが役に立つ現場は確かに存在していた。

# bad
articles.order('field(id, ?)', a)

# good
articles.order(Arel.sql('field(id, ?)'), a)

ちなみに、順序を指定したい場合ActiveRecord 7からは articles.in_order_of(...) のように書くこともできるはず。

Sevencop/RailsUniquenessValidatorExplicitCaseSensitivity

これも Sevencop/RailsBelongsToOptional と似ていて、一時的に有効化してautocorrectする目的でつくったCop。

# bad
validates :name, uniqueness: true

# good
validates :name, uniqueness: { case_sensitive: true }

それまでUniquenessValidatorがMySQLのCollationを考慮する場合があったのが、しなくなるので、その挙動変更のために準備するためのもの。

Sevencop/RailsWhereNot

これもRailsの互換性対策のCop。全体的にRails prefixが付いているやつは解毒剤みたいなやつが多いので、ふつうの人は見る価値が無いかも。

# bad
where.not(key1: value1, key2: value2)

# good
where.not(key1: value1).where.not(key2: value2)

Sevencop/RequireOrdered

require の順序を辞書順にするやつ。

# bad
require 'b'
require 'a'

# good
require 'a'
require 'b'

eslint-plugin-importに似てるかもしれない。

同セクション内での並び順だけを確認するようにしている。逆コンウェイの法則じゃないけど、これによって require の順序に依存しない実装に変わることと、暗黙的な順序関係が存在する場合にセクションを分割するworkaroundによってそれが明示されるようになることを期待している。

Sevencop/RSpecExamplesInSameGroup

controller-specとrequest-specは、単なる単体テストと違ってめちゃくちゃ長い時間が掛かるので、その手のテスト種別では効率化のために同一の事前条件を持つテストケースは統合しましょうというやつ。

# bad
context 'when user is logged in' do
  it 'returns 200' do
    subject
    expect(response).to have_http_status(200)
  end

  it 'creates Foo' do
    expect { subject }.to change(Foo, :count).by(1)
  end
end

# good
context 'when user is logged in' do
  it 'creates Foo and returns 200' do
    expect { subject }.to change(Foo, :count).by(1)
    expect(response).to have_http_status(200)
  end
end