破壊的変更の手順
このページでは、既存のコードがコンパイルできなくなる可能性のある、コンパイラにおけるバグ修正や健全性の修正を行うためのベストプラクティスの手順を定義します。このテキストは RFC 1589 に基づいています。
動機
時折、既存のコードがコンパイルできなくなる原因となる、コンパイラのバグ修正、健全性の修正、またはその他の変更を行う必要が生じます。このような場合、Rust のユーザーが円滑に移行できるような方法で変更を扱うことが重要です。避けたいのは、既存のプログラムが不透明なエラーメッセージによって突然コンパイルできなくなることです。むしろ、何が問題なのか、どのように修正するのか、なぜその変更が行われたのかについて明確なガイダンスを伴う、段階的な警告期間を設けることが望ましいです。この RFC では、そのような円滑な移行を実現することを目的として、破壊的変更を扱うために私たちが開発してきた手順について説明します。
このポリシーの要点の 1 つは、(a) 可能な限り、最初からハードエラーではなく警告を発行すべきであること、および (b) 既存のコードがコンパイルできなくなるすべての変更には、関連する追跡 issue が用意されることです。この issue は、その変更の結果に関するフィードバックを集める場を提供します。変更が予想外に大きな影響を及ぼすこともあれば、検討されていなかった方法で変更を避けられる場合もあります。そのような場合には、方針を変更して変更をロールバックするか、別の解決策を見つけることを決定する場合があります(警告が使われている場合、これは特に容易です)。
何がバグ修正に該当するのか?
この RFC は、破壊的変更がいつ許可されるかを定義しようとするものではないことに注意してください。それについてはすでに RFC 1122 で扱われています。この文書では、行われる変更がそれらのポリシーに従っていることを前提としています。以下は RFC 1122 の条件の要約です。
- 健全性に関する変更: 型システムで見つかった穴の修正。
- コンパイラのバグ: RFC または lang-team の決定に記載された仕様上のセマンティクスを、コンパイラが実装していない箇所。
- 仕様が不十分な言語セマンティクス: コンパイラの振る舞いが一貫しておらず、正式な振る舞いが以前に決定されていなかったグレーゾーンの明確化。
詳細については RFC を参照してください!
詳細設計
破壊的変更を行う手順は次のとおりです(各手順については以下でさらに詳しく説明します)。
- 変更の影響を評価するために crater run を行う。
- その変更専用の 特別な追跡 issue を作成する。
- すぐにエラーを報告しない。代わりに、前方互換性 lint 警告を発行する。
- これが単純ではない場合もあります。過去に採用したさまざまな手法については、以下のテキストを参照してください。
- 警告が実現困難な場合:
- エラーを報告するが、ユーザーを追跡 issue に導く、的を絞ったエラーメッセージを出すよう最大限努力する
- 問題を修正する PR を、影響を受ける既知のすべての crate に送る
- または、少なくとも、それらの crate の所有者に問題を知らせ、追跡 issue に案内する
- 変更が少なくとも 1 サイクルの間公開された状態になったら、変更を安定化し、それらの警告をエラーに変換できる。
最後に、プラグインに影響する rustc_ast への変更については、一般的なポリシーとして、これらの変更をまとめて行います。これについては以下でさらに詳しく説明します。
追跡 issue
すべての破壊的変更には、その変更専用の 追跡 issue を付随させるべきです。この issue の本文では、ユーザーがコードを修正するために何をしなければならないかに焦点を当てて、行われる変更を説明するべきです。issue は親しみやすく実用的であるべきです。詳細全体については RFC や他の issue にユーザーを案内するのが適切な場合もあります。この issue は、ユーザーが質問やその他の懸念事項をコメントできる場としても機能します。
これらの破壊的変更の追跡 issue 用テンプレートは こちら にあります。そのような issue がどのようなものになるべきかの例は、こちら にあります。
将来互換性警告の発行
破壊的変更を扱う最良の方法は、将来互換性警告を発行することから始めることです。これは lint 警告の特別なカテゴリです。新しい将来互換性警告は、次のように追加できます。
#![allow(unused)]
fn main() {
// 1. lint を `compiler/rustc_lint/src/builtin.rs` で定義し、
// 将来非互換性のメタデータを追加します:
declare_lint! {
pub YOUR_LINT_HERE,
Warn,
"illegal use of foo bar baz"
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(FutureReleaseError #1234) // ここに追跡 issue を記述します!
},
}
// 2. それ専用の lint pass を追加します。
// 既存の pass の一部として lint を発行する場合、この手順は省略できます。
#[derive(Default)]
pub struct MyLintPass {
...
}
impl {Early,Late}LintPass for MyLintPass {
...
}
impl_lint_pass!(MyLintPass => [YOUR_LINT_HERE]);
// 3. lint pass 内のどこかで lint を発行します:
cx.emit_span_lint(
YOUR_LINT_HERE,
pat.span,
// 何らかの診断 struct
MyDiagnostic {
...
},
);
}
最後に、compiler/rustc_lint/src/lib.rs で lint を登録します。
そのファイルには、すでにその方法を示す例が多数あります。
役立つ手法
新しい警告を、以前から存在する古いエラーから除外するのが難しい場合がよくあります。過去に使われた手法の 1 つは、古いコードを変更せずに実行し、それが報告したであろうエラーを収集することです。そうすれば、その元の集合に含まれない、あなたが出すことになる任意のエラーに対して警告を発行できます。別の選択肢は、元のコードが完了した後にエラーが報告されていればコンパイルを中止することです。そうすれば、新しいコードは以前にエラーがなかった場合にのみ実行されることがわかります。
Crater と crates.io
Crater は、あなたの変更を含むコンパイラを使って、すべての crates.io crate と多くの公開 GitHub リポジトリをコンパイルする bot です。その後、あなたの変更によってコンパイルできなくなった、またはコンパイルできるようになった crate を含むレポートが生成されます。Crater run が完了するまでには数日かかる場合があります。
影響を評価するために、常に crater run を行うべきです。影響を受ける crate の作者に、少なくとも破壊的変更を通知することは丁寧で思いやりのある対応です。問題を修正する PR を送れるなら、それに越したことはありません。
直接エラーを発行することが許容される場合はあるか?
影響が無視できるほど小さいと考えられる変更は、直接エラーの発行に進むことができます。目安の 1 つは crates.io で確認することです。影響を受けるプロジェクトが 合計 10 個未満(ルートエラーではない)であれば、そのままエラーに進めます。このような場合でも、これまでと同様に「破壊的変更」ページを作成し、エラーがユーザーをこのページに誘導するようにする必要があります。言い換えると、ユーザーが警告ではなくエラーを受け取る点を除き、すべては同じであるべきです。さらに、影響を受けるプロジェクトに PR を送るべきです(理想的には、その変更を実装する PR が rustc に取り込まれる前に)。
影響が無視できるほど小さいとは考えられない場合(たとえば、10 個を超えるクレートが影響を受ける場合)、警告が必要です(ただし、特定のケースでコンパイラチームが特別な例外を認めることに同意した場合を除きます)。警告の実装が現実的でない場合は、影響を受けるクレートの数を減らすために、変更を取り込む前にクレートを移行する積極的な戦略を取るべきです。このシナリオに取り組むための手法をいくつか示します。
- 問題の一部に対して警告を発行し、新しいエラーは可能な限り最小のケース集合に限定します。
- 問題の修正方法を示し、ユーザーを追跡 issue に誘導する、非常に正確なエラーメッセージを出すようにします。
- 修正を段階的に行うことも理にかなっている場合があります。
- まず、可能なところに警告を追加し、それらが取り込まれてからエラーの発行に進みます。
- 修正が取り込まれる_前_に修正版が利用可能になるよう、影響を受けるクレートの作者と協力し、下流のユーザーがそれらを使用できるようにします。
安定化
変更が行われた後、その変更は不安定機能に使用しているのと同じプロセスで安定化します。
-
新しいリリースが行われた後、破壊的変更に対応する未解決の追跡 issue を確認し、その一部を最終コメント期間(FCP)に推薦します。
-
このような issue の FCP は 1 サイクル続きます。サイクルの最後の 1、2 週間でコメントを確認し、最終判断を行います。
- エラーに変換: その変更をハードエラーにするべきです。
- 差し戻し: 警告を削除し、古いコードのコンパイルを引き続き許可するべきです。
- 延期: まだ判断できない、もう少し待つ、または他の戦略を試します。
理想的には、破壊的変更は最終化される前に、コンパイラの stable ブランチに取り込まれているべきです。
リントの削除
「future warning」をハードエラーにすることを決定したら、カスタムリントを削除する PR が必要です。例として、overlapping_inherent_impls 互換性リントを削除するために必要な手順を示します。まず、リント名を大文字(OVERLAPPING_INHERENT_IMPLS)に変換し、その文字列をソース全体で ripgrep します。基本的には、このリント名が言及されている各場所を変換します(コンパイラでは大文字の名前を使用し、マクロが自動的に小文字の文字列を生成します。そのため、overlapping_inherent_impls を検索しても多くは見つかりません)。
NOTE: これらの正確なファイルはもう存在しませんが、手順は今も同じです。
リントを削除します。
最初に見つかる可能性が高い参照は、これに似た rustc_session/src/lint/builtin.rs 内のリント定義です :
#![allow(unused)]
fn main() {
declare_lint! {
pub OVERLAPPING_INHERENT_IMPLS,
Deny, // これは Warning と書かれている場合もあります
"two overlapping inherent impls define an item with the same name were erroneously allowed",
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(FutureReleaseError #1234), // ここに追跡 issue を記述してください!
},
}
}
この declare_lint! マクロは、関連するデータ構造を作成します。これを削除します。また、ファイルの後半で OVERLAPPING_INHERENT_IMPLS への言及が lint_array! の一部として見つかるので、それも削除します。
削除されたリントの一覧にリントを追加します。
compiler/rustc_lint/src/lib.rs には、「名前変更および削除されたリント」の一覧があります。このリントを一覧に追加できます。
#![allow(unused)]
fn main() {
store.register_removed("overlapping_inherent_impls", "converted into hard error, see #36889");
}
ここで #36889 はあなたのリントの追跡 issue です。
リントを発行する箇所を更新する
最後に見つかる参照の種類は、実際にリント自体をトリガーする箇所(つまり、警告が表示される原因)です。これらは削除したくありません。代わりに、エラーへ変換したいはずです。この場合、add_lint 呼び出しは次のようになります。
#![allow(unused)]
fn main() {
self.tcx.sess.add_lint(lint::builtin::OVERLAPPING_INHERENT_IMPLS,
node_id,
self.tcx.span_of_impl(item1).unwrap(),
msg);
}
この用途では node_span_lint が使われていることもよくあります。
これをエラーに変換したいです。場合によっては、このシナリオに対する既存のエラーがあるかもしれません。それ以外の場合は、新しい診断コードを割り当てる必要があります。新しい診断コードを割り当てる手順はこちらにあります。 拡張説明の中で、この点に関するコンパイラの動作が変更されたことに触れ、その変更の追跡 issue への参照を含めるとよいでしょう。
E0592 をコードとして採用したとしましょう。その場合、上記の add_lint() 呼び出しを次のようなものに変更できます。
#![allow(unused)]
fn main() {
struct_span_code_err!(self.dcx(), self.tcx.span_of_impl(item1).unwrap(), E0592, msg)
.emit();
}
または、より良い方法として、次のような構造化診断を使います。
#![allow(unused)]
fn main() {
#[derive(Diagnostic)]
struct MyDiagnostic {
#[label]
span: Span,
...
}
}
テストを更新する
最後に、テストスイートを実行します。これらの中には、以前 overlapping_inherent_impls リントを参照していたテストがあるはずで、それらを更新する必要があります。一般的には、テストに #[deny(overlapping_inherent_impls)] があった場合、それは単に削除できます。
./x test
すべて完了です!
PR を開きます。=)