【Rails】ノイズになる警告を一時的に抑制する方法あれこれ【乱用厳禁】

## はじめに
Railsの開発をしているとログやターミナルに警告が表示されることがあります。

たとえば以下はRSpecのテストコードを実行したときの実行結果です。
いくつか警告（warning）が表示されています。

```
$ bundle exec rspec spec/models/user_spec.rb
warning: parser/current is loading parser/ruby31, which recognizes 3.1.4-compliant syntax, but you are running 3.1.3.
Please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/environments/test.rb:75)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/initializers/devise.rb:15)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from <class:User> at /Users/jnito/dev/some-app/app/models/user.rb:96)

Randomized with seed 44700
.

Finished in 0.22515 seconds (files took 1.91 seconds to load)
1 example, 0 failures

Randomized with seed 44700
```

警告は文字通り「警告」なので早めに対処した方が良いです。
放置すると将来的に深刻な問題を引き起こす可能性があります。

しかし、「対処した方がよいのは100%承知してるけど、今はちょっと無理なんです！！🥺」というときもあります。

しかも、「いったん無視したい大量の警告」と「早めに対処したい少量の警告」が混在していると、前者がノイズになって後者を見落としてしまう恐れがあります。

たとえば以下のようなイメージです（あくまでイメージですが）。

```
$ bundle exec rspec spec/models/user_spec.rb
warning: parser/current is loading parser/ruby31, which recognizes 3.1.4-compliant syntax, but you are running 3.1.3.
Please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/environments/test.rb:75)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/initializers/devise.rb:15)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from <class:User> at /Users/jnito/dev/some-app/app/models/user.rb:96)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/environments/test.rb:75)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/initializers/devise.rb:15)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/environments/test.rb:75)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/initializers/devise.rb:15)
DEPRECATION WARNING: Passing the coder as positional argument is deprecated and will be removed in Rails 7.2. (called from <class:User> at /Users/jnito/dev/some-app/app/models/user.rb:78)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from <class:User> at /Users/jnito/dev/some-app/app/models/user.rb:96)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/environments/test.rb:75)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from block in <main> at /Users/jnito/dev/some-app/config/initializers/devise.rb:15)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from <class:User> at /Users/jnito/dev/some-app/app/models/user.rb:96)

Randomized with seed 44700
.

Finished in 0.22515 seconds (files took 1.91 seconds to load)
1 example, 0 failures

Randomized with seed 44700
```

こうした状況を想定して、この記事ではノイズになる警告を一時的に抑制する方法をいくつか紹介します。

:::note warn
警告
ここで紹介する方法はあくまで **一時的に抑制したい場合だけ** に利用してください。
抑制したまま警告を放置し続けると、忘れた頃に深刻な問題を引き起こす可能性があります。
:::

### 動作確認をした実行環境

- Rails 7.1.3.2
- Ruby 3.1.3
- warning 1.3.0

## RailsのDEPRECATION WARNINGを抑制する

簡単に制御できるやり方が見つからなかったので、ActiveSupport::Deprecationクラスの`behavior`メソッドをprependでオーバーライドすることにしました。

以下は`config/application.rb`でオーバーライドする例です。

```ruby:config/application.rb
require_relative "boot"

require "rails/all"

# テストコードの実行時のみ警告を抑制する
if Rails.env.test?
  ActiveSupport::Deprecation.prepend(
    Module.new do
      def behavior
        original = super
        original.map do |prc|
          -> (message, callstack, deprecator) do
            # 警告メッセージに以下のどちらかが含まれていたら無視する
            # - Rails.application.secrets
            # - Passing the coder as positional argument
            ignore = ["Rails.application.secrets", "Passing the coder as positional argument"].any? { |m| message.include?(m) }
            prc.call(message, callstack, deprecator) unless ignore
          end
        end
      end
    end
  )
end

Bundler.require(*Rails.groups)

# ...
```

`config/environments/test.rb`や`config/initializers`に記述することもできますが、なるべく早いタイミングでオーバーライドしないと先に警告が出力されてしまうケースがあるため、今回は`config/application.rb`に設定を記述しました。

## Kernelのwarnで出力される警告を抑制する

Railsが出力するDEPRECATION WARNINGと、それ以外の警告は出力の仕組みが異なるので、対処方法も異なります。

