domain: rust ルール
Async はすべての層で一貫して async にする [R:async-all-the-way]
部分的にフック no-block-on-in-async で強制されています。ランタイムスレッドが async 内部でブロックされるその他すべての方法(同期ファイル/ネットワーク I/O、await をまたいで保持される std::sync ロック、spawn_blocking に委譲されていない長時間の CPU 処理)、および
src/ツリー外の Rust ファイルは、この指示のみで管理されます。
非同期コード内で block_on を使用しないでください。非同期ランタイムは多数のタスクを少数のスレッドで多重化します。ブロッキング呼び出しは単一のタスクを遅延させるだけでなく、その期間ワーカープールからスレッドを除外し、本来そこで実行されるはずだったすべてのタスクを遅延させます。被害を受けるタスクはブロックを引き起こしたコードとは無関係であり、そのため症状はまったく別の場所での説明不能なテールレイテンシとして現れ、事後的な原因特定はほとんど不可能です。
block_on が正しいのは、同期コードが非同期コードに入る境界(main、テスト、C ライブラリからのコールバック)のみです。内部に入ったら、そのまま内部に留まり続けてください。非同期 I/O、ガードが await を超えて生存する必要がある場所での非同期対応ロック、および CPU バウンドな計算や同期サードパーティクライアントなど、真にブロックする可能性のある作業には spawn_blocking を使用します。
同様に、std::sync::Mutex ガードを await をまたいで保持してはいけません。これはブロッキングとしてフラグ付けされませんが、タスクがスケジュール解除されている間にロックを保持するため、競合するタスクが自身のワーカースレッドをブロックし、その結果はロックではなくデッドロックとして現れます。
カレントスレッドランタイムでのネストされた block_on はパフォーマンスを低下させるのではなく、即座にデッドロックします。それが正直な障害の現れ方です。マルチスレッドの場合は、スレッド数によって同じ間違いが隠蔽され、負荷がかかると隠れ家がなくなります。
関数が所有権を必要としない限り、シグネチャでは借用を受け取る [R:borrow-in-signatures]
String ではなく &str、Vec<T> ではなく &[T]、PathBuf ではなく &Path を取ります。シグネチャは関数が必要とするものを示す宣言であり、所有権を持つパラメータは「これを保持し続ける」ことを意味します。そのため、本体が読み取りのみを行う場合、シグネチャは真実ではなくなり、すべての呼び出し元がアロケーションのコストを支払うことになります。
関数が値を格納する場合にのみ、所有権を持つパラメータを使用します。その場合、impl Into<String> が丁寧な形式です。String を保持する呼び出し元はコストなしでムーブインでき、&str を保持する呼び出し元は自覚的に 1 回アロケートします。出力側では、&String ではなく &str を返してください。余分な間接参照は呼び出し元に何ももたらさず、フィールドの表現を公開 API に固定してしまいます。
コストはめったに単一のアロケーションではありません。所有権を持つパラメータは伝播します。呼び出し元はそれを満たすためにクローンし、その呼び出し元はそれを満たすためにクローンし、考慮なしに選ばれたシグネチャは、それぞれが局所的には必要に見えるクローンの列になります。R:justify-every-clone はそれらのクローンを表面化させる場所です。このルールは、それらが決して作成されないようにする方法です。
ロジックの前に、まず型を書いて設計する [R:design-types-first]
作成以来 2 回再発しています。最新は 2026-11-01。
設計が決まるまで型をスケッチし、それからロジックを書きます。型レベルのスケッチは、テストファーストの赤フェーズが目指している実行可能な仕様であり、より強力です。テストは動作空間の点をサンプリングするのに対し、型は空間全体を制約し、コンパイラはそれをコンパイル時に、コードが存在する限りどこでも証明します。型が正しければ、実装の大部分は自動的に記述され、多くの間違った実装はコンパイルを停止します。Rust では、この順序付けはテストファーストのデフォルトを優先します。テストは削除されず、適切なツールであるレイヤーに格下げされます。型がエンコードできない振る舞いの法則に対するプロパティテスト、過去のバグに対する回帰ピンとしてのユニットテスト。各保証を保持できる最強のレイヤーで適用し、レビュー時にはまず「合格するか」ではなく「これらの保証のうちどれを上位レイヤーに移動できるか」を問います。
エラー enum は失敗を識別する値を保持する [R:errors-name-what-failed]
部分的にフック no-string-error で強制されています。フィールドのないバリアント、識別値を省略するバリアント、および認識されない値を保持するバリアントと行から区別できない包括的な
Unknown(String)は、この指示のみで管理されます。
個別の失敗はすべて thiserror enum のバリアントであり、各バリアントはインスタンスを識別する値を保持します。
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("TCP connect to {addr} failed")]
Tcp { addr: SocketAddr, #[source] source: io::Error },
#[error("timed out after {elapsed:?}")]
Timeout { elapsed: Duration },
}
これにより、文字列では得られない 2 つのことが得られます。呼び出し元は match できます。次回メッセージを編集するときに変更される散文を解析するのではなく、Timeout を再試行したり、Tcp を表面化したりできます。また、メッセージは実際のアドレスを指定するため、調査の開始ではなく、1 つのログ行でアクションを実行できます。
原因をテキストに補間する代わりに、#[source] に保持します。チェーンはエッジで 1 回出力され、各レイヤーが下のレイヤーを再述することはありません。
呼び出し元が合理的に異なる方法で処理する可能性のある条件ごとに 1 つのバリアント。4 つの原因を Other(String) にまとめると、enum 内で文字列エラーが再現されます。型として読み取られ、散文として動作し、このルールがキャッチするために存在する形状です。
すべての clone() には理由が伴うか、設計が間違っている [R:justify-every-clone]
部分的にフック no-clone-without-comment で強制されています。コメントが呼び出しを再述するのではなく、実際の理由を述べているかどうか。および
src/ツリー外の Rust ファイルは、この指示のみで管理されます。
借用チェッカーエラーは所有権に関する質問です。clone() はそれに答えません。それは答えることを回避するためのコストを支払います。まず答えを試みてください。1 つの所有者が明らかになるように再構築する、名前付きライフタイムで借用を取る、より小さなフィールドにまたがって借用を分割する、または値が真に共有されている場合(コピーではなく)に Arc/Rc で共有する。
その調査を生き残ったクローンについては、その理由をコードベース全体で使用されているハウス形式で横に書きます。
let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // allow:clone: the OutputFile owns its provenance, outliving the &Library borrow
コメントがルールをレビュー可能にします。注釈のないクローンは互いに区別がつかないため、レビュアーはすべてのクローンについて所有権の議論を再導出する必要があり、実際にはどれも再導出しません。注釈付きのクローンはチェック可能な主張を述べており、その周囲の設計が変更された日に見つけて削除できます。
理由は所有権またはライフタイムに関するものでなければなりません。「コンパイルを通すため」はエラークラスを再述したものに過ぎず、正当化ではありません。コードが 1 つの値であると信じている値の 2 つのコピーは、間違った方を変更する誰かを待っている正しさのバグです。
レビューを生き残るほとんどのクローンは、呼び出しサイトではなくシグネチャによって作成されます。R:borrow-in-signatures がプレッシャーの発生源であり、パラメータを修正することで、クローンを注釈するよりも削除する方が一般的です。
モジュールはカプセル化の境界。pub は意図的なエクスポート [R:module-visibility-is-deliberate]
pub(crate) と pub(super) を自由に使用し、pub はクレートが意図的にエクスポートするものだけに予約します。可視性は書類仕事ではありません。これは、まだ自由に変更できるものの宣言であり、コンパイラがチェックできる唯一のそのような宣言です。
デフォルトが重要なのは、pub は追加が安価で削除が高価だからです。アイテムが公開されると、外部の呼び出し元がそれに依存する可能性があるため、モジュールを再編成できなくなり、フィールドの名前を変更できなくなり、1 つの呼び出しサイト用に書かれたヘルパーがサポート対象の API になります。それらのいずれも決定されたわけではありません。デフォルトでそうなったのです。
モジュールは、型と同様に不変条件を保持します。チェック後にのみ呼び出すことができるヘルパーは、そのチェックの横にあり、モジュールに対してプライベートであるため、「〜の後でのみ」は他の誰もそれをまったく呼び出すことができないことによって強制されます。それを公開にすると、その保証は doc コメントに変換されます。
意図したサーフェスを pub use でクレートルートに明示的に再エクスポートし、その背後にあるものは可能な限りプライベートにしておきます。公開 API は、コードがたまたま存在した場所の残差ではなく、誰かが書いたリストになります。
R:private-fields-only は 1 レベル下の同じ議論であり、アイテムではなくフィールドに関するものです。2 つは分離可能です。pub 型のプライベートフィールドは構造をリークしますが、pub(crate) 型のパブリックフィールドはリークしません。したがって、一方を満たしても他方については何も言えません。
重要な戻り値に #[must_use] を付ける [R:must-use-on-consequential-returns]
Result を返すすべての関数、および戻り値が呼び出しの目的であるすべての関数に #[must_use] を付けます。コンパイラは、現在と将来のすべての呼び出しサイトで破棄された結果をフラグ付けし、レビュアーが 100 の中から 1 つの裸のステートメントに気付くのを待つ必要がなくなります。
関数の属性よりも型の属性を優先します。#[must_use] struct Receipt; は、このルールを読んだことのない誰かが後で書いたものを含め、Receipt を返すすべての関数に適用されます。関数レベルの属性は毎回覚えておく必要があります。型が常に重要な場合は型にアタッチし、同じ型が破棄しても問題ない場合の狭いケースにのみ、関数にフォールバックします。
結果として生じる警告を CI でエラーに昇格させます。#[must_use] の違反が誰も読まないメモを出力する場合、それは追加の構文を伴うコメントであり、それが主張する保証はどこにも保持されていません。
新しい型を積極的に:異なるドメインの概念には異なる型を [R:newtype-liberally]
基盤となる表現が同一であっても、すべてのドメイン概念に独自の型を与えます。Miles(f64) と Kilometers(f64)、Host(String) と Port(u16)。2 つの裸の f64 や、その順序を人間だけが覚えている String と u16 は決して使わないでください。新しい型は通常ゼロコストです。プリミティブと同じマシンコードにコンパイルされるため、追加されるのは必要なコンパイルエラーだけです。ただし、そのコストが重要な場合は、R:verify-the-abstraction-compiled-away でその主張を繰り返すのではなくチェックする必要があります。書くことができない単位の混同は、レビューでキャッチされるものよりも安価であり、キャッチされないものよりもはるかに安価です。R:parse-dont-validate は新しい型の出所です。境界がそれを証人として造幣します。このルールは、プリミティブにアンラップして戻すのではなく、その後どこにでもそれを運ぶことに関するものです。
ライブラリの戻り値の型に anyhow を使用しない [R:no-anyhow-in-libraries]
フック no-anyhow-in-lib でも強制されています。
ライブラリクレートは型付きエラー enum (thiserror) を返すため、Result は何が問題になる可能性があるかを正確に示し、呼び出し元はそれを match できます。anyhow は最も外側のバイナリのエッジにのみ属します。このルールは卒業しました。no-anyhow-in-lib フックが書き込み時にそれを決定論的に強制するようになりました。
未使用のローカル変数を let _ = で黙らせない。使用するか、報告するか、削除する [R:no-let-underscore-on-a-local]
部分的にフック no-let-underscore-local で強制されています。アンダースコアプレフィックス付きのバインディングは、この指示のみで管理されます。
コンパイラの未使用バインディング警告はリーダーです。これは、計算する手間をかけた値が何にも到達していないことを示しています。let _ = x; はそのリーダーをオフにするために存在し、ローカル変数では、それが正しかった瞬間にほぼ常にオフにします。
2 つの形状があり、どちらも -D warnings の下でクリーンにコンパイルされながら、レビュアーには誰かが行った決定として読み取られます。
- 計算されて破棄された区別。 フラグが解析され、モードがデコードされ、設定値が読み取られます。そして破棄されるため、それが分離することを意図したケースは暗黙的に 1 つとして処理されます。
- キャッチされて破棄された失敗。
if let Err(e) = op() { let _ = e; continue; }はどこにもイベントを残しません。失敗が発生しましたが、システム内の何も後でそれを言うことができません。
警告が発生したら、3 つのうちの 1 つを行います。値を使用する。未使用の区別は通常、ブランチが欠落していることを意味し、警告がそれを指しています。報告または伝播する。無視しても安全なエラーでも、名前付きイベント、またはなぜ後続の誰も知る必要がないのかについてのコメントが取得されます。または、それを計算するコードを削除する。 バインディングが設計上デッドである場合(ループ変数への最後の代入など)、警告をミュートするのではなく、デッド代入が決して発生しないようにループを書き直します。
呼び出しに対する let _ = f(); は異なるケースです。戻り値を破棄します。これは R:must-use-on-consequential-returns が管理します。このルールは、すでに保持しているローカル変数に関するものです。R:guarantee-needs-a-reader は、コンパイラが未読のバインディングをキャッチすることに依存しています。let _ = はコンパイラが見ることができない唯一のスペルです。
let _ = name; を書こうとするときはいつでも、障害モードをチェックします。この値は何を決定するために計算され、その決定は今どこで行われるのですか? 答えが「どこでもない」場合、警告は正しかったのです。
プロダクションコードで unwrap() を使用しない [R:no-unwrap-in-production]
フック no-unwrap-in-src + フック no-expect-empty-msg でも強制されています。
プロダクションコードで unwrap() を使用しないでください。expect() は、リソースと不変条件を指定する意味のあるパニックメッセージでのみ使用するか、? でエラーを返し、呼び出し元にそれを表面化する方法を決定させます。このルールは卒業しました。no-unwrap-in-src フックが書き込み時にそれを決定論的に強制するようになったため、指示レイヤーはもはやそれを行う必要がありません。
パースし、検証しない [R:parse-dont-validate]
部分的にフック no-free-validator で強制されています。内部の呼び出しサイトでの解析済みデータの再チェックは、この指示のみで管理されます。
最も外側の境界で生の入力をリッチなドメイン型に変換し、チェックが行われたことを証明する証人新しい型を生成します。内部コードは証人を受け取り、再チェックしません。1 つの境界、1 つのチェック、その後はコンパイラによってどこでも強制されます。R:parse-wide-then-range-check はこれをさらに洗練します。境界は、不正な値に名前を付けるために、その値を表現できなければなりません。
広くパースしてから範囲チェック [R:parse-wide-then-range-check]
範囲外の値を表現するのに十分な幅の型にパースし、その後範囲チェックを行います。