# dylintでRustの自前lintを運用する

Rustコードベースでは、まずClippyで既存lintを回すことが多い。ところが、自前lint向けの規約はClippy本体のlint一覧には載らない。

モジュール境界でnewtypeを強制したい、Clippy抑制には理由コメントを付けたい、といった要件は、`clippy.toml`の閾値変更だけでは足りない。

`clippy.toml`は既存Clippy lintの設定を変えるだけで、新しい走査ロジックは追加できない。`pub fn`の引数型やallow属性の直前行といった、型情報や属性に触れるルールは、自前lintとして実装する必要がある。

dylintでは、自前lintを`lintライブラリ`として用意する。
`lintライブラリ`は、検査対象のアプリとは別に置くRust crateで、lintの登録処理とHIR走査ロジックを含む。
dylintは、このcrateをcdylib（Rustの動的ライブラリcrate-type）としてビルドし、`cargo dylint`実行時にロードする。
アプリ本体の通常ビルドにlintを焼き込まず、検査ルールだけを差し替え可能にする点が「動的」の意味になる。

この構成では、検査ルールをアプリ側crateの通常ビルドから切り離せる。

lintライブラリ側には、nightlyと`rustc_private` feature（rustc内部APIを使うための機能フラグ）が要る。一方で、アプリ側crateはstableのまま検査できる。

## ディレクトリ構成の見取り図

設定ファイルとコマンドは、lintライブラリ側とアプリ側workspaceの2系統に分かれる。

例の構成では、lintライブラリとアプリ側workspaceを同階層のディレクトリに置く。

```text
project-root/
├── lint-rules/                 # lintライブラリ（nightly固定）
│   ├── Cargo.toml              # cdylib + dylint_linting
│   ├── rust-toolchain.toml     # nightly + rustc-dev
│   ├── .cargo/config.toml      # linker = dylint-link
│   ├── src/lib.rs              # LateLintPass 実装
│   └── ui/                     # ui_test 用 (.rs / .stderr)
└── sample-workspace/           # アプリ側 workspace（stable のまま）
    ├── Cargo.toml              # workspace.metadata.dylint
    └── sample-crate/           # 検査対象 crate
        └── src/lib.rs
```

各ディレクトリの役割は次のとおり。

| パス | 役割 | toolchain |
| --- | --- | --- |
| `lint-rules/` | lintをcdylibとしてビルドし、`register_lints`をexport | nightly + `rustc-dev` |
| `sample-workspace/` | 検査対象の通常アプリ。metadataでlintライブラリを宣言 | stable（通常のcargo build） |
| `sample-workspace/sample-crate/` | `cargo dylint`が実際に走査するcrate | stable |

表のとおり、`lint-rules`は`sample-workspace`の`members`に入れない。

`lint-rules`をworkspace member（同じworkspaceに含めるcrate）にすると、`cargo build`の対象になる。`lint-rules/rust-toolchain.toml`でpinしたnightlyがworkspaceルートからのビルドでも選ばれ、アプリ側crateまでnightly解決が及ぶ。

実行の流れは次のとおり。

```mermaid
flowchart LR
  subgraph nightly_side [lint-rules / nightly]
    LR[lint-rules を cdylib ビルド]
  end
  subgraph stable_side [sample-workspace / stable]
    APP[sample-crate のソース]
  end
  LR -->|cdylib| DYL[cargo dylint]
  APP --> DYL
  DYL -->|検査結果| OUT[warning / error 出力]
```

実行の起点は`sample-workspace/`。同階層の`lint-rules/`でビルドしたcdylibを`cargo dylint`が読み込み、`sample-crate/`を走査する。コマンドとCargo.tomlの具体例は「dylintの最小構成」節で示す。

## 自前lintが必要になるケース

Clippyは、Rustコミュニティ全体に通用する汎用パターンの検出を得意とする。関数シグネチャやallow属性のようにリポジトリ規約へ依存する要件では、自前lintが必要になりやすい。典型例は次のとおり。

| 要件 | Clippy既存lintだけでは |
| --- | --- |
| `pub fn`シグネチャのraw数値型をdomain newtype（意味ごとに分けた型）に限定 | ルール自体が存在しない |
| `#[allow(clippy::...)]`に理由コメントを必須化 | 抑制の説明責任までは追わない |
| ファイル行数やテスト配置などのリポジトリ規約 | 組織固有のためClippy本体に載りにくい |