### プロジェクト内に書いたコードが原因になっている場合

`$VERBOSE`に`nil`をセットすると、その間だけ警告を抑制できます。

以下は「やんごとなき理由でgemに設定された定数を再代入しないといけない場合」に発生する警告を抑制する例です。

```
# ↓この警告を消したい
/Users/jnito/dev/some-app/config/initializers/some_awesome_gem.rb:2: warning: already initialized constant SomeAwesomeGem::IMPORTANT_VALUE
/Users/jnito/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/bundler/gems/some_awesome_gem/lib/some_awesome_gem/command.rb:5: warning: previous definition of IMPORTANT_VALUE was here
```

```ruby:config/initializers/some_awesome_gem.rb 
# SomeAwesomeGemという架空のgemに定義されたIMPORTANT_VALUEという定数をやむを得ず再代入する
module SomeAwesomeGem
  old_verbose = $VERBOSE
  $VERBOSE = nil # ここから下は警告が出なくなる
  IMPORTANT_VALUE = 123
  $VERBOSE = old_verbose # 元の値に戻す（警告が出るようになる）
end
```

`$VERBOSE`の役割と設定値については公式リファレンスを参照してください。

https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=2dv.html

### gem内のコードが原因になっている場合

自分がプロジェクト内で書いたコードではなく、gemのコードが警告を発する場合もあります。
たとえば、parser gemを使っていると、以下のような警告が表示される場合があります。

``` 
warning: parser/current is loading parser/ruby31, which recognizes 3.1.4-compliant syntax, but you are running 3.1.3.
Please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
```

こうした警告の抑制はWarningモジュールの`warn`メソッドをオーバーライドすると便利です。
以下は `config/application.rb` でWarningモジュールを使って上記の警告を抑制する例です。

```ruby:config/application.rb
require_relative "boot"

require "rails/all"

# テストコードの実行時のみ警告を抑制する
if Rails.env.test?
  module Warning
    def self.warn(message, category: nil)
      # 警告メッセージに "parser" が含まれていたら警告を抑制する
      ignore = ["parser"].any? { |m| message.include?(m) }
      super(message, category: category) unless ignore
    end
  end
end

Bundler.require(*Rails.groups)

# ...
```

「プロジェクト内に書いたコードが原因になっている場合」で抑制した警告も、このWarningモジュールで一緒に抑制できます。

```ruby:config/application.rb
require_relative "boot"

require "rails/all"

# テストコードの実行時のみ警告を抑制する
if Rails.env.test?
  module Warning
    def self.warn(message, category: nil)
      # 警告メッセージに "parser" か "IMPORTANT_VALUE" が含まれていたら警告を抑制する
      ignore = ["parser", "IMPORTANT_VALUE"].any? { |m| message.include?(m) }
      super(message, category: category) unless ignore
    end
  end
end

Bundler.require(*Rails.groups)

# ...
```

Warningモジュールの`warn`メソッドの詳しい内容は以下の公式リファレンスを参照してください。

https://docs.ruby-lang.org/ja/latest/method/Warning/s/warn.html

## Rubyが出力する非推奨警告を抑制する（？）

以下のように（Railsではなく）Ruby自体が非推奨警告を出力する場合があります。

```
/Users/jnito/dev/some-app/config/environments/test.rb:4: warning: File.exists? is deprecated; use File.exist? instead
```

しかし、Rubyの非推奨警告はデフォルトでは出力されません。
Rubyの非推奨警告が出力されるのは `RUBYOPT=-W:deprecated` という環境変数が指定されていた場合だけです。

```
# RUBYOPT=-W:deprecated を指定して意図的にRubyの非推奨警告を出力する
$ RUBYOPT=-W:deprecated bundle exec rspec spec/models/user_spec.rb
/Users/jnito/dev/some-app/config/environments/test.rb:4: warning: File.exists? is deprecated; use File.exist? instead

Randomized with seed 20373
.

Finished in 0.2127 seconds (files took 2.12 seconds to load)
1 example, 0 failures

Randomized with seed 20373
```

なので、この警告を抑制するのであれば、どこかで設定されている`RUBYOPT=-W:deprecated`環境変数を削除してください。

```
# RUBYOPT=-W:deprecated を付けなければRubyの非推奨警告は出力されない
$ bundle exec rspec spec/models/user_spec.rb

Randomized with seed 20373
.

Finished in 0.2127 seconds (files took 2.12 seconds to load)
1 example, 0 failures

Randomized with seed 20373
```

## $stderr に直接puts/printされている場合

たまに `$stderr` に対して直接警告を `puts` / `print` するgemがあったりします。

たとえば、MiniMagickを使っていると、以下のような警告が出る場合があります。

```
convert: profile 'icc': 'RGB ': RGB color space not permitted on grayscale PNG `/var/folders/r1/fsj20_kx4tj5b9why4z15nmw0000gn/T/image_processing20220826-54331-dqrbrb.png' @ warning/png.c/MagickPNGWarningHandler/1750.
```

MiniMagick gemのコードを見ると、 `$stderr` に対して直接 `print` していることがわかります。

```ruby 
# https://github.com/minimagick/minimagick/blob/v4.12.0/lib/mini_magick/shell.rb

module MiniMagick
  class Shell
    def run(command, options = {})
      stdout, stderr, status = execute(command, stdin: options[:stdin])

      if status != 0 && options.fetch(:whiny, MiniMagick.whiny)
        fail MiniMagick::Error, "`#{command.join(" ")}` failed with status: #{status} and error:\n#{stderr}"
      end

      # $stderr に直接 print している
      $stderr.print(stderr) unless options[:stderr] == false

      [stdout, stderr, status]
    end
```


こういうケースは簡単に対処できないので、やむを得ずモンキーパッチを当てたりします。
以下は `config/initializers/mini_magick.rb` というファイルを作って、そこで元々の処理をオーバーライドする例です。

```ruby:config/initializers/mini_magick.rb
if Rails.env.test?
  module MiniMagick
    class Shell
      def run(command, options = {})
        stdout, stderr, status = execute(command, stdin: options[:stdin])

        if status != 0 && options.fetch(:whiny, MiniMagick.whiny)
          fail MiniMagick::Error, "`#{command.join(" ")}` failed with error:\n#{stderr}"
        end

        # "RGB color space not permitted on grayscale" という文言が含まれていたら出力しない
        unless stderr.include?('RGB color space not permitted on grayscale')
          $stderr.print(stderr) unless options[:stderr] == false
        end

        [stdout, stderr, status]
      end
    end
  end
end
```



## コラム：「ついうっかり」で放置し続けないように工夫を入れる
警告を抑制するのはあくまで「一時的」であるべきです。
しかし、人間はついうっかり抑制したまま警告を放置してしまう可能性があります。
その「ついうっかり」をなくすための工夫を紹介します。

### TODOコメントを入れる
コードレビューを必ず通してからプルリクエストをマージするワークフローになっている場合は、「あとで必ず消す」というTODOコメントを付けておくといいですね。
こうしておけば、万一警告を抑制したままになっていてもレビュアーが「これ、消さなくて大丈夫？」とコメントしてくれるかもしれません。

```ruby:config/application.rb
require_relative "boot"

require "rails/all"

# TODO: あとで必ず消す！！（というTODOコメントを付けておく）
if Rails.env.test?
  module Warning
    def self.warn(message, category: nil)
      ignore = ["parser"].any? { |m| message.include?(m) }
      super(message, category: category) unless ignore
    end
  end
end

Bundler.require(*Rails.groups)

# ...
```

### 有効期限を付ける
以下のように有効期限付きで警告を抑制するのも良いかもしれません。

```ruby:config/application.rb
require_relative "boot"

require "rails/all"

# 2024-5-31まで警告を抑制する（2024-6-1以降は警告が出力される）
if Rails.env.test? && Date.today <= Date.new(2024, 5, 31)
  module Warning
    def self.warn(message, category: nil)
      ignore = ["parser"].any? { |m| message.include?(m) }
      super(message, category: category) unless ignore
    end
  end
end

Bundler.require(*Rails.groups)

# ...
```

## まとめ：ご利用は計画的に！！
というわけで、この記事ではノイズになる警告を一時的に抑制する方法をいくつか紹介してみました。

繰り返しになりますが、警告を抑制するのはあくまで「一時的に」ととどめましょう。
警告は無視するのではなく、早めに対処するのが最善であることをお忘れなく。

:::note warn
**重要：** 警告は無視せず、一日も早い対処を！！
:::