はじめに
こんにちは。姓は#LR_parser_gangs、名はydahです。最近は子が「となりのトトロ」にハマっており、毎日「となりのトトロ」がリビングで流れています。 全く飽きないのか、毎日のように「となりのトトロ観る〜?(意訳:となりのトトロが観たいのでリモコンを操作して欲しい)」と言い続け1ヶ月が経とうとしています。 これは ANDPAD Advent Calendar 2024 3日目の記事です。
今回はRubyKaigi 2024やKaigi on Rails 2024で秘蔵のesaとして配布した、RuboCopのカスタムCop作成のための資料を公開します。 カスタムCopの作成ができると、自社のコーディング規約合わせた柔軟なルールを定義し管理することができるようになります。
アップストリームに提案を送るのもよいですが、汎用的でない場合や、限定された状況でのみ有用なルールというのは一般的にはアップストリームには取り込まれにくいです。 また、事前に自分たちのコードに適用してみるということも容易にできるので、まずは自分たちのCopとして作成してみるのもよいでしょう。
もしアップストリームに提案を送りたいが、その前に相談したい場合にはrubocop/rubocop-jpで相談してもらうのもいいと思います。Watchしているので、RSpec、Capybara、FactoryBot、RSpec Rails向けのCopの場合は特にお手伝いできるかもしれません1。
RuboCopは、Rubyの静的解析ツール(Linter)およびコードフォーマッターです。 デフォルトでは、RuboCopチームが管理しているRuby Style Guideに準拠したガイドラインの多くが適用されます。 コードを解析して発見された問題を報告するだけでなく、RuboCop はそれらの問題を自動的に修正できます。
RuboCopには提供されているルールだけでなく、自分でカスタムルールも作成できます。 カスタムルールはカスタムCop(Custom Cop)と呼ばれ、独自のルールを作成してRuboCopに適用できます。
この記事では、RuboCopのカスタムCopを作成する方法や、実際にカスタムCopを作成する際のポイントについて解説します。
カスタムCopの置き場の作成
カスタムCopを作成するためには、まずカスタムCopの置き場を作成する必要があります。 カスタムCopの置き場は、Railsアプリと同じリポジトリで管理する場合と、別リポジトリで管理する場合があります。 簡単にですが、それぞれの方法について説明します。
Railsアプリと同じリポジトリで管理する場合
Railsアプリと同じリポジトリで管理する場合は、lib/rubocop/cop
ディレクトリにカスタムCopを配置します2。
弊社のとあるリポジトリの場合は、lib/rubocop/cop/andpad
にカスタムCopを配置しています。
新しくルールを作成したら、それだけではRuboCopが認識しないので、.rubocop.yml
に追記する必要があります。
require: - rubocop-rails - rubocop-rspec + - ./lib/rubocop/cop/andpad/your_custom_cop +Andpad/YourCustomCop: + Enabled: true
これはこの方式で管理している場合には、カスタムCopを作成する毎に .rubocop.yml
に追記する必要があるので注意が必要です。
別リポジトリで管理する場合
カスタムCopを別リポジトリで管理する場合は、リポジトリを作成する必要があります。 リポジトリを作成したら、テンプレートを使ってリポジトリを作成するのを推奨します。
公式が提供しているテンプレートを使用する場合
RuboCopのカスタムCopを作成するためのテンプレートがRuboCopの公式として提供されています。
以下の通りgemをインストールして、テンプレートを使ってリポジトリを作成します。
$ gem install rubocop-extension-generator $ rubocop-extension generate rubocop-foobar Creating gem 'rubocop-foobar'... MIT License enabled in config Code of conduct enabled in config Changelog enabled in config Initializing git repo in /Users/yudai.takada/tmp/rubocop-foobar create rubocop-foobar/Gemfile create rubocop-foobar/lib/rubocop/foobar.rb create rubocop-foobar/lib/rubocop/foobar/version.rb create rubocop-foobar/sig/rubocop/foobar.rbs create rubocop-foobar/rubocop-foobar.gemspec create rubocop-foobar/Rakefile create rubocop-foobar/README.md create rubocop-foobar/bin/console create rubocop-foobar/bin/setup create rubocop-foobar/.gitignore create rubocop-foobar/.github/workflows/main.yml create rubocop-foobar/LICENSE.txt create rubocop-foobar/CODE_OF_CONDUCT.md create rubocop-foobar/CHANGELOG.md Gem 'rubocop-foobar' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html create rubocop-foobar/lib/rubocop-foobar.rb create rubocop-foobar/lib/rubocop/foobar/inject.rb create rubocop-foobar/lib/rubocop/cop/foobar_cops.rb create rubocop-foobar/config/default.yml create rubocop-foobar/spec/spec_helper.rb create rubocop-foobar/.rspec create rubocop-foobar/.rubocop.yml update lib/rubocop/foobar.rb update lib/rubocop/foobar.rb update lib/rubocop/foobar/version.rb update rubocop-foobar.gemspec update rubocop-foobar.gemspec update Rakefile update Gemfile update README.md It's done! You can start developing a new extension of RuboCop in rubocop-foobar. For the next step, you can use the cop generator. $ bundle exec rake 'new_cop[Foobar/SuperCoolCopName]'
これで、カスタムCopを作成するためのリポジトリの作成が完了しました。
雛形から作成する場合
私がよく使っている雛形(GitHub ActionsやRakeタスクなどを含む)を使ってリポジトリを作成する場合は、以下のリポジトリをテンプレートリポジトリとして作成するという方法もあります。
your_extension, YourExtension, Your Extensionというファイル名とファイル内の文字列を適宜変更して使ってください。
また、サンプルとしてYourExtension/Example
というカスタムCopを作成しています。
作成を進めるにあたって不要なので、以下のファイルを削除してください。
- lib/rubocop/cop/your_extension/example.rb
- spec/rubocop/cop/your_extension/example_spec.rb
もし、動作しない場合にはお気軽にIssueを立ててください3。 これで、カスタムCopを作成するためのリポジトリの作成が完了しました。
カスタムCopの作成
カスタムCopの雛形の作成
カスタムCopを作成するためには、RuboCop::Cop::Base
を継承したクラスを作成します。
以下の例では、RuboCop::Cop::Base
を継承したRuboCop::Cop::Andpad::YourCustomCop
クラスを作成しています。
# lib/rubocop/cop/andpad/your_custom_cop.rb module RuboCop module Cop module Andpad class YourCustomCop < ::RuboCop::Cop::Base MSG = 'Your custom message' def on_send(node) return unless node.method_name == :your_method add_offense(node, message: MSG) end end end end end
もし、カスタムCopを作成するためのリポジトリを作成した場合は、以下のコマンドを実行してカスタムCopを作成できます。
$ bundle exec rake 'new_cop[Andpad/YourCustomCop]' [create] lib/rubocop/cop/andpad/your_custom_cop.rb [create] spec/rubocop/cop/andpad/your_custom_cop_spec.rb [modify] lib/rubocop/cop/foobar_cops.rb - `require_relative 'andpad/your_custom_cop'` was injected. [modify] A configuration for the cop is added into config/default.yml. Do 4 steps: 1. Modify the description of Andpad/YourCustomCop in config/default.yml 2. Implement your new cop in the generated file! 3. Commit your new cop with a message such as e.g. "Add new `Andpad/YourCustomCop` cop" 4. Run `bundle exec rake changelog:new` to generate a changelog entry for your new cop.
このコマンドによって、必要なファイルが生成されます。 カスタムCopの雛形は以下のようになります。
# frozen_string_literal: true module RuboCop module Cop module Andpad # TODO: Write cop description and example of bad / good code. For every # `SupportedStyle` and unique configuration, there needs to be examples. # Examples must have valid Ruby syntax. Do not use upticks. # # @safety # Delete this section if the cop is not unsafe (`Safe: false` or # `SafeAutoCorrect: false`), or use it to explain how the cop is # unsafe. # # @example EnforcedStyle: bar (default) # # Description of the `bar` style. # # # bad # bad_bar_method # # # bad # bad_bar_method(args) # # # good # good_bar_method # # # good # good_bar_method(args) # # @example EnforcedStyle: foo # # Description of the `foo` style. # # # bad # bad_foo_method # # # bad # bad_foo_method(args) # # # good # good_foo_method # # # good # good_foo_method(args) # class YourCustomCop < Base # TODO: Implement the cop in here. # # In many cases, you can use a node matcher for matching node pattern. # See https://github.com/rubocop/rubocop-ast/blob/master/lib/rubocop/ast/node_pattern.rb # # For example MSG = 'Use `#good_method` instead of `#bad_method`.' # TODO: Don't call `on_send` unless the method name is in this list # If you don't need `on_send` in the cop you created, remove it. RESTRICT_ON_SEND = %i[bad_method].freeze # @!method bad_method?(node) def_node_matcher :bad_method?, <<~PATTERN (send nil? :bad_method ...) PATTERN def on_send(node) return unless bad_method?(node) add_offense(node) end end end end end
カスタムCopのテストの作成
カスタムCopを作成する際には、まずテストを書いておくと便利です。 テストを書くことで、カスタムCopが正しく動作しているかを確認できます。
テストですが、GitHub Copilotを使えば凄まじく楽に書けるのでサッと書いてしまいましょう。4 テストの雛形は以下です。
# frozen_string_literal: true RSpec.describe RuboCop::Cop::Andpad::YourCustomCop, :config do let(:config) { RuboCop::Config.new } # TODO: Write test code # # For example it 'registers an offense when using `#bad_method`' do expect_offense(<<~RUBY) bad_method ^^^^^^^^^^ Use `#good_method` instead of `#bad_method`. RUBY end it 'does not register an offense when using `#good_method`' do expect_no_offenses(<<~RUBY) good_method RUBY end end
RuboCopにはRuboCopのテストコードを書きやすくするためのヘルパーメソッドがRuboCop::RSpec::ExpectOffense
として提供されています。
よく使うのは、expect_offense
とexpect_no_offenses
です。
これらは、それぞれoffenseが発生することとoffenseが発生しないことをテストするためのヘルパーメソッドです。
expect_offense
の第1引数には、offenseが発生するコードを記述します。
また、^
を使ってoffenseが発生する位置を示して、offenseメッセージを記述します。
その他に、よく使うのはexpect_correction
です。
RuboCopには違反を知らせるだけでなく、違反の自動修正をする機能もあります。
以下のようにRuboCopを実行すると違反の自動修正が行われます。
# 安全だとマークされた自動修正の実行 $ rubocop -a $ rubocop --auto-correct # 安全でない自動修正も含む実行 $ rubocop -A $ rubocop --auto-correct-all
RuboCopで違反を自動修正する場合には、expect_correction
を使ってテストを書くことができます。
# frozen_string_literal: true RSpec.describe RuboCop::Cop::Andpad::YourCustomCop, :config do let(:config) { RuboCop::Config.new } it 'registers an offense when using `#bad_method`' do expect_offense(<<~RUBY) bad_method ^^^^^^^^^^ Use `#good_method` instead of `#bad_method`. RUBY expect_correction(<<~RUBY) good_method RUBY end end
上記は、bad_method
をgood_method
に自動修正することをテストする例です。
このように、expect_correction
に自動修正後のコードを記述することで、違反の自動修正をテストできます。
テストを書くときのポイントとしては思いつく限り色んなパターンを網羅することです。 たとえば、メソッド呼び出しの場合なら、引数がある場合、引数がない場合、複数の引数がある場合などをテストすると良いでしょう。 また、レシーバーの有無や、メソッド呼び出しが複数行にまたがる場合も書いておくとより安心です。
カスタムCopの実装
ではテストが書けたら、実際にカスタムCopを実装していきましょう。
前提知識 : AST(Abstract Syntax Tree)
カスタムCopの実装は、まずはAST(Abstract Syntax Tree)を理解することが重要です。 ASTは、プログラムの構文を木構造で表現したものです。
RuboCopでは、parser
gemを使ってRubyのコードをASTに変換します5。
以下のように parser
gemをインストールして、ruby-parse コマンドを使用すれば、出力によってASTがどのように構築されているかを確認できます。
$ gem install parser $ ruby-parse -e 'puts "Hello, World!"' (send nil :puts (str "Hello, World!"))
sendはメソッド呼び出しを表し、nilはレシーバーがないことを表します。 putsメソッドは引数を1つ取るので、引数の部分にstrがあります。strは文字列を表します。
sendやstrはノードの種類を表しています。どんなノードがあるかは以下のリンクを参照してください。
前提知識 : on_xxxメソッド
RuboCopのカスタムCopを作成する際には、on_xxx
メソッドを定義する必要があります。
on_xxx
メソッドは、特定のノードが見つかったときに呼び出されるメソッドです。
たとえば、on_send
メソッドはメソッド呼び出しのノードが見つかったときに呼び出されます。
def on_send(node) # メソッド呼び出しのノードが見つかったときに呼び出される処理 end
on_send
メソッド以外にも、on_class
, on_module
, on_def
, on_defs
など、on_xxx
メソッドが用意されています。
これらのメソッドを使って、特定のノードが見つかったときに処理を行うことができます。
カスタムCopの実装
さて、ここまで来たら実際にカスタムCopを実装していきましょう。
以下の例では、on_send
メソッドを使って、特定のメソッド呼び出しを検出するカスタムCopを作成しています。
# lib/rubocop/cop/andpad/your_custom_cop.rb module RuboCop module Cop module Andpad class YourCustomCop < ::RuboCop::Cop::Base MSG = 'Use `#good_method` instead of `#bad_method`.' RESTRICT_ON_SEND = %i[bad_method].freeze def_node_matcher :bad_method?, <<~PATTERN (send nil? :bad_method ...) PATTERN def on_send(node) return unless bad_method?(node) add_offense(node) end end end end end
この例だと、bad_method
メソッド呼び出しを検出して、"Use #good_method
instead of #bad_method
.'"というメッセージを出力するカスタムCopを作成しています。
ここで新登場の要素があるのでひとつずつ説明します。
MSG
MSG
は、違反が検出されたときに表示されるメッセージです。
MSG
には、"Use #good_method
instead of #bad_method
."というメッセージを設定しています。
たとえば、メッセージを動的に生成したい場合は、add_offense
メソッドの第2引数にキーワード引数としてmessage
を渡すことで、メッセージを動的に生成できます。
これはbad_method
が複数あるようなケースに違反した箇所のメソッド名を使用してメッセージを生成する場合などに使えます。
MSG = 'Use `#good_method` instead of `%<bad_method>s`.' # (中略) on_send(node) return unless bad_method?(node) add_offense(node, message: format(MSG, bad_method: node.method_name)) end
RESTRICT_ON_SEND
RESTRICT_ON_SEND
は、on_send
メソッドを呼び出す条件を指定するための定数です。
というのも、on_send
メソッドは、すべてのメソッド呼び出しのノードに対して呼び出されるため、非常に多くのノードが対象となります。
そのため、RESTRICT_ON_SEND
を使って、特定のメソッド呼び出しのノードに対してのみon_send
メソッドを呼び出すように制限できます。
RESTRICT_ON_SEND
には、on_send
メソッドを呼び出す条件となるメソッド名を配列で指定します。
この例では、bad_method
メソッド呼び出しのノードに対してのみon_send
メソッドを呼び出すように制限しています。
RESTRICT_ON_SEND = %i[bad_method].freeze
def_node_matcher
def_node_matcher
は、ASTのノードをマッチさせるためのメソッドです。
def_node_matcher
は、第1引数にメソッド名、第2引数にマッチさせるノードのパターンを指定します。
この例では、bad_method
メソッド呼び出しのノードをマッチさせるために、def_node_matcher
を使ってbad_method?
メソッドを定義しています。
def_node_matcher :bad_method?, <<~PATTERN (send nil? :bad_method ...) PATTERN
さまざまな機能があるので、詳しくは以下のリンクを参照してください。
_
や...
などのワイルドカードや、$
を使って対象のノードのキャプチャはよく使うので、覚えておくと便利です。
_
の例を挙げると、数値リテラルのノードをマッチさせる場合は、以下のように記述します。
def_node_matcher :number_literal?, <<~PATTERN (int _) end
これなら、1
, 2
, 3
などの数値リテラルのノードをマッチさせることができます。
...
の例を挙げると、sumメソッド呼び出しのノードをマッチさせる場合は、以下のように記述します。
def_node_matcher :sum?, <<~PATTERN (send nil? :sum ...) end
これなら、sum(1, 2, 3)
であれ、sum(1, 2, 3, n)
であれ、sum
メソッド呼び出しのノードをマッチさせることができます。
$
の例を挙げると、bad_method
メソッド呼び出しの引数をキャプチャする場合は、以下のように記述します。
def_node_matcher :bad_method_with_arg?, <<~PATTERN (send nil? :bad_method $...) end
これを使って、bad_method
メソッド呼び出しの引数をキャプチャできます。
def on_send(node) bad_method_with_arg?(node) do |args| # argsには、`bad_method`メソッド呼び出しの引数が格納されている pp args # => [s(:int, 1), s(:int, 2), s(:int, 3)] add_offense(node) end end
たとえば、引数が1
, 2
, 3
の場合は、[s(:int, 1), s(:int, 2), s(:int, 3)]
という配列がargsに格納されます。
そもそもノードパターンに合わない場合は、nil
が返るので、ブロック内に処理が進まないようになっています。
つまり、bad_method
メソッド呼び出しの引数がある場合のみ、add_offense
メソッドを呼び出すようになっています。
また、def_node_search
というメソッドもあります。
def_node_search
は、def_node_matcher
と同じようにASTのノードをマッチさせるためのメソッドですが、def_node_search
は、ASTのノードを再帰的に探索します。
たとえば、bad_method
メソッド呼び出しのノードを再帰的に探索する場合は、以下のように記述します。
def_node_search :bad_method?, <<~PATTERN (send nil? :bad_method ...) end
これによって以下のようなノードにbad_method
メソッド呼び出しがあることを検出できます。
good_method(good_method(bad_method)) # ruby-parse -e 'good_method(good_method(bad_method))' # (send nil :good_method # (send nil :good_method # (send nil :bad_method)))
自動修正の実装
前述しましたがカスタムCopを作成する際には、違反を知らせるだけでなく、違反の自動修正も行うことができます。
違反を自動修正する場合にはRubocop::Cop::AutoCorrector
モジュールをextendして自動修正をする処理を実装します。
# lib/rubocop/cop/andpad/your_custom_cop.rb module RuboCop module Cop module Andpad class YourCustomCop < ::RuboCop::Cop::Base include RuboCop::Cop::AutoCorrector MSG = 'Use `#good_method` instead of `#bad_method`.' RESTRICT_ON_SEND = %i[bad_method].freeze def_node_matcher :bad_method?, <<~PATTERN (send nil? :bad_method ...) PATTERN def on_send(node) return unless bad_method?(node) add_offense(node) do |corrector| corrector.replace(node.loc.selector, 'good_method') end end end end end end
この例では、bad_method
メソッド呼び出しを検出して、good_method
に自動修正するカスタムCopを作成しています。
add_offense
メソッドのブロック内で、corrector.replace
メソッドを使って、bad_method
メソッド呼び出しをgood_method
に置換しています。
correctorはRuboCop::Cop::Corrector
クラスのインスタンスで、Parser::Source::TreeRewriter
クラスを継承しています。
そのため、Parser::Source::TreeRewriter
クラスのメソッドを使って、ASTのノードを書き換えることができます。
https://www.rubydoc.info/gems/parser/Parser/Source/TreeRewriterwww.rubydoc.info
よく使う操作について、以下に示します。
ノードを削除する場合
Parser::Source::TreeRewriter#remove
メソッドを使用して、指定したノードを削除できます。
https://www.rubydoc.info/gems/parser/Parser%2FSource%2FTreeRewriter:removewww.rubydoc.info
たとえば、以下のコードがあるとします。
object.method_name(arg)
そして、以下の通り指定したノードを削除します。
corrector.remove(node.loc.selector)
修正後のコードは次のようになります。
object.(arg)
ノードを挿入する場合
Parser::Source::TreeRewriter#insert_before
メソッドまたは Parser::Source::TreeRewriter#insert_after
メソッドを使用して、ノードの前後にコードを挿入できます。
https://www.rubydoc.info/gems/parser/Parser%2FSource%2FTreeRewriter:insert_beforewww.rubydoc.info
たとえば、以下のコードがあるとします。
object.method_name
そして、以下の通り指定したノードの前後に文字列を挿入します。
corrector.insert_before(node.loc.expression, 'before ') corrector.insert_after(node.loc.expression, ' after')
修正後のコードは次のようになります。
before object.method_name after
ノードを置換する場合
Parser::Source::TreeRewriter#replace
メソッドを使用して、指定したノードを別の内容に置き換えることができます。
https://www.rubydoc.info/gems/parser/Parser%2FSource%2FTreeRewriter:replacewww.rubydoc.info
たとえば、以下のコードがあるとします。
object.method_name
そして、以下の通り指定したノードを置換します。
corrector.replace(node.loc.expression, 'replace')
修正後のコードは次のようになります。
object.replace
このように、RuboCop::Cop::Corrector
が提供しているメソッドを活用することで、コードの違反箇所を検出するだけでなく、自動修正をするCopを作成できます。
安全な自動修正と安全でない自動修正の切り替え
RuboCopには、安全な自動修正と安全でない自動修正をマークするためのオプションがあります。
config/default.yml
にSafeAutoCorrect
を設定することで、安全な自動修正と安全でない自動修正を切り替えることができます。
デフォルトでは、SafeAutoCorrect
はtrue
になっているので、安全な自動修正が有効になっています。
なので、もし自動修正が安全でない場合には、SafeAutoCorrect
をfalse
に設定してください。
Andpad/YourCustomCop: SafeAutoCorrect: false
この設定をすることで、安全でない自動修正が有効になります。
つまり、rubocop -a
やrubocop --auto-correct
を実行したときにはその違反の自動修正がされないようになります。
また、この安全であるかそうでないかは、もうひとつのオプションであるSafe
によっても判断されます。
Safe
がfalse
の場合も、SafeAutoCorrect
と同様に安全でない自動修正が有効になります。
Andpad/YourCustomCop: Safe: false
SafeとSafeAutoCorrectの違いは、Safeはそのルールが安全であるかどうかを示す(つまり、違反そのものがFalse Positiveである可能性があるかどうかを示す)のに対して、SafeAutoCorrectはその違反の自動修正が安全であるかどうかを示すものです。
AutoCorrect: contextual
の利用
AutoCorrect: contextual
は、RuboCop v1.61 から導入された自動修正のパラメーターです。
RuboCopを通常通り実行した場合には自動修正がされますが、エディターでの編集中には自動修正がされないようにできます。
つまり、rubocop --lsp
、rubocop --editor-mode
として実行した場合、またはRuboCop::LSP.enable
が適用されている場合には自動修正がされません。
どういうときにこのパラメーターを使用するかというと、エディターでの編集中に自動修正がされると困るケースがあります。
例えば、RSpec/Focus
はRSpecで特定のテストだけを実行するために提供されている機能を使用している場合に違反として検出しますが、エディターでの編集中に自動修正がされてしまうと特定のテストだけ実行できなくなってしまいます。
# bad describe MyClass, focus: true do end # good describe MyClass do end
このような場合には、AutoCorrect: contextual
を使用してエディターでの編集中に自動修正がされないようにできます。
RSpec/Focus: AutoCorrect: contextual
このようにエディターでの編集中に自動修正がされて困るケースがある場合には、AutoCorrect: contextual
を設定してください。
Mixinの利用
RuboCopは、便利なMixinを提供しています。
たとえば、違反の範囲を指定するための便利なMixinとして、RuboCop::Cop::RangeHelp
があります。
RangeHelp
を使うと、違反の範囲を指定するための便利なメソッドが使えます。
たとえば、range_between
メソッドを使って、違反の範囲を指定できます。
# lib/rubocop/cop/andpad/your_custom_cop.rb module RuboCop module Cop module Andpad class YourCustomCop < ::RuboCop::Cop::Base include RuboCop::Cop::RangeHelp MSG = 'Use `#good_method` instead of `#bad_method`.' RESTRICT_ON_SEND = %i[bad_method].freeze def_node_matcher :bad_method?, <<~PATTERN (send nil? :bad_method ...) PATTERN def on_send(node) return unless bad_method?(node) add_offense(node) do |corrector| corrector.replace(range_between(node.loc.selector.begin_pos, node.loc.selector.end_pos), 'good_method') end end end end end end
その他にも便利なMixinがいくつか提供されている6ので、必要に応じて使ってみてください。
カスタムCopを導入する前に
ここまでは、カスタムCopを作成する方法について解説してきました。 無事テストも通れば今すぐにでも導入してしまいたくなるかもしれませんが、導入する前に確認しておくべきことがいくつかあります。
実際のコードでRuboCopを実行する
カスタムCopを導入する前に、実際のコードでRuboCopを実行してみましょう。 テストコードは通っていても、実際のコードでエラーが発生することがあります。
多くの場合は、自身の開発しているコードベースでRuboCopを実行してみることで、問題がないかを確認できます。 しかし、お手元に大きなコードベースがない場合は、real-world-ruby-appsなどのサンプルアプリケーションを使ってRuboCopを実行してみると良いでしょう。
real-worldシリーズのリポジトリは以下にあります。
- Real World Ruby Apps: https://github.com/jeromedalbert/real-world-ruby-apps
- Real World Rails: https://github.com/eliotsykes/real-world-rails
- Real World RSpec: https://github.com/pirj/real-world-rspec
それぞれのインストール方法はリポジトリのREADMEに記載されているので、それにしたがってインストールしてRuboCopを実行してみましょう。 ここでポイントがあって、カスタムCopを追加したプロジェクトのルートディレクトリに移動します。
$ cd path/to/your_project
そして、RuboCopを実行します。
$ bundle exec rubocop ../real-world-ruby-apps
RuboCopを実行すると、real-world-ruby-appsのコードベースに対してRuboCopが実行されます。 ただし、Rubyのバージョンが異なる場合は、シンタックスエラーが発生することがあるので、シンタックスエラーが発生した場合にはRubyのバージョンを合わせるか、エラーが発生したファイルを除外して実行すると良いでしょう。
カスタムCop導入時のTips
カスタムCopを追加した際には、既存のコードで違反が発生することがあります。 そのため、カスタムCopを導入するには「一緒に修正するコードをコミットする必要があるだろうか」という疑問が生じると思います。
しかし、RuboCopには.rubocop_todo.yml
というファイルがあり、このファイルには違反を無視するための設定が記述されています。
このファイルを使うことで、違反を無視できます。
このファイルは以下のコマンドを実行することで生成できます。
$ bundle exec rubocop --auto-gen-config
このコマンドを実行すると、.rubocop_todo.yml
が生成されます。
そして、.rubocop.yml
からinherit_from
を使って.rubocop_todo.yml
を読み込むように設定します。
inherit_from: .rubocop_todo.yml
このコマンドはさまざまなオプションがあります。 詳しくは以下のリンクを参照してください。
私がよく使うオプションは以下の通りです。
$ rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp --no-auto-gen-enforced-style
--no-exclude-limit
デフォルトでは、違反しているファイルが多い場合には以下のようにcop自体を無効化するようになっています。
# Offense count: 23 # Cop supports --auto-correct. Layout/BlockEndNewline: Enabled: false
このオプションを使うことで、違反しているファイルが多い場合でもcop自体を無効化しないようにします。
--no-offense-counts
生成される.rubocop_todo.yml
に違反の数を記述しないようにします。
デフォルトでは、違反の数が記述されています。
# Offense count: 23 # Cop supports --auto-correct. Layout/BlockEndNewline: Enabled: false
しかし、違反の数が記述されていると、違反の数が変わるたびに.rubocop_todo.yml
が変更されてしまうので、違反の数を記述しないようにすると良いでしょう。
--no-auto-gen-timestamp
生成される.rubocop_todo.yml
にタイムスタンプを記述しないようにします。
デフォルトでは、タイムスタンプが記述されています。
# This configuration was generated by # rubocop --auto-gen-config # on 2022-01-01 00:00:00 +0900
しかし、タイムスタンプが記述されていると、.rubocop_todo.yml
が--auto-gen-config
で再生成されるたびにタイムスタンプが変わってしまうので、タイムスタンプを記述しないようにすると良いでしょう。
--no-auto-gen-enforced-style
一部の cop は EnforcedStyle
を設定可能なオプションとして持っています。
たとえば、Style/AccessModifierDeclarations が該当します。
前述の通り--auto-gen-config
を実行して、.rubocop-todo.yml
を作成する場合、以下のどちらかの通りに違反を抑制します。
- ファイル単位で該当の cop の違反の抑制
- cop そのものを無効化する設定を追加して抑制 (規定の違反ファイル数を超過した場合)
ですが、EnforcedStyle
を持つ cop については動作が異なります。
.rubocop_todo.yml
を生成する際に、そのスタイルの設定を追加することによって違反を抑制する場合があります。
例を挙げると Style/ClassAndModuleChildren の場合、以下の2つのスタイルがあります。
EnforcedStyle: nested (default)
# good # have each child on its own line class Foo class Bar end end
EnforcedStyle: compact
# good # combine definitions as much as possible class Foo::Bar end
すべてが compact のスタイルで書かれていた場合には auto-gen-config
を実行した場合に .rubocop_todo.yml
は以下のように生成します。
Style/ClassAndModuleChildren: EnforcedStyle: compact
通常の場合には、既に統一されているスタイルがあった場合に、それを知ることができるため便利な機能です。
しかし、--auto-gen-config
しても違反が残ってしまうケースがありました。
それは Layout/SpaceInsideHashLiteralBraces の以下のようなケースです。
# test.rb
a = {
}
この場合に bundle exec rubocop --auto-gen-config
を実行すると .rubocop_todo.yml
は以下のように生成します。
# .rubocop_todo.yml # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space, compact # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: space
この状態で再度 bundle exec rubocop
を実行すると、以下の違反が残っていることがわかります。
test.rb:1:6: C: [Correctable] Layout/SpaceInsideHashLiteralBraces: Space inside empty hash literal braces detected. a = { ...
これは確かにすべてのファイルで EnforcedStyle: space
というスタイルに適合したコードなので、EnforcedStyle: space
を設定することで違反を抑制しようとします。
しかし、この cop には以下の設定オプションも存在しています。
EnforcedStyleForEmptyBraces: no_space (default)
# The `no_space` EnforcedStyleForEmptyBraces style enforces that # empty hash braces do not contain spaces. # bad foo = { } bar = { } baz = { } # good foo = {} bar = {} baz = {}
なので、実際には違反が残り続けるという現象が発生していました。
そのため、このようなケースも防げるように --no-auto-gen-enforced-style
オプションを使用することを推奨します。
その他便利なツール
rubocop-todo-regenerator
特定のラベルを PR につけると .rubocop_todo.yml
を再生成したコミットを積んでくれるカスタムアクションです。
新しいルールを追加したときはひとまず .rubocop_todo.yml
に追加してルールの追加のみを実施することが多いと思います。
ただ、大きなプロジェクトだと .rubocop_todo.yml
の再生成に時間がかかることがあります。
そこで、 ルールを追加するコミットを積んだ Pull Request を作ってしまって、このワークフローを起動します。
そうすることで GitHub Actions に.rubocop_todo.yml
の再生成のコミットを積むのを任せることができます。
ワークフローの例は以下の通りです。
# .github/workflows/rubocop-todo-regenerator.yml name: rubocop-todo-regenerator on: pull_request: types: - labeled jobs: run: runs-on: ubuntu-latest steps: - uses: ydah/rubocop-todo-regenerator@main with: github_token: ${{ secrets.WRITABLE_GITHUB_TOKEN }}
まとめ
さて、ここまでカスタムCopを作成する方法について解説してきました。 カスタムCopを作成することで、自身のプロジェクトに適したルールを追加できます。
作り方についても解説しましたが、便利なMixinなどは実際に似たようなことをやろうとしているCopがあるかどうかを調べると良いでしょう。 そのコードを参考にしてみると、より効率的にカスタムCopを作成できると思います。
少しでも本記事が皆様のカスタムCopを作成するための一助になれば幸いです。
- 著者はRuboCop RSpec Teamとして、RSpec、Capybara、FactoryBot、RSpec Rails向けのRuboCop拡張のコミッターをしています。↩
- リポジトリで他に適切な場所があればそちらに配置してもよいと思います。↩
- あまり定期的に新しいリポジトリを作成することはないので、気付いていないことが多いです。↩
- 最近使ってみたのですが、かなりいい感じにテストコードを書いてくれて感動しました。細かいところは手直しする必要があるかもしれませんが、かなり助かります。↩
- 厳密にはparser gemで構文解析後にrubocop-astというRuboCop用のAST拡張に変換しています。↩
- https://github.com/rubocop/rubocop/tree/master/lib/rubocop/cop/mixin↩