Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Rubyの非推奨警告はデフォルトで表示されないことを見落としていませんか?(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。
訳文ではキャプションの位置を読みやすく変更しています。

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)

関連記事

Rails: Zeitwerkで使えるカスタムinflectorの作り方4種(翻訳)

Rails: モンキーパッチが元コードの更新で乖離するのを防ぐ6つの方法(翻訳)

Rails: アプリケーションを静的解析で"防弾"する3つの便利ワザ(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。