gemが定義したメソッドによる偽陽性警告を抑制する
RSpecなどのモックフレームワークが動的に定義するメソッドに起因する [WARNING] の偽陽性出力を、Gem.path を使った呼び出し元チェックで抑制するように create_command の挙動が改善されました。
背景
rspec-mocks などのモックフレームワークを使うと、テスト実行時に Thor サブクラスへの偽陽性の警告が大量に出力されるという問題がありました。Thor クラスは method_added フックを通じて新しいメソッドが追加されるたびに create_command を呼び出します。この時点で desc も no_commands ブロックも存在しない場合、使用法や説明なしにコマンドを作成しようとした旨の警告を出力します。
問題の核心は、no_commands ブロック内で正しく宣言されたメソッドであっても、rspec-mocks が alias_method や define_method でスタブをセットアップする際に no_commands のコンテキスト外から method_added を再度トリガーしてしまう点にあります。PRに示された再現例では、formatter メソッドを no_commands で正しく宣言しているにもかかわらず、RSpecが内部処理として __formatter_without_any_instance__ などのメソッドを動的定義した結果、以下のような警告が複数出力されます:
[WARNING] Attempted to create command "__formatter_without_any_instance__" without usage or description. ...
[WARNING] Attempted to create command "formatter" without usage or description. ...
[WARNING] Attempted to create command "formatter" without usage or description. ...
実際のテストスイートでは、多数のメソッドがモックされるたびにこれらの警告が増殖し、出力が非常にノイジーになります。これは開発者の本物の誤りを示す警告を埋もれさせてしまうため、テスト結果の可読性を損なう問題でした。
技術的な変更
create_command メソッドに defined_in_gem? ヘルパーメソッドを導入し、呼び出し元がインストール済みgemのコード内であれば警告をスキップするようにしました。
変更前:
def create_command(meth) #:nodoc:
# ...
else
puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \
"Call desc if you want this method to be available as command or declare it inside a " \
"no_commands{} block. Invoked from #{caller[1].inspect}."
false
end
end
変更後:
def create_command(meth) #:nodoc:
# ...
else
caller_line = caller[1]
unless defined_in_gem?(caller_line)
puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \
"Call desc if you want this method to be available as command or declare it inside a " \
"no_commands{} block. Invoked from #{caller_line.inspect}."
end
false
end
end
def defined_in_gem?(caller_line) #:nodoc:
return false unless caller_line
Gem.path.any? { |path| caller_line.include?(path) }
end
Gem.path はRubyGemsがインストール先として管理するディレクトリパスの配列です。caller[1] で取得したスタック1段上の呼び出し元パスが Gem.path のいずれかを含む場合、それはユーザーコードではなくインストール済みgemによるメソッド定義と判断し、警告を出力しません。
また、spec/thor_spec.rb にも対応するテストケースが追加されています。defined_in_gem? をスタブ化して true を返すことで、gemコード起因のメソッド追加では警告が出力されないことを検証しています。
設計判断
呼び出し元のパスを Gem.path と照合するアプローチ が採用され、特定のフレームワークへの依存を排除したフレームワーク非依存の実装になっています。
PRでは、この修正が満たすべき制約として以下の3点が明示されています:
- 開発者が
descやno_commandsを書き忘れた場合は引き続き警告を表示する - RSpecモックなどの偽陽性は表示しない
-
rspec-mocksに限らず、あらゆる動的メソッド定義に対してフレームワーク非依存で機能する
Gem.path による判定は、RubyGemsの標準APIを利用するため追加の依存関係が不要です。インストール済みgemのコードパスは Gem.path 配下に存在するという前提のもと、文字列の include? で簡潔に判定しています。caller_line が nil の場合に return false でガードしており、スタックトレースが取得できない稀なケースでも安全に警告を出力する側に倒れる設計になっています。
まとめ
この変更は、呼び出し元がインストール済みgemかどうかという判定軸を create_command に持ち込むことで、フレームワーク非依存の形で偽陽性警告を抑制しています。テストスイートにおけるノイズを減らしながら、開発者が本来受け取るべき本物の警告は確実に維持されます。