表の典型例を満たす自前lintは、リポジトリ内にlintライブラリを置く。dylintはそのlintライブラリをcdylibとしてビルドし、通常のcargo checkと同じアプリ側crateに差し込む。

## dylintの最小構成

dylint導入では、同階層に置いた`lint-rules/`と`sample-workspace/`を次の3つのstepで接続する。

| step | 作業 |
| ---: | --- |
| 1 | ホストに`cargo-dylint@5.0.0`と`dylint-link@5.0.0`をインストールする |
| 2 | `lint-rules/`をcdylibとして設定する |
| 3 | `sample-workspace/`からlintを実行する |

### step 1: ホストへのインストール

`lint-rules/`と`sample-workspace/`の外（開発マシンまたはCIイメージ）で、`cargo-dylint`と`dylint-link`の2つを入れる。

```bash
cargo install cargo-dylint@5.0.0 dylint-link@5.0.0 --locked
```

5.0.0に固定する。対応Rustバージョンは「バージョンと対応Rust」節で扱う。

### step 2: lint-rules/ をcdylibとして設定

lintライブラリはcdylibとしてビルドする。Cargo.toml（manifest）、toolchain、linker設定の3点を`lint-rules/`に置く。

#### Cargo.toml と rust-toolchain.toml

```toml
# lint-rules/Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
dylint_linting = "=5.0.0"

[dev-dependencies]
dylint_testing = "=5.0.0"
```

`dylint_library`マクロは、上のCargo.tomlで依存に入れた`dylint_linting`に含まれる。
cdylibには、このマクロでdylint向けの必須シンボルを埋め込む。
このシンボルがないと、`cargo dylint`がlintライブラリとして読み込めない。

続いて `rust-toolchain.toml` を置く。

```toml
# lint-rules/rust-toolchain.toml
[toolchain]
channel = "nightly-YYYY-MM-DD"
components = ["llvm-tools-preview", "rustc-dev"]
```

`rustc-dev`は、lintライブラリのビルドに必要なrustup component（追加コンポーネント）。`channel`のnightly日付は、`dylint_linting` 5系が想定するrustc API世代と揃える。詳細は「nightlyとdylint_lintingのバージョン」節で扱う。

#### .cargo/config.toml（linker指定）

dylint-linkは、cdylibをdylintが検索するファイル名でリンクする。通常のlinkerのままだと出力ファイル名が異なり、`cargo dylint list`にlintライブラリが現れない（「dylint-link」節も参照）。

```toml
[target.'cfg(all())']
rustflags = ["-C", "linker=dylint-link"]
```

### step 3: sample-workspace/ からlintを実行

アプリ側workspaceにlintライブラリを宣言し、`cargo dylint`を走らせる。

#### Cargo.toml（lintライブラリの宣言）

manifest側では、lintライブラリの場所を`libraries`に書く。

```toml
# sample-workspace/Cargo.toml
[workspace]
members = ["sample-crate"]
resolver = "2"

[workspace.metadata.dylint]
libraries = [{ path = "<lint-library-path>" }]
```

`[workspace.metadata.dylint]`は、dylint向けのworkspace設定ブロックで、`libraries`にlintライブラリの場所を書く。

#### 実行コマンド

`sample-workspace/`のルートにcdしてから、nightly toolchain上で`cargo dylint`を走らせる。アプリ側crateの通常ビルドはstableのままでよいが、dylint実行環境にはstep 1のCLIとnightlyが必要になる。

lintライブラリの指定は次の2通り。

```bash
cd sample-workspace

# manifest を使わず CLI で path 指定
cargo dylint --path <lint-library-path> -- --all-targets

# manifest の libraries を読む（--all）
cargo dylint --all -- --all-targets
```

CLIの`--path`でlintライブラリを都度指定できる。manifestの`[workspace.metadata.dylint]`を読むときは`--all`を使う。

`deny`設定のlintがerrorになると、`cargo dylint`の終了コードは非ゼロになる。

ここまでが最小構成になる。

続いてlintライブラリの実装に入る。

### バージョンと対応Rust

`cargo-dylint`のメジャーバージョンは、Rust MSRV（Minimum Supported Rust Version、対応する最小Rustバージョン）と連動する。MSRVより古いRustでは、`cargo install`自体が失敗する。

lintライブラリcrateとCLIのバージョンをずらすと、dylint実行時にABI不一致で落ちることもある。

