前回、RuboCopの処理を追いかけました。
長い旅になりましたが、なんとかRuboCopのコマンドが実行されてから画面に結果が出力されるまでの流れを完全に理解しました。せっかくコードを読んだわけなので何かしらの変更が出来ないか試してみたいと思います。
以前、知人からRuboCopにはカスタムCopを設定できるという話を聞いたので、カスタムCopならぬ本体へのCopの追加をやってみました。
今回、追加するCopは「変数名にp
が使われている際に警告を出力する」という非常に簡単なものです。
Rubyでは標準出力を行うp
という関数が用意されています。
そのためp = ...
という変数宣言があると、出力をしたいのか変数宣言をしたいのか分からず、個人的には非常に気持ち悪く感じるのです。
実装手順
あれこれ試した結果、以下の手順をこなせば新しくCopを追加することが出来ました。
意外にもやることは非常に少ないです。前回の記事で触れたようにRuboCopは拡張性に富んだ設計がされています。
変更の全貌は僕のgithubにて公開していますので、合わせてご覧ください。
- lib/rubocop/cop/style直下に新規ファイルを追加する
- lib/rubocop.rbに追加したCopを
requiere
する - 追加したファイルにコールバックとOffenseを追加する条件・処理を実装
ファイル名は「違法な変数名」というニュアンスからとってillegal_variable.rb
としました。
ファイルの追加とrequire
まずはモジュールの定義だけしておきます。
他のstyle直下のCopにならってモジュールを定義しました。
lib/rubocop/style/illegal_variable.rb
# frozen_string_literal: true module RuboCop module Cop module Style class IllegalVariable < Base end end end end
追加したファイルを読み込むようにします。
lib/rubocop.rb
require_relative 'rubocop/cop/style/illegal_variable' :
これで準備は完了したので、あとはコールバックとOffenseを追加する条件をゴリゴリ書けばOKです。
他のCopでは条件判定を行う処理は別のモジュールにmixinとして定義されることがありますが、今回は簡単のため、このまま同じモジュールに処理を書いてしまいます。
コールバックのオーバーライド
前回の記事で紹介したようにRuboCopはCopに対していくつかのコールバックを提供しています。
Cop側で任意のコールバックをオーバーライドすることで、チェックを行うタイミングを選択することが可能です。
- on_new_investigation
- on_investigation_end
- on_other_file
今回は特に考慮したいことがないため、全てのon_...
が実行される前に実行してくれるon_new_investigation
を実装するようにします。
on_...
の一例としてはon_if
やon_while
などがあります。名前の通り、ファイル中にif
が登場すると呼び出されるコールバックになります。
class IllegalVariable < Base def on_new_investigation nil end end
main.rbの用意とbinding.irbでの確認
今回、追加するStyle/IllegalVariableの条件に合致するコードを用意しました。
他のCopの条件には合致しないようにしたので、上手くIllegalVariableが実装できればIllegalVariableの警告のみが表示される想定です。
# frozen_string_literal: true p = 'hello world' puts p
この時点でon_new_investigation
にbinding.irbを仕込んでみると無事にirbが立ち上がることを確認しました。
From: /Users/.../ruby/rubocop-reading/rubocop/lib/rubocop/cop/style/illegal_variable.rb @ line 22 : 21: def on_new_investigation => 22: binding.irb 23: nil 24: end 25:
Offenseを追加する条件
次にCopのエラー記録を保持するOffenseを追加する条件を記述していきます。
p = ...
というようにp
を使って変数宣言がされていた場合にOffenseを追加するようにします。ファイル中の情報は抽象構文木(AST)として、processed_source.tokens
に保持されています。
[ #<RuboCop::AST::Token:0x0000000105ecc970 @pos=#<Parser::Source::Range /Users/takamizawa46/workspace/ruby/rubocop-reading/main.rb 0...29>, @text="# frozen_string_literal: true", @type=:tCOMMENT>, #<RuboCop::AST::Token:0x0000000105ecc858 @pos=#<Parser::Source::Range /Users/takamizawa46/workspace/ruby/rubocop-reading/main.rb 31...32>, @text="p", @type=:tIDENTIFIER>, #<RuboCop::AST::Token:0x0000000105ecc830 @pos=#<Parser::Source::Range /Users/takamizawa46/workspace/ruby/rubocop-reading/main.rb 33...34>, @text="=", @type=:tEQL>, #<RuboCop::AST::Token:0x0000000105ecc7b8 @pos=#<Parser::Source::Range /Users/takamizawa46/workspace/ruby/rubocop-reading/main.rb 35...48>, @text="hello world", @type=:tSTRING>, ]
main.rbにてp = 'hello world'
と宣言した箇所は以下、3つのtokenに分割されています。
- p(tIDENTIFIER)
- =(tEQL)
- hello world(tSTRING)
tokenを利用してp
の後に=
が続いていれば、Copの条件に合致したと判定するようにします。
start_p_lines = processed_source.tokens.filter_map.with_index do |token, i| if token.type == :tIDENTIFIER && token.text == 'p' next_token = processed_source.tokens[i + 1] next_token.type == :tEQL && next_token.text == '=' && token.line end end
ファイル中にp = ...
のような記述がある場合、後の処理のために定義された箇所の行数が返るようにしました。
当然、複数の宣言がされる可能性があるため、戻り値は配列です。この戻り値の要素数が0より大きければCopの条件に合致したことになります。
def on_new_investigation start_p_lines = processed_source.tokens.filter_map.with_index do |token, i| if token.type == :tIDENTIFIER && token.text == 'p' next_token = processed_source.tokens[i + 1] next_token.type == :tEQL && next_token.text == '=' && token.line end end return if start_p_lines.size.zero? end
irb> start_p_lines
=> [3]
Offenseの追加
あとはOffenseを追加するのみです。
他のCopにならってOffenseを追加するようにしました。結果的に、ほとんど既存のCopと同じ処理になりました。
class IllegalVariable < Base include RangeHelp MSG_DETECTED = "Not recommend using 'p' as a variable because it can be confused with the standard output function 'p'." def on_new_investigation : missing_offense(processed_source, start_p_lines) end private def missing_offense(processed_source, start_p_lines) start_p_lines.each do |line| range = source_range(processed_source.buffer, line, line) add_offense(range, message: MSG_DETECTED) end end end
これで全ての実装が完了しました。
いざ動作確認
さっそくrubocop
コマンドを実行してみます。どうなるでしょうか...。
$ bundle exec rubocop main.rb Inspecting 1 file C Offenses: main.rb:3:4: C: Style/IllegalVariable: Not recommend using 'p' as a variable because it can be confused with the standard output function 'p'. p = 'hello world' ^ 1 file inspected, 1 offense detected
無事に新しく追加したCopであるIllegalVariableの警告が出力されました。やりました。
続いてmain.rbを更新してp = ...
の宣言を複数、行ってみます。無事に複数の警告が出力されるでしょうか...。
# frozen_string_literal: true p = 'hello world' puts p p = 'hello world' puts p p = 'hello world' puts p
$ bash run-rubocop.sh Inspecting 1 file C Offenses: main.rb:3:4: C: Style/IllegalVariable: Not recommend using 'p' as a variable because it can be confused with the standard output function 'p'. p = 'hello world' ^ main.rb:6:7: C: Style/IllegalVariable: Not recommend using 'p' as a variable because it can be confused with the standard output function 'p'. p = 'hello world' ^ main.rb:9:10: C: Style/IllegalVariable: Not recommend using 'p' as a variable because it can be confused with the standard output function 'p'. p = 'hello world' ^ 1 file inspected, 3 offenses detected
複数、宣言しても問題なく警告が出力されました。問題なさそうですね。
無事に前回、RuboCopのコードを読んだ得た知識を元に、Copの追加ができました。非常に嬉しいです。
これがRuboCop本体への機能追加であれば、specの追加も行うべきですが、今回は省略しています。
少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。