エディション
この章では、rustc におけるエディションサポートの仕組みの概要を説明します。 ここでは、エディションが何であるかを理解していることを前提とします(Edition Guide を参照)。
エディションの定義
--edition CLI フラグは、クレートで使用するエディションを指定します。
これは Session::edition からアクセスできます。
クレートのエディションを確認するための Session::at_least_rust_2021 のような便利な関数がありますが、
グローバルセッションを確認するのか、スパンを確認するのかについては注意する必要があります。下記の
Edition hygiene を参照してください。
at_least_rust_20xx 便利メソッドの代替として、Edition 型は
span.edition() >= Edition::Edition2021 のような範囲チェックを行うための比較もサポートしています。
新しいエディションの追加
新しいエディションを追加するには、主に Edition enum にバリアントを追加し、その後
壊れたものをすべて修正します。例については #94461 を参照してください。
フィーチャーとエディションの安定性
Edition enum は、エディションが安定しているかどうかを定義します。
安定していない場合、それを有効にするには -Zunstable-options CLI オプションを渡す必要があります。
新しいフィーチャーを追加するとき、将来のエディションにおける安定性の扱い方として、次の 2 つの選択肢があります。
span.at_least_rust_20xx()のようにスパンのエディションを確認する(Edition hygiene を参照)か、Session::editionを確認するだけにする。これは、そのフィーチャーが利用可能であることを示すために、 エディション自体の安定性に暗黙的に依存します。- 新しい挙動を feature gate の背後に置く。
比較的単純な変更であれば、現在のエディションだけを確認すれば十分な場合があります。 しかし、より大きな言語変更については、フィーチャーゲートの作成を検討すべきです。 フィーチャーゲートを使用することには、いくつかの利点があります。
- フィーチャーゲートにより、新しいフィーチャーの作業や実験がしやすくなります。
#![feature(…)]属性が使用されたときに、新しいフィーチャーが有効化されているという意図が明確になります。- まだ完成していないフィーチャーが、完成して準備ができているエディション固有のフィーチャーのテストを妨げないようにできるため、エディションのテストが容易になります。
- フィーチャーをエディションから切り離すことで、そのフィーチャーの準備ができたときに、次のエディションに追加すべきかどうかをチームが意図的に判断しやすくなります。
フィーチャーが完成して準備ができたら、フィーチャーゲートを削除できます(そして、そのコードは有効かどうかを判断するために
スパンまたは Session のエディションを確認するだけにすべきです)。
フィーチャーチェックを行うには、いくつかの異なる選択肢があります。
-
エディションに関与するかもしれないし、しないかもしれない非常に実験的なフィーチャーについては、
tcx.features().my_featureのような通常のフィーチャーゲートを実装し、当面はエディションを無視できます。 -
エディションに関与する可能性がある実験的なフィーチャーについては、
tcx.features().my_feature && span.at_least_rust_20xx()を使ってゲートを実装すべきです。 これにより、ユーザーは引き続き#![feature(my_feature)]を指定する必要があり、 エディション内で準備ができて受け入れられている他のエディションフィーチャーのテストを妨げることを避けられます。 -
エディションの一部であることが確定する段階に進んだ実験的なフィーチャーについては、
tcx.features().my_feature || span.at_least_rust_20xx()を使ってゲートを実装するか、 フィーチャーチェックを完全に削除してspan.at_least_rust_20xx()だけを確認すべきです。
複数の場所でフィーチャーゲーティングを行う必要がある場合は、更新箇所が 1 つだけになるように、 チェックを 1 つの関数に置くことを検討してください。例:
// Edition 2021 の disjoint closure captures からの例。
fn enable_precise_capture(tcx: TyCtxt<'_>, span: Span) -> bool {
tcx.features().capture_disjoint_fields || span.rust_2021()
}
Lint が安定性をどのように扱うかについての詳細は、下記の Lint と安定性 を参照してください。
エディションのパース
ほとんどの場合、字句解析器はエディション非依存です。
Lexer 内では、トークンをエディション固有の挙動に基づいて変更できます。
たとえば、c"foo" のような C 文字列リテラルは、2021 より前のエディションでは複数のトークンに分割されます。
2021 エディションの予約済みプレフィックスなども、ここで処理されます。
エディション固有のパースは比較的まれです。1 つの例は async fn で、
トークンのスパンを確認して 2015 エディションかどうかを判断し、その場合はエラーを出力します。
これは、その構文がすでに無効であった場合にのみ実行できます。
パーサー内でエディションチェックを行う必要がある場合、通常はトークンのエディションを見ることになります。
Edition hygiene を参照してください。
まれなケースでは、代わりに ParseSess::edition からグローバルエディションを確認する必要がある場合があります。
ほとんどのエディション固有のパース挙動は、パーサー内ではなく migration lints によって処理されます。
これは、(新しい構文ではなく)構文の変更がある場合に適しています。
これにより、古い構文は以前のエディションで引き続き動作できます。
その後、lint が挙動の変更を確認します。
古いエディションでは、lint パスは新しいエディションへの移行を支援するために移行 lint を出力すべきです。
新しいエディションでは、代わりにコードが emit_err でハードエラーを出力すべきです。
たとえば、非推奨の start...end パターン構文は、2021 より前のエディションでは
ellipsis_inclusive_range_patterns lint を出力し、2021 では emit_err メソッドを介したハードエラーになります。
キーワード
新しいキーワードは、エディションの境界を越えて導入できます。
これは [Symbol::is_used_keyword_conditional] のような関数によって実装されており、
キーワードが定義される順序に依存しています。
新しいキーワードが導入されるときは、そのキーワードを識別子として使用している可能性のあるコードを
自動移行で移行できるように、keyword_idents lint を更新すべきです([KeywordIdents] を参照)。
検討すべき代替案として、そのキーワードが使用される位置だけで区別するのに十分であれば、
そのキーワードを弱いキーワードとして実装することがあります。
検討すべき追加の選択肢として、[RFC 3101] で導入された k# プレフィックスがあります。
これにより、キーワードが導入されるエディションより前のエディションでキーワードを使用できます。
これは現在実装されていません。
[Symbol::is_used_keyword_conditional]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_span/symbol/struct.Symbol.html#method.is_used_keyword_conditional
keyword_idents: https://doc.rust-lang.org/nightly/rustc/lints/listing/allowed-by-default.html#keyword-idents
[KeywordIdents]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/builtin/struct.KeywordIdents.html
[RFC 3101]: https://rust-lang.github.io/rfcs/3101-reserved_prefixes.html
エディションの衛生性
スパンには、そのスパンの由来となったクレートのエディションがマークされています。 これが何を意味するのかについて、ユーザー中心の説明は、Edition Guide のマクロハイジーンを参照してください。
通常は、グローバルな Session のエディションを見るのではなく、トークンのスパンから
エディションを使用するべきです。
たとえば、sess.at_least_rust_2021() の代わりに span.edition().at_least_rust_2021() を使用します。
これは、マクロがクレートをまたいで使用されたときに正しく動作することを保証するのに役立ちます。
lint
lint には、エディションと相互作用するためのいくつかの異なるオプションがあります。 lint は 将来非互換のエディション移行lint にできます。これは、新しいエディションへの 移行をサポートするために使用されます。 あるいは、lint はエディション固有にできます。この場合、特定のエディションから 既定レベルが変更されます。
移行lint
移行lint は、プロジェクトをあるエディションから次のエディションへ移行するために使用されます。
これらは MachineApplicable な提案で実装されており、
コードを書き換え、前のエディションと次のエディションの両方で正常にコンパイルされるようにします。
たとえば、keyword_idents lint は、新しいキーワードと衝突する識別子を対象に、
衝突を避けるため raw identifier 構文を使用するようにします(たとえば async を r#async に変更します)。
移行lint は、lint 宣言内で FutureIncompatibilityReason::EditionError または
FutureIncompatibilityReason::EditionSemanticsChange の将来非互換
オプションを使って宣言しなければなりません。
declare_lint! {
pub KEYWORD_IDENTS,
Allow,
"エディションのキーワードが識別子として使用されていることを検出します",
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(EditionError 2018 "slug-of-edition-guide-page")
};
}
このように宣言すると、その lint は適切な
rust-20xx-compatibility lint グループに自動的に追加されます。
ユーザーが cargo fix --edition を実行すると、cargo は --force-warn rust-20xx-compatibility
フラグを渡し、エディション移行中にこれらすべての lint が表示されるよう強制します。
Cargo はさらに --cap-lints=allow も渡すため、他の lint がエディション移行を妨げることはありません。
サンプルコードが正しいエディションを設定していることを確認してください。サンプルは以前のエディションを示し、移行警告がどのように見えるかを示すべきです。たとえば、2024 移行用のこの lint では、2021 のサンプルを示しています。
declare_lint! {
/// `keyword_idents_2024` lint は ... を検出します
///
/// ### 例
///
/// ```rust,edition2021
/// #![warn(keyword_idents_2024)]
/// fn gen() {}
/// ```
///
/// {{produces}}
}
移行lint の既定値は Allow または Warn のいずれかにできます。
Allow の場合、ユーザーは通常、手動でエディション移行を行っている場合や、
移行中に問題が発生した場合を除き、この警告を見ることはありません。
ほとんどの移行lint は Allow です。
既定で Warn の場合、すべてのエディションのユーザーにこの警告が表示されます。
Warn は、その変更を全員が認識すること、およびすべてのエディションでコードの更新を促すことが
重要だと考える場合にのみ使用してください。
多くのプロジェクトに影響する新しいデフォルト警告lintは、ユーザーに大きな混乱と不満をもたらす可能性が
あることに注意してください。
エディションが安定化してから数年後に Allow を Warn に切り替えることを検討してもよいでしょう。
これは、新しいエディションに更新していない比較的少数のユーザーにのみ表示されます。
エディション固有のlint
lint は、特定のエディションから異なるレベルを持つようにマークできます。
lint 宣言では、@edition マーカーを使用します。
declare_lint! {
pub SOME_LINT_NAME,
Allow,
"自分のlintの説明",
@edition Edition2024 => Warn;
}
ここでは、SOME_LINT_NAME は 2024 より前のすべてのエディションで既定値が Allow で、その後は Warn
になります。
他の選択肢があるため、これは一般に控えめに使用するべきです。
-
エディションに関係しない影響の小さいスタイル上の変更では、単にすべての エディションで lint を
Warnにできます。別の書き方を採用してもらいたいのであれば、思い切って すべてのプロジェクトに表示されるようにすることを決めてください。新しいデフォルト警告lintが多くのプロジェクトに影響すると、ユーザーに大きな混乱と 不満をもたらす可能性があることに注意してください。
-
新しいスタイルを新しいエディションでハードエラーに変更し、移行lintを使って プロジェクトを新しいスタイルへ自動変換します。たとえば、
ellipsis_inclusive_range_patternsは 2021 ではハードエラーであり、それ以前のすべてのエディションでは警告します。これらはエディションの安定化後には追加できないことに注意してください。
-
移行lintも時間とともに変更できます。 たとえば、移行lint は最初は既定で
Allowにできます。 移行を行うユーザーは、自動的に新しいコードへ更新されます。 その後、数年が経ってから、以前のエディションで lint をWarnにできます。たとえば
anonymous_parametersは 2018 Edition の移行lint(かつ 2018 ではハードエラー)であり、 以前のエディションでは既定でAllowでした。 その後、3年後に、それ以前のすべてのエディションでWarnに変更され、すべてのユーザーに そのスタイルが段階的に廃止されつつあるという警告が表示されるようになりました。 これが最初から警告だった場合、多くのプロジェクトに影響し、大きな混乱を招いていたでしょう。 エディションの一部にすることで、ほとんどのユーザーは最終的に新しいエディションへ更新し、 移行によって処理されました。Warnへの切り替えは、更新しなかった少数のユーザーにのみ影響しました。
lint と安定性
リントには不安定であるというマークを付けることができます。これは、新しいエディション機能を開発していて、 移行リントを試したい場合に役立ちます。 機能ゲートは、次のようにリントの宣言で指定できます。
declare_lint! {
pub SOME_LINT_NAME,
Allow,
"my cool lint",
@feature_gate = sym::my_feature_name;
}
すると、そのリントはユーザーが適切な #![feature(my_feature_name)] を持っている場合にのみ発火します。
ただし、移行をテストする crater 実行の時期になったら、その機能ゲートを削除する必要があることに
注意してください。
あるいは、今後の不安定なエディション向けに、機能ゲートなしでデフォルトで許可される 移行リントを実装することもできます。 技術的には、エディションが安定化される前にユーザーがそのリントを有効にできる可能性はありますが、ほとんどのユーザーは 新しいリントの存在に気付かず、何かを妨げたり破壊的変更を引き起こしたりすることはないはずです。
イディオムリント
2018 エディションでは、rust-2018-idioms リントグループの下に「イディオムリント」という概念がありました。
この概念は、rust-2018-compatibility リントグループの下にある強制的な移行とは別のリントグループに、
新しいイディオム的なスタイルを置くことで、特定のエディション変更にどのようにオプトインするかについて
ある程度の柔軟性を与えるものでした。
全体として、このアプローチはあまりうまく機能しなかったようであり、 今後イディオムグループを使用する可能性は低いです。
標準ライブラリの変更
プレリュード
各エディションには、標準ライブラリの特定のプレリュードがあります。
これらは core::prelude と std::prelude の通常のモジュールとして実装されています。
プレリュードには新しい項目を追加できますが、これはユーザーの既存コードと競合する可能性があることに注意してください。
通常は、競合を避けるために既存のコードを移行する 移行リントを使用するべきです。
たとえば、rust_2021_prelude_collisions は、2021 の新しいトレイトとの競合を処理するために使用されます。
カスタマイズされた言語の挙動
通常、標準ライブラリに破壊的変更を加えることはできません。 まれな場合には、チームがこのルールを破ってでも挙動の変更が十分に重要だと判断することがあります。 欠点は、古いシグネチャや挙動と新しいシグネチャや挙動のどちらを使用すべきかを区別できるようにするため、 コンパイラで特別な処理が必要になることです。
一例は、配列の into_iter() のメソッド解決の変更です。
これは IntoIterator トレイトに #[rustc_skip_array_during_method_dispatch] 属性を付けることで実装され、
それによってコンパイラに対して、エディションに基づく代替のトレイト解決の選択肢を考慮するよう伝えます。
もう 1 つの例は、panic! マクロの変更です。
これには複数の panic マクロを定義し、組み込みの panic マクロ実装がそれを展開する適切な方法を
判断する必要がありました。
これには、古いコードを新しい形式に調整するための non_fmt_panics 移行リントも含まれており、
panic マクロの使用を検出するために rustc_diagnostic_item 属性が必要でした。
一般に、非常に価値の高い状況を除き、これらの特殊なケースは避けることをお勧めします。
標準ライブラリのエディションを移行する
標準ライブラリ自体のエディションを更新するには、おおまかに次のプロセスが含まれます。
- 新しく安定化されたエディションが beta に到達し、ブートストラップコンパイラが更新されるまで待ちます。
- 移行リントを適用します。一部のコードは外部サブモジュール1にあり、標準ライブラリは条件付きコンパイルを多用しているため、これは込み入ったプロセスになる可能性があります。また、標準ライブラリ自体に対して
cargo fix --editionを実行するのは実用的でない場合があります。1 つの方法は、各リントについて各クレートの先頭に個別に#![warn(...)]を追加し、./x check libraryを実行し、移行を適用し、#![warn(...)]を削除して、各移行を個別にコミットすることです。完全なカバレッジを得るには、多くの異なるターゲットに対して--target付きで./x checkを実行する必要があるでしょう(そうしないと、おそらく CI を通すのに何日も、あるいは何週間も費やすことになります)2。さらにヒントについては、高度な移行ガイドも参照してください。backtrace-rsに移行を適用します。2024 の例。これはクレート自体のエディションを更新しないことに注意してください。なぜなら、そのクレートは crates.io で独立して公開されており、そうすると最小 Rust バージョンを制限してしまうためです。そのエディションが更新されるまでリグレッションを避けるために、いくつかの#![deny()]属性を追加することを検討してください。stdarchに移行を適用し、そのエディションとフォーマットを更新します。2024 の例。- backtrace と stdarch のサブモジュールを更新する PR を投稿し、それらがマージされるまで待ちます。
- 標準ライブラリのクレートに移行リントを適用し、それらのエディションを更新します。
coreから始めて、1 度に 1 つのクレートに取り組むことをお勧めします。2024 の例。
エディションを安定化する
エディションチームがゴーサインを出した後、エディションを安定化するプロセスはおおまかに次のとおりです。
LATEST_STABLE_EDITIONを更新します。Edition::is_stableを更新します。- エディションを番号で参照しているドキュメントを探し出して更新します。
//@ editionヘッダーを使用しているテストを整理して、-Zunstable-optionsフラグを削除し、それらが実際に安定版であることを確認します。注: 理想的には、これは自動化されるべきです。#133582 を参照してください。- 変更されたテストを bless します。
lint-docsを更新して、新しいエディションをデフォルトにします。
2024 の例を参照してください。
-
将来的には、これらのサブモジュールを
rust-lang/rustに取り込むように変わることが期待されます。 ↩ -
また、多くの異なるターゲットで大量のテストを行う必要がある可能性が高く、そのような場合に docker テスト が役立ちます。 ↩