Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyコードのリンティング徹底解説 ― RuboCopとStandardRBの使い分け

リンティング(Linting)とは、コードを静的に解析し、潜在的な問題を発見するプロセスのことです。

「何が問題なのか」の定義は、プログラミング言語によって異なるだけでなく、同じ言語内でもプロジェクトごとに変わることがあります。これらの問題は、大きく以下のカテゴリに分類できます。

  • プログラム的な問題(Programmatic)
  • セキュリティ上の問題(Security)
  • スタイル上の問題(Stylistic)
  • パフォーマンス上の問題(Performance)

それでは、それぞれの具体例を見ていきましょう。

スタイル上の問題

コードのスタイルに「唯一の正解」は存在せず、読み手の好みの問題です。ただし、重要なのは一貫性です。よく議論になるポイントには、次のようなものがあります。

  1. ダブルクォート vs シングルクォート
  2. タブ vs スペース
  3. 1行の最大文字数
  4. 複数行にわたるメソッド呼び出しのインデント(下記参照)
# 常に1行で書く
foo(:a, :b, :c)
 
# 最初の引数に揃える
foo(:a,
    :b,
    :c
)
 
# メソッド名に揃える
foo(
  :a,
  :b
)

これらは完全に主観的なものですが、プロジェクトごとに基準を合意しておくことで、コードベース全体の一貫性を保つことができます。

プログラム的な問題

ここには、次のような問題が含まれます。

  • 極端に長いメソッド本体:可読性や保守性の低下につながります
  • 循環的複雑度(Cyclomatic Complexity):コードの複雑さを測るために広く使われる指標です
  • 条件式内での代入:if x = trueと書いた場合、おそらくif x == trueの書き間違いでしょう。仮に意図的な代入だとしても、直感的ではない書き方です

セキュリティ上の問題

一部の関数や実装方法には、開発者が気づきにくいセキュリティ上のリスクが潜んでいます。

たとえばRubyでは、Kernel#openはファイルや外部URLを開ける柔軟なメソッドですが、同時に任意のファイルシステムアクセスも許してしまいます。open("| ls")のような奇妙な呼び出しが可能なのです。そのため、開発者に警告を表示し、より安全な方法(File#openIO.popenURI.parse#open)を使うか、あるいは自己責任で現状の挙動を維持するかを意識的に判断できるようにするのが賢明です。

パフォーマンス上の問題

Rubyの内部動作には、文脈によってどの選択肢がより高速になるかが変わるような細かなポイントが数多くあります。

リンターがこうした点について警告してくれることで、プログラムの細部を最適化しながら、自然と学ぶこともできます。

たとえば、Ruby 2.5で導入されたString#delete_suffixは、文字列の末尾から部分文字列を削除するメソッドです。次の2行は等価ですが、後者の方が汎用的な正規表現マッチに依存しないため、より高性能です。

str = 'string_with_suffix'
 
# 悪い例
str.gsub(/suffix\z/, '')
 
# 良い例
str.delete_suffix('suffix')

自動修正機能

リンターの重要な特徴のひとつが、検出した問題の一部またはすべてを自動的に修正できる点です。

行の長さなどのスタイル面は自動化しやすいため、その負担を開発者から取り除くのは理にかなっています。一方、巨大なメソッドのリファクタリングのように、主観的な判断や人間の介入が必要な問題については、自動化は不可能です。

規約か、設定か

どのルールを採用すべきかについては、コミュニティやプロジェクト内で激しい議論が交わされることが少なくありません。

従来の解決策は、チームごとにリンティングルールを自由に設定できるようにし、メンバー間で議論を解決してもらうというものでした。しかし近年、複数の言語にわたって「単一の標準規約へ統一する」動きが広がっています。

すべての場所で強制されているわけではありませんが、基本的な考え方は、コードスタイルに関する開発者の精神的な負荷を完全に取り除くことです。「最適な行長は何文字か」などを議論する代わりに、誰もがコミュニティで合意されたルールを使うだけ、というシンプルさを目指します。

Rubyにおいてこれは、既存の2つのリンターにおおよそ対応しています。柔軟な設定を可能にするRuboCopと、逆のアプローチとして共通の標準を定義するStandardRBです。

RuboCop