## HIRを走査するlintを書く

lintライブラリの実装は`lint-rules/src/lib.rs`に置く。

HIR（High-level Intermediate Representation）は、型検査後のrustc中間表現を指す。
ソースを文字列として探すのではなく、型が付いた構文木をコールバックで受け取って調べる。
dylintのlint APIでは、HIRを走査するrustcのlintトレイト`LateLintPass`（lint実装の単位）を同じ形で使える。
Clippy lint開発ガイドの`declare_lint`マクロや`LateContext`の使い方も流用できる。

1つの`LateLintPass`実装で複数のlintを扱っても、処理はコールバックごとに分ける。

実装するlintは次の2つ。

| lint名 | 検査内容 | レベル |
| --- | --- | --- |
| `PUB_FN_RAW_PRIMITIVE` | `pub fn`の引数型・戻り値型に`u32`や`i32`などのraw数値型が出たら指摘する | Warn |
| `ALLOW_CLIPPY_NEEDS_REASON` | `#[allow(clippy::...)]`の直前に`// clippy-allow-reason:`コメントがなければ指摘する | Deny |

前者は関数定義を受け取る`check_fn`で検査する。
後者は`mod`・`fn`・`struct`などのitem属性を受け取る`check_item`で検査する。

登録手順は次の3つのstepに分かれる。

1. `declare_lint`マクロでlint名とレベル（Warn/Deny）を宣言し / CIで落とすlintをDenyにする
2. `register_lints`でlint一覧と`LateLintPass`を`LintStore`へ登録する
3. `LateLintPass`の`check_fn`/`check_item`等でHIRを走査し / pub fn向け / 属性向けでコールバックを分ける

この分け方にしておくと、関数シグネチャの規約と属性まわりの規約を同じ`LateLintPass`実装で扱っても、どのコールバックが何を検査するかをレビューしやすい。

以下はlintライブラリのlib.rs骨格のみ示す。先頭のrustc_private featureとextern crate群は省略する。

```rust
// HIR（型検査後の構文木）と lint API
use rustc_hir::intravisit::FnKind;
use rustc_hir::{Body, FnDecl, FnRetTy, Item, QPath, TyKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::impl_lint_pass;
use rustc_span::def_id::LocalDefId;
use rustc_span::{Span, sym};

// dylint が cdylib から呼ぶ dylint_version 等のシンボルを自動生成
dylint_linting::dylint_library!();

// cargo dylint 実行時、rustc が最初に呼ぶ入口
#[unsafe(no_mangle)]
pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
    // dylint.toml 等の設定を rustc セッションへ反映
    dylint_linting::init_config(sess);
    // 下の declare_lint! で宣言した lint 名を rustc に登録
    lint_store.register_lints(&[PUB_FN_RAW_PRIMITIVE, ALLOW_CLIPPY_NEEDS_REASON]);
    // HIR 走査用の LateLintPass インスタンスを登録
    lint_store.register_late_pass(|_| Box::<RepoPolicyLints>::default());
}

// lint 1: pub fn シグネチャに raw 数値型があれば Warn
rustc_session::declare_lint! {
    pub PUB_FN_RAW_PRIMITIVE,
    Warn,
    "raw numeric primitive in `pub fn` signature — use a newtype instead"
}

// lint 2: #[allow(clippy::...)] に理由コメントがなければ Deny（CI で落とす）
rustc_session::declare_lint! {
    pub ALLOW_CLIPPY_NEEDS_REASON,
    Deny,
    "`#[allow(clippy::...)]` must have a preceding `// clippy-allow-reason:` comment"
}

// 1 つの LateLintPass 実装が複数 lint を emit する
#[derive(Default)]
pub struct RepoPolicyLints;

impl_lint_pass!(RepoPolicyLints => [PUB_FN_RAW_PRIMITIVE, ALLOW_CLIPPY_NEEDS_REASON]);

impl<'tcx> LateLintPass<'tcx> for RepoPolicyLints {
    // 関数定義を HIR 上で 1 件ずつ受け取るコールバック
    fn check_fn(
        &mut self,
        cx: &LateContext<'tcx>,
        kind: FnKind<'tcx>,
        decl: &'tcx FnDecl<'tcx>,
        _body: &'tcx Body<'tcx>,
        _span: Span,
        def_id: LocalDefId,
    ) {
        // クロージャと非 pub fn は raw 型 lint の対象外
        if matches!(kind, FnKind::Closure) || !cx.tcx.local_visibility(def_id).is_public() {
            return;
        }
        // 引数型を走査
        for input_ty in decl.inputs {
            check_ty(cx, input_ty);
        }
        // 戻り値型があれば走査（-> () はスキップ）
        if let FnRetTy::Return(ret_ty) = decl.output {
            check_ty(cx, ret_ty);
        }
    }

    // mod / fn / struct 等のアイテムごとに呼ばれる。属性 lint 用
    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
        let attrs = cx.tcx.hir_attrs(item.hir_id());
        for attr in attrs {
            // #[allow(...)] 以外は無視
            if !attr.has_name(sym::allow) {
                continue;
            }
            // snippet に clippy:: が含まれる allow だけ対象
            let Ok(snippet) = cx.sess().source_map().span_to_snippet(attr.span()) else {
                continue;
            };
            if !snippet.contains("clippy::") {
                continue;
            }
            // 属性直前の非空行が // clippy-allow-reason: で始まっていれば OK
            if previous_non_empty_line(cx, attr.span())
                .is_some_and(|line| line.starts_with("// clippy-allow-reason:"))
            {
                continue;
            }
            // 理由コメントがなければ Deny lint を emit
            cx.span_lint(ALLOW_CLIPPY_NEEDS_REASON, attr.span(), |diag| {
                diag.primary_message(
                    "`#[allow(clippy::...)]` must have a preceding `// clippy-allow-reason:` comment",
                );
            });
        }
    }
}

// 型ノード（HIR 上の Ty）を見て、パス型が raw プリミティブなら warning
fn check_ty<'tcx>(cx: &LateContext<'tcx>, ty: &'tcx rustc_hir::Ty<'tcx>) {
    if let TyKind::Path(QPath::Resolved(_, path)) = &ty.kind {
        let Some(name) = path.segments.last().map(|s| s.ident.name.as_str()) else {
            return;
        };
        if matches!(name, "u32" | "i32" | "u64" | "u128" | "i128") {
            // ty.span に diagnostic を付ける（sample-crate 側の該当型位置）
            cx.span_lint(PUB_FN_RAW_PRIMITIVE, ty.span, |diag| {
                diag.primary_message(format!(
                    "raw type `{name}` in `pub fn` signature — use a newtype instead"
                ));
            });
        }
    }
}

// 属性 span の直前にある最後の非空行をソースマップから取得
fn previous_non_empty_line<'tcx>(cx: &LateContext<'tcx>, span: Span) -> Option<String> {
    let Ok(prev) = cx.sess().source_map().span_to_prev_source(span) else {
        return None;
    };
    prev.lines()
        .rev()
        .map(str::trim)
        .find(|line| !line.is_empty())
        .map(ToString::to_string)
}
```

`check_fn`はpub fnシグネチャ上のraw数値型（u32/i32等の固定幅プリミティブ）にwarningを出す。lib.rs内の`matches`マクロでu32/i32等を列挙する。

`check_item`は理由コメントのないallowにerrorを出す。

diagnosticをソース位置に付ける`span_lint`は、message文字列を直接渡す古い形ではなく、diagnosticを組み立てるクロージャ形式を使う。

2本のlintを同時に確認するため、`sample-workspace/sample-crate/src/lib.rs`には次のコードを置く。
先頭の`allow`には理由コメントを付けず、`load_widget`の引数にはraw数値型の`u32`を使う。

```rust
#[allow(clippy::struct_field_names)]
pub struct Widget {
    widget_id: u32,
}

pub fn load_widget(widget_id: u32) -> Widget {
    Widget { widget_id }
}
```

### 実行時のdiagnostic

前節の2通り（`--path` / `--all`）のいずれでも、次の自前lint diagnosticが得られる。どちらもnightly上の`cargo dylint`で実行する。

```text
error: `#[allow(clippy::...)]` must have a preceding `// clippy-allow-reason:` comment
 --> sample-crate/src/lib.rs:1:1

warning: raw type `u32` in `pub fn` signature — use a newtype instead
 --> sample-crate/src/lib.rs:6:31
```

`--path`指定と`--all`指定の差分は、lintライブラリの解決経路だけ。上記では`dead_code`等のrustc既定warningを省略している。

`sample-workspace/sample-crate/src/lib.rs`では、引数位置の`u32`にraw数値型warningが付く。戻り値は`Widget`なので、このサンプルではraw数値型warningの対象にならない。先頭のallowには理由コメントがないためerrorになる。

## workspace metadataとCIへの載せ方

ここまでの`--path <lint-library-path>`指定は、手元で動作を確認するには十分。
ただしCIでは、lintライブラリの場所と実行コマンドをリポジトリ側に固定しておきたい。

そのために、workspace metadataでlintライブラリの場所を宣言し、CIでは`cargo dylint --all`を実行する。
dylintはlintライブラリ側をnightlyでビルドし、アプリ側crateをstableのまま検査するため、CIではtoolchainと`cfg(dylint_lib)`の扱いも明示しておく。

### metadataでlintライブラリを宣言

`[workspace.metadata.dylint]`を書いておけば、`--path`を毎回手打ちする必要はない。

### CIでの実行

CI例では次を実行する。

```bash
cargo dylint --all -- --all-targets
```

期待するdiagnosticは「実行時のdiagnostic」節と同じになる。

### nightlyとstableの二系統

lintライブラリ側はnightly + `rustc-dev`、アプリ側はstable、という二系統を分離する。

CIでnightly toolchainを入れ忘れると、`rustc_private`不足でlintライブラリのビルドが失敗する。

`cargo dylint`はnightly上で実行する。アプリ側crateの通常ビルドはstableのままにできる。

### unexpected_cfgsの宣言

Rust 1.80以降は`unexpected_cfgs` lintが既定有効になる。dylint運用では、dylintが自動で付ける`cfg`もこのlintの対象になる。

dylintが付与する`cfg(dylint_lib)`をmanifestで宣言しないと、ビルドログに `unexpected_cfgs` warning が毎回出る。
運用上は同じwarningがCIログに出続けるため、`sample-workspace/Cargo.toml`に次を書く。

```toml
[workspace.lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = ["cfg(dylint_lib, values(any()))"]
```

`check-cfg`行は、dylintが付与する`cfg(dylint_lib)`を許可する宣言。CIログで毎回同じwarningを見る状態を避けるため、この宣言もlint導入時に入れておく。

各member crate（例：`sample-crate/`）はlint設定をworkspaceから継承する。

```toml
[lints]
workspace = true
```

dylintはlintライブラリごとに`--cfg=dylint_lib="LIBRARY_NAME"`を付与する。条件付きコンパイル（`#[cfg(...)]`でlint名をallowするパターン）を使う場合、上記がないと`unexpected_cfgs` warningが出る。

## ui_testでlint期待値を固定する

lintの挙動は、`lint-rules/`内で`dylint_testing::ui_test`により`.rs`と`.stderr`のペアとして固定できる。Clippyのuiテストと同じ形式になる。

`ui_test`は`lint-rules/ui/`配下の各`.rs`をdylintでコンパイルし、出たdiagnosticが同名の`.stderr`と一致するかを検証する。rustc/dylintのバージョンを上げたとき、期待出力のずれをテストで先に検知できる。

```text
lint-rules/
├── src/lib.rs
└── ui/
    ├── pub_fn_raw_primitive.rs      # 入力（違反コード）
    ├── pub_fn_raw_primitive.stderr  # 期待 diagnostic
    ├── allow_clippy_reason.rs
    └── allow_clippy_reason.stderr
```

テスト本体は`lint-rules/src/lib.rs`末尾に1本置く。`dylint_testing::ui_test`にcrate名と`ui/`ディレクトリを渡す。

```rust
#[test]
fn ui() {
    // CARGO_PKG_NAME = "article_dylint_rules" → ui/ ディレクトリを走査
    dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
}
```

### 例1: raw数値型warningのui_test

`pub_fn_raw_primitive.rs`は、pub fnのu32だけがlint対象になるui_test用fixture。

```rust
pub fn bad_u32(x: u32) -> u32 {
    x
}

pub fn ok_bool(x: bool) -> bool {
    x
}

fn private_ok(x: u32) -> u32 {
    x
}

fn main() {}
```

期待出力は`.stderr`に書く。`$DIR`と`LL`はパス・行番号のプレースホルダで、実行環境差を吸収する。全文を固定するとパス変更のたびにテストが壊れるため、この形式を使う（Clippy ui_testと同型）。

```text
warning: raw type `u32` in `pub fn` signature — use a newtype instead
  --> $DIR/pub_fn_raw_primitive.rs:1:19
   |
LL | pub fn bad_u32(x: u32) -> u32 {
   |                   ^^^
   |
   = note: `#[warn(pub_fn_raw_primitive)]` on by default

warning: raw type `u32` in `pub fn` signature — use a newtype instead
  --> $DIR/pub_fn_raw_primitive.rs:1:27
   |
LL | pub fn bad_u32(x: u32) -> u32 {
   |                           ^^^

warning: 2 warnings emitted
```

引数位置と戻り値位置の2件だけwarningになる（このfixtureは戻り値もu32）。非pub fnやbool引数はwarning対象外。

### 例2: allow理由不足errorのui_test

`allow_clippy_reason.rs`は、理由コメント付きallowを通し、無いallowだけerrorにするui_test用fixture。

```rust
// clippy-allow-reason: test fixture showing acceptable suppression.
#[allow(clippy::struct_field_names)]
struct Good {
    field_name: i32,
}

#[allow(clippy::struct_field_names)]
struct Bad {
    field_name: i32,
}

fn main() {
    let _ = Good { field_name: 1 };
    let _ = Bad { field_name: 2 };
}
```

期待出力は次のとおり。

```text
error: `#[allow(clippy::...)]` must have a preceding `// clippy-allow-reason:` comment
  --> $DIR/allow_clippy_reason.rs:7:1
   |
LL | #[allow(clippy::struct_field_names)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[deny(allow_clippy_needs_reason)]` on by default

error: aborting due to 1 previous error
```

`Good`側の allow には lint が付かず、`Bad`側の allow 1件だけ error になる。

### ui_testの実行

lintライブラリのテストは`lint-rules/`でnightly上から実行する。

```bash
cd lint-rules
cargo +nightly-YYYY-MM-DD test
```

2つのuiペアが成功すれば、2本のlintの期待出力を固定できる。

## 自前lintの手段とdylintが向くケース

「自前lintをCIで回したい」と言っても、手段ごとにできることと運用コストが異なる。
型や属性に触る組織向けlintでは、Clippy forkやコンパイラ外ツールよりdylintの分離構成が扱いやすい場合がある。

### 手段の選択肢

| 手段 | 典型例 | 向くこと | 重いところ |
| --- | --- | --- | --- |
| Clippy既存lint + `clippy.toml` | `blacklisted_name`の設定 | 汎用パターンの検出 | 新規ルールは定義できない |
| Clippy本体への貢献 | コミュニティ向けlintのPR | 誰でも使えるlintの追加 | 組織固有ルールは通りにくい |
| Clippy fork | 組織専用Clippyビルド | Clippyと同じUXでlint追加 | rustc/nightly追従を自前で引き受ける |
| proc-macro | 特定マクロ呼び出しの禁止 | 呼び出し点に限定したコンパイル時チェック | HIR全体走査は向かない。stableではwarningも制約あり |
| dylint | 組織lintをcdylib配布 | `LateLintPass`ベースの自前lint | lintライブラリ側のnightly運用 |
| コンパイラ外ツール | cargo-deny、semgrep、synスクリプト | 依存ポリシー、パターン検索 | rustc diagnosticとは別パイプライン |

補足として、かつてはcompiler plugin attribute（コンパイラ拡張属性）でlintを差し込めたが、rustcからは削除されている。

`cargo clippy`にlintライブラリをプラグインとして読み込む公式APIもない。

Clippy maintainerは[issue #6595](https://github.com/rust-lang/rust-clippy/issues/6595)で、lintを`cargo clippy`へ動的追加する設計は想定していない、と述べている。

この前提に立つと、型・属性に触る自前lintは`cargo clippy`への追加ではなく、別コマンド経路として選ぶ方が整理しやすい。

### dylintの特性

dylintは、rustc APIで別lintツールを書く経路を実用化する仕組みとして扱える。

[dylint README](https://github.com/trailofbits/dylint)でも「Clippyは静的なlint集合、dylintは動的ライブラリからlintを読む」と説明されている。

Clippyのlint実装モデルを保ちながら、実行経路と配布単位を分けられる点がdylintの特徴になる。

この分離により、Clippy lint開発の知見を保ちつつ、lintライブラリだけを切り出せる。

| # | 性質 | 運用での扱い |
| ---: | --- | --- |
| 1 | `LateLintPass`/HIR走査 | 2本lintをClippyガイドと同じ形で書ける |
| 2 | `cargo clippy`とは別コマンド | lintライブラリをcdylibとして差し替え可能 |
| 3 | 検査対象crateとlintライブラリの分離 | `sample-workspace`をstableのままCI化できる |
| 4 | lint単位の配布 | workspace metadataでlintライブラリを宣言 |
| 5 | rustc diagnostic出力 | deny lintをCI終了コードで扱える |

表の中でも、手段選びで先に見るのはlintライブラリ側のnightly固定（`rustc_private`依存）と、ui_test節の`.stderr`回帰になる。バージョンを上げると期待出力がずれ、テストが落ちることがある。

表3行目の分離構成は、アプリ側crateをstableのまま保つための前提になる。

### 向く用途

たとえば、境界APIの引数にdomain newtypeを強制し、`#[allow(clippy::...)]`には理由コメントを求めたい場合を考える。
どちらも型や属性をHIR上で見るため、`clippy.toml`の設定だけでは足りない。
さらにアプリ側workspaceから別crateのlintライブラリを参照したいなら、dylintの分離構成が効きやすい。

このように、次の条件が重なるとき、dylintは有力な選択肢と考えられる。

| 条件 | 具体例 |
| --- | --- |
| rustcの型情報が必要 | HIR上の型を見てAPI境界のルールを検査する |
| 属性やitem単位の構造が必要 | 属性と対象itemを合わせて検査する |
| 汎用lintでは表現できない | `clippy.toml`ではルール新設不可 |
| Clippy forkを避けたい | forkはrustc追従が重い |
| lintを独立crateとして配布したい | workspace metadataから別crateのlintライブラリを参照 |
| Clippyと同じテスト文化 | `dylint_testing::ui_test`で`.rs`/`.stderr`固定 |

### 向かないケース

一方で、rustcのHIRを走査しなくても判定できるなら、dylintを入れる理由は弱いと考えられる。
たとえば依存ライセンスやCVEの検出は、Cargo.tomlとlockfileを見るだけで足りる。
この用途では、cargo-denyの方が扱いやすいことが多い。
特定マクロの禁止も、呼び出し点だけで完結するならproc-macroや`compile_error`マクロで閉じられる。

代表例は次のとおり。

| 用途 | 素直な代替 |
| --- | --- |
| 汎用lintのバグ・スタイル検出 | Clippy既存lint / Clippy本体への貢献 |
| マクロ呼び出し1箇所の禁止 | proc-macro / `compile_error!` |
| 依存ライセンス・CVE・manifestポリシー | cargo-deny等 |
| nightly運用自体を避けたい | syn走査スクリプト / レビュー運用 |

Marker（自前lint向けの実験的framework）も同カテゴリの候補だが、執筆時点ではAPIが不安定と見る。

## nightlyとバージョン同期・unexpected_cfgsなどの注意点

dylint運用でつまずきやすい点をまとめる。

### nightlyと`dylint_linting`のバージョン

lintライブラリは`rustc_private`に依存するため、nightly日付と`dylint_linting` / `dylint_testing`のバージョンをセットで固定する必要がある。
nightly日付は、使う`dylint_linting`が対応するrustc API世代に合わせる。
Cargo.tomlでは、`dylint_linting = "=5.0.0"`、`dylint_testing = "=5.0.0"`のように完全一致で固定する。
`"5.0.0"`だけだとCargoのsemver解決で5系の新しい版を取り得るため、nightly日付やui_testの期待出力が意図せずずれる。

バージョンをずらすと、rustc APIのシグネチャ変更などでlintライブラリのコンパイルが落ちる。

### `dylint-link`

通常のlinkerの代わりにdylint-linkを使うと、dylintが認識できるファイル名のcdylibが生成される。これを省略すると`cargo dylint list`でlintが現れない。

### lintライブラリとアプリ側workspaceの分離

`lint-rules/`をアプリ側workspaceの`members`に含めると、通常の`cargo build`でもlintライブラリがworkspace解決の対象になる。lintライブラリはnightlyと`rustc_private`を必要とするため、アプリ側crateまでnightly toolchainの影響を受ける。

dylint実行だけnightlyに寄せ、通常の`cargo build`はstableのままにしておくため、`lint-rules/`はworkspace外の別crateとして置く。
