Rails: Rubyの非推奨警告はデフォルトで表示されないことを見落としていませんか?(翻訳)
アプリケーションで非推奨警告(deprecation warning)が出力されていないかどうかをチェックする習慣は、技術スタックを最新の状態に保つうえで欠かせません。Railsの場合、環境ごとのコンフィグファイルで明示的にActiveSupport::Deprecation
が設定されるので、表示された非推奨警告に対応することは一般に行われています。しかしRailsはそれでよくても、Ruby自体が表示する非推奨警告に対応するよう適切に設定しているプロジェクトはほとんど見かけません。RailsとRubyの両方を常に最新状態に保つには、両方の非推奨警告に対応することが重要です。
🔗 Railsで非推奨警告を扱うしくみ
Railsでは、config/environments/ディレクトリの下にある環境ごとの設定ファイルに以下のようにActiveSupport::Deprecation
がセットアップされています。
# config/environments/development.rb
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
この設定は、「development環境では非推奨警告をすべてRailsロガーに出力し、容認してはならない非推奨事項が存在する場合は例外を発生させる」というシンプルな意味です。既に対応済みの非推奨がリグレッション(regression: 再発)しないようにするため、対応済みの非推奨事項を許可しないのが普通です。
config.active_support.deprecation
では以下の振る舞いを指定できます。
:raise
:stderr
:log
:notify
:report
:silence
また、call
メソッドに応答する任意のオブジェクト(lambda)を渡すことも可能です。
development環境では、通常:raise
または:log
に設定します。
test環境では以下のように、表示された非推奨警告やエラーをCIで収集することをおすすめします。
# config/environments/test.rb
if ENV.has_key?('CI')
logger = Logger.new('log/deprecations.txt')
config.active_support.deprecation = logger.method(:info)
else
config.active_support.deprecation = :log
end
一方、production環境ではログ出力を行い、エラーをraiseさせないのが普通です。しかしRailsで自動生成されるconfig/environments/production.rbコンフィグでは、デフォルトでconfig.active_support.report_deprecations = false
が設定されます(これは:silence
と同じ振る舞いになります)。production環境で非推奨警告を収集するには、手動で設定を変更する必要があります。
Rubyの非推奨警告はどのようなしくみになっているか
Rubyも非推奨警告を出力できますが、Railsほど単純ではなく、明示的に設定する必要があります。
Rubyで非推奨の機能が使われていることを通知するには、Ruby組み込みのWarning
モジュールを利用します。ただし、Rubyの警告メッセージはデフォルトでは$stderr
に出力されるため、多くの開発者に無視されています。
また、Ruby 2.7.2以降は、Warning[:deprecated] = true
を明示的に設定しない限り、特定の種類の警告メッセージは表示されなくなっています(#17591)。
私がオススメしたいのは、Railsが採用しているのと同じ非推奨警告の戦略を、Rubyにも適用する方法です。
これを行うには、RubyのKernel#warn
メソッドをオーバーライドします。Kernel#warn
はRubyの警告メッセージ出力に使われており、特定の警告メッセージがRailsのActiveSupport::Deprecation#warn
に渡されます。
🔗 Ruby 3以上、Rails 7.1以上の場合
# config/initializers/capture_ruby_warnings.rb
Rails.application.deprecators[:ruby] = ActiveSupport::Deprecation.new(nil, 'Ruby')
module CaptureRubyWarnings
def warn(message, category: nil)
if category == :deprecated
Rails.application.deprecators[:ruby].warn("#{message}", caller)
else
super
end
end
end
Warning[:deprecated] = true
Warning.extend(CaptureRubyWarnings)
🔗 Ruby 3未満、Rails 7.1以上の場合
Ruby 3より前のKernel#warn
メソッドにはcategory
キーワード引数がありませんでした(#17122)。そのため、Ruby 3未満を利用している場合は、メッセージが非推奨警告かどうかを判定するために文字列のマッチングを実行する必要があります。
# config/initializers/capture_ruby_warnings.rb
Rails.application.deprecators[:ruby] = ActiveSupport::Deprecation.new(nil, 'Ruby')
module CaptureRubyWarnings
def warn(message)
if message =~ /deprecated/i
Rails.application.deprecators[:ruby].warn("#{message}", caller)
else
super
end
end
end
Warning[:deprecated] = true if Warning.respond_to?(:[]=) # 注意: Warning.respond_to?が使えるのは2.7.0以降
Warning.extend(CaptureRubyWarnings)
🔗 Ruby 3以上、Rails 7.1未満の場合
Rails 7.1より前のアプリケーション設定には、依存関係ごとに1個ずつ定義されるdeprecatorのコレクションがありませんでした。そのため、以前の私たちはグローバルの ActiveSupport::Deprecation
シングルトンを直接コールバックさせていました。
# config/initializers/capture_ruby_warnings.rb
module CaptureRubyWarnings
def warn(message, category: nil)
if category == :deprecated
ActiveSupport::Deprecation.warn("[RUBY] #{message}", caller)
else
super
end
end
end
Warning[:deprecated] = true
Warning.extend(CaptureRubyWarnings)
🔗 Ruby 3未満、Rails 7.1未満の場合
# config/initializers/capture_ruby_warnings.rb
module CaptureRubyWarnings
def warn(message)
if message =~ /deprecated/i
ActiveSupport::Deprecation.warn("[RUBY] #{message}", caller)
else
super
end
end
end
Warning[:deprecated] = true if Warning.respond_to?(:[]=) # 注意: Warning.respond_to?が使えるのは2.7.0以降
Warning.extend(CaptureRubyWarnings)
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
訳文ではキャプションの位置を読みやすく変更しています。