RuboCopは、ドキュメント化されたルールセットを提供する一般的なアプローチを採用しています。各ルールは特定の問題を検出します。開発者は自分のプロジェクト内で、特定のルールを無効化したり調整したりできます。

# 許容される最大行長を設定
Layout/LineLength:
  Max: 80
 
# 循環的複雑度のチェックを無効化
Metrics/CyclomaticComplexity:
  Enabled: false

すでに妥当なデフォルト設定が含まれているため、設定が必要になるのは、変更したい特定のルールだけです。

bundle exec rubocopを実行すると、RuboCopがコードベース全体を解析し、検出したすべての問題を一覧表示します。

# test.rb
def badName
  if something
    return "inner result"
  end
 
  "outer result"
end
$ bundle exec rubocop
Inspecting 1 file
C
 
Offenses:
 
test.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
def badName
^
test.rb:1:5: C: Naming/MethodName: Use snake_case for method names.
def badName
    ^^^^^^^
test.rb:2:3: C: [Correctable] Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
  if something
  ^^
test.rb:3:12: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
    return "inner result"
           ^^^^^^^^^^^^^^
test.rb:6:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "outer result"
  ^^^^^^^^^^^^^^
 
1 file inspected, 5 offenses detected, 4 offenses auto-correctable

その後、bundle exec rubocop --auto-correctを実行すれば、設定に従って大多数の問題が自動修正されます。

さらに、CIパイプラインにbundle exec rubocopを組み込めば、リンティングルールを満たさないコードがマージされるのを防ぐことができます。

StandardRB

StandardRBは比較的新しいプロジェクトで、実際には内部でRuboCopを使用しています。

StandardRBの主な目的は、まったく別のリンターを作ることではなく、「誰もがそのまま使える標準」に到達することです。議論の種ではなく、みんなが従う規約として。

最初に発表されたライトニングトークでは、その動機が非常に明確に語られています。構文の細部について議論する時間を減らし、コミュニティ全体の合意に従うようにすれば、本当に重要なこと――優れたプロダクトやライブラリを作ること――にもっと多くの時間を使えるようになる、というわけです。

内部でRuboCopを使っているため、出力形式も同じです。唯一の違いは、ルールを一切カスタマイズできないことです。

StandardRBは最近1.0.0に到達しました。つまり、どのルールを採用すべきかという議論の大部分は、すでにイシューページで行われています。特定のルールに賛成できない場合でも、そこに関連する議論が見つかる可能性が高いでしょう。

結局のところ、あらゆる論点が十分に議論されたと信頼できます。コミュニティ全体が100%合意するのは不可能です。このアプローチの哲学は、人は柔軟であり、反対しつつも決定にコミットできる(disagree and commit)というものです。

まとめ

過去のプロジェクトでリンティングルールの細部にこだわり、恥ずかしくなるほどの時間を費やした経験から、私はStandardRBのアプローチの価値を実感しており、可能な限り採用をおすすめします。

一貫性のメリットをすべて享受しながら、議論のオーバーヘッドを排除できることに加え、ほとんどのルールに対する自動修正まで備えています。これにより、私たちはより効率的により良いソフトウェアを提供し、本当に重要なことに集中できるのです。

他の言語でも、同様の低設定志向のコードフォーマッタが採用されつつあります。Elixirのmix formatterやRustのrustfmtはある程度の設定を許容していますが、コミュニティは驚くほど前向きに標準を受け入れています。

とはいえ、皮肉なことにこの考え方に同意できないのであれば、RuboCopも依然として十分に有効な選択肢です。

P.S. Ruby Magicの記事を公開と同時にお読みになりたい方は、Ruby Magicニュースレターを購読してください。記事を見逃すことはありません!

  1. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端

  2. Rubyでの静的分析入門!parser gemでメソッド定義を抽出する方法

    ソースコードを解析して、すべてのメソッドがどこで定義され、どんな引数を受け取るのかを把握したいと思ったことはありませんか? どうすれば実現できるのでしょうか? 最初に思いつくのは、正規表現(regexp)を書くことかもしれません。 しかし、もっと良い方法があるとしたらどうでしょう? 答えは「あります」! 静的解析(Static Analysis)とは、ソースコードそのものから情報を抽出するためのテクニックです。 これは、ソースコードをトークンへと変換する(パースする)ことで実現されます。 それでは早速見ていきましょう! parser gemを使う Rubyには標準ライブラリとしてRipper