🗑

手軽にRustコード中の不要な#[allow]を検出する

#tech#Rust

2026-02-12

小ネタです

TL;DR

  • 世の中のRustコードには不要な#[allow]アトリビュートを用いて警告を抑制しているものが多く存在する
  • これが真に不要かどうかを検出してくれる仕組みが無さそうだったので作った
  • https://github.com/lapla-cogito/cargo-unused-allow

はじめに

Rustはエコシステムでバッドプラクティスとされがちなコードを検出して抑制してくれる仕組みを整えています.これはRustコンパイラ(rustc)及び公式linterであるClippyに搭載されています.例えば次のようなコードに対して

fn f() {
    println!("aaa");
}

fn main() {
    let a = 3;
    let b = 5;
    println!("3+5={}", a + b);
}

cargo buildをすると次のような警告が表示されます.

$ cargo build
(中略)
warning: function `f` is never used
 --> src/main.rs:1:4
  |
1 | fn f() {
  |    ^
  |
  = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
(後略)

f()がどこからも使われていないためにrustcが警告を表示しています.これは非常に単純な例ですが,rustcもClippyも多くのlintルールを持っています.rustcのものはここから辿れますし,Clippyのものはここにまとまっています.

しかしながら事情により特定の場面で警告を抑制したい場面があります.こういった時には#[allow]アトリビュートを用いて警告を抑制することができます:

#[allow(dead_code)]
fn f() {
    println!("aaa");
}

fn main() {
    let a = 3;
    let b = 5;
    println!("3+5={}", a + b);
}

こうすると警告は出ません.これは例えばCI等でRUSTFLAGS-D warningsを設定していて警告もエラー扱いにするようにしていたりする場合において,諸般の事情で特定の部分だけ警告を抑制したいときに便利です.はたまたlintの偽陽性で誤って警告されるのを抑制することにもよく用いられると思います.

しかしながら,コードベースが肥大化してきたりすると実装の変更やlinter側の改善により本来不要になった#[allow]を除去し忘れる可能性があります.そしてこの"不要になった"という情報は検出される仕組みが見当たりませんでした.多くはあっても害は無いものではあると思いますが,将来の変更により本来出るべき警告が出なくなる可能性等を考えると不要なものを消せるならそれに越したことはありません.この観点から本来不要な#[allow]を検出できるものを作りました.

#[expect]

不要になった#[allow]を警告してくれる仕組みは公式で用意されるべきではないかと思われる方もおられると思います.実際issueとしては挙がっていますが,今のrustc及びClippyのアーキテクチャではどのlintがどこで発生して,どの#[allow]により抑制されたかを追跡していないためRust側でこの機能を実装するにはlintアーキテクチャの刷新が必要です.

代替案として#[expect]というアトリビュートがRust 1.81で安定化されました(上のissueもこれによりcloseされています).これは従来#[allow]を書いていた部分を#[expect]で書くことで,その場所でそのlintが発生することを明示的に期待することを示せます.実際にビルド時に発生しなかった場合はunfulfilled_lint_expectationsというlintが発火してユーザーは不要な#[expect]に気付くことができます.

なので既存のコードベースで#[allow]している部分を全部#[expect]に書き換えて不要な#[expect]を消せば解決...!と思われるかもしれません(実際適切に書き換えられれば解決だと思います).しかしこれには特に次のような問題があると思います:

  • #[allow]の中には複数lintを指定することができる(例:#[allow(unused_variables, unused_assignments)])ので一部が不要,一部が有用である場合の書き換えを機械的にやるのは面倒
  • クレートごとにグローバルに#[allow]することもできる(例:#![allow(unused_variables)])ので,このようなケースは実際にどこに#[expect]を付けるべきか調査する必要がある

これまで私はオープン/プライベート問わず様々なRustプロジェクトを見てきましたが,依然として様々な場所で#[allow]が使われて続けているという印象があります.これは古いコードで#[allow]を付けてそのまま見過ごされているものも,#[expect]が使えるのに#[allow]で済ませているものもあります.

実際GitHub上で"[allow("が含まれているRustファイルを検索してみると,これを書いている時点では約280万件がヒットしました.まあまあ多いのではないでしょうか(もちろんRust 1.81より下だから#[expect]が使えなかったり書き捨てのものもあるとは思いますが).

cargo-unused-allow

上述のモチベーションにより,Rust内部のlintアーキテクチャを刷新するのは得たい恩恵に対して変更が大きいと考え,不要な#[allow]を検出するツールcargo-unused-allowを作りました.インストール方法などは次のリポジトリに書いてあります:

https://github.com/lapla-cogito/cargo-unused-allow

1.81以降のRustとClippyがインストールされていることが前提です(が,まあ多くの環境でこれは自然に達成されているのではないでしょうか).

雑な使い方

検出してほしいプロジェクトの内部でcargo unused-allowを実行します.そこそこ大きめのコードベースとして,例えば0a88ccc時点のripgrepに対して適用してみたところ,次のように1カ所検出されました:

$ cargo unused-allow
cargo-unused-allow: project root: /home/lapla/repos/ripgrep
cargo-unused-allow: found 100 .rs file(s)
cargo-unused-allow: modified 8 file(s), running clippy...
(中略)
Found 1 unused #[allow(...)] attribute(s):

  1. crates/ignore/src/gitignore.rs:666:15  [lint: deprecated]
     #![allow(deprecated)]
              ^^^^^^^^^^

These #[allow(...)] attributes are not suppressing any lint and can be safely removed.
Hint: run with --fix to remove them automatically, or use #[expect(...)] instead.

もう少し大きなコードベースで試そうと思ってdenoで試したら20個ほどヒットしました.まあまあありますね.

検出された不要な#[allow]を自動で取り去ってくれるとさらに便利です.このためそれに対応するフラグ--fixも存在します.例を示します:

$ cat src/main.rs

#[allow(unused, clippy::needless_return)]
fn clean_return(x: i32) -> i32 {
    x * 2
}

fn main() {}

$ cargo unused-allow --fix
(中略)
Found 1 unused #[allow(...)] attribute(s):

  1. src/main.rs:1:18  [lint: clippy::needless_return]
     #[allow(unused, clippy::needless_return)]
                     ^^^^^^^^^^^^^^^^^^^^^^^

cargo-unused-allow: fixed src/main.rs
Removed 1 unused lint suppression(s).

$ cat src/main.rs

#[allow(unused)]
fn clean_return(x: i32) -> i32 {
    x * 2
}

fn main() {}

元コードのclean_returnは内部でreturnを使用していないため,clippy::needless_returnをallowするのは無意味ですがunusedをallowすることは効力があります.そのため--fixを適用した後にはclippy::needless_returnだけが適切に除外されていることが分かります.同じ行の前後に#[allow]以外のコンテンツがあったりしてもいい感じにやってくれます.また,#[cfg_attr(condition, allow(...))]のようなcfg_attr形式でのallowもサポートしています.その他細かい使い方についてはリポジトリに書いてあります.

仕組み

#[expect]の節でも触れたように,#[expect]#[allow]と同じように書けますが,その場所で指定したlintが発火しなかったらunfulfilled_lint_expectationsが発火するという差分があります.この差分を利用すると不要な#[allow]の検出は次のように非常に単純に実装できます:

  • 指定されたプロジェクト中の各Rustファイルの#[allow](もしくは#![allow])を全て#[expect](もしくは#![expect])に書き換える
  • 内部でcargo clippy --message-format=json -- -Wunfulfilled-lint-expectationsを走らせる
  • Clippyの結果がJSONで返ってくるのでunfulfilled_lint_expectationsの結果だけ取り出す,これがすなわち不要な#[allow]の集合
  • 一時的に書き換えたexpectをallowに戻す

しかしながら当然#[expect]を内部で用いるのでこれが安定化されたバージョンであるRust 1.81以上が必要です.

他の実装案として,dylintを用いてカスタムlintを実装するというものもありましたが,これは外部lintが,他のlintが発生したものの#[allow]で抑制されたという情報を取りえないので却下としました(し,実際に採用した案の方が簡潔で確実だと思います).