発展的な移行戦略

移行ツールの仕組み

cargo fix --edition コマンドは、cargo check コマンドと同様のコマンドを、次のエディションでコンパイルされなくなるコードを検知する特別なリントが有効になった状態で実行することで機能します。 このリントには、コードを変更したら現在と次のエディションの双方に適合させるための指示も含まれています。 cargo fix コマンドはソースコードをそれに従って変更し、再び cargo check を実行して修正がうまく行ったか確認します。 うまく行かなかった場合、変更を巻き戻して警告を表示します。

現在と次のエディションの両方に同時に適合したコードに書き換えると、コードを段階的に移行することが楽になります。 自動移行が完全には成功しなかったか、手作業で変えてやる必要がある場合は、元ののエディション留まったまま同じことを繰り返してから、Cargo.toml を編集して次のエディションに進めてもよいです。

cargo fix --edition が適用するリントは、リントグループの一部です。 例えば、2018 から 2021 に移行する場合、Cargo は rust-2021-compatibility というリントグループをコードの修正に使用します。 それぞれのリントを移行に役立てるコツについては、後の「部分的な移行」の章をご覧ください。

cargo fix は、cargo check を複数回実行する可能性があります。 たとえば、一通りの修正を終えても、修正によって新たな警告が出て、さらなる修正が必要になるかもしれません。 Cargo は、新しく警告が出なくなるまでこれを繰り返し続けます。

複数の設定を移行する

cargo fix は一度に1つのコンフィギュレーションでしか動きません。 Cargo のフィーチャ条件付きコンパイルを使用している場合、cargo fix を異なるフラグで複数回実行する必要があるかもしれません。

例えば、あなたのコードが #[cfg] を使ってプラットフォームによって違うコードを含むようになっていた場合、cargo fix--target オプションつきで実行して、ターゲットごとに修正をする必要があるかもしれません。 クロスコンパイルができない場合、コードを別のマシンに移して作業せざるを得ないかもしれません。

同様に、フィーチャによる条件分岐、例えば #[cfg(feature = "my-optional-thing")] のようなものがある場合、--all-features フラグを使って、フィーチャの壁を超えて cargo fix がすべてのコードを変更できるようにするとよいでしょう。 フィーチャごとに別々にコードを移行したい場合は、--features フラグを使って一つずつ移行作業をすることもできます。

巨大なプロジェクトやワークスペースの移行

大きなプロジェクトで問題が発生した場合、作業を単純にするために、段階的に移行していくこともできます。

Cargo のワークスペースでは、エディションはパッケージごとに定義されているため、自然とパッケージごとに1つずつ移行をすることになります。

Cargo のパッケージにおいては、全パッケージを同時に移行することも、Cargo ターゲットごとに1つずつ移行することもできます。 例えば、パッケージに複数のバイナリ、テスト、サンプルコード(example ターゲット)がある場合、cargo fix --edition コマンドに特定のターゲット選択用のフラグを組み合わせることで、一つのターゲットだけを移行することもできます。 デフォルトでは、cargo fix--all-targets が暗黙に指定されていると扱います。

より発展的には、Cargo.toml 中に各ターゲットで使用するエディションを指定することもできます:

[[bin]]
name = "my-binary"
edition = "2018"

おそらく普通はこれは必要ありませんが、ターゲットがたくさんあって全部いっぺんに移行作業できないような場合には一つの選択肢になるでしょう。

壊れたコードを元に部分的に移行する

ときどき、コンパイラに提案された修正ではうまくいかないことがあります。 すると、Cargo は何が起こったかとどんなエラーが出たかを示す警告を報告しますが、デフォルトでは Cargo は変更を巻き戻します。 しかし、Cargo にはコードを壊れたままにしておいてもらい、手作業で問題を解決する、というのも有効な手段です。 ほとんどの修正は正しいかもしれませんし、壊れた修正もだいたい正しくて、少しいじれば問題ないかもしれないのです。

そんなときは、cargo fix コマンドを実行するときに --broken-code オプションをつけて、Cargo が変更を巻き戻さないようにできます。 そうすれば、エラーを実際に見てみることも、修正点を確認することもできます。

プロジェクトを段階的に移行するもう一つの方法は、それぞれの修正を一つずつ適用することです。 このためには、各リントを警告として追加して、(--edition なしの)cargo fix を実行するか、お手持ちのエディタや IDE が「クイックフィックス」をサポートしていればそれを使って提案を適用すればよいです。

例えば、2018 エディションには keyword-idents という、キーワードとの衝突をすべて修正するためのリントがあります。 各クレートのトップ(例えば src/lib.rssrc/main.rs の先頭)に #![warn(keyword_idents)] を追加して、cargo fix を実行すれば、そのリントによる提案だけを受け入れることができます。

各エディションで有効化されるリントの一覧は、リントグループのページを見るか、 rustc -Whelp コマンドを実行すれば確認できます。

マクロの移行

マクロの中には、エディションを進めるにあたって手作業が必要なものがあります。 例えば、cargo fix --edition は、次のエディションで動作しない文法を生成するマクロを自動修正することは難しいかもしれません。

これは、手続き型マクロmacro_rules を使ったマクロの双方で問題になります。 macro_rules を使ったマクロは、マクロが同じクレートに属していたら自動でアップデートできる場合もありますが、いくつかの状況ではできません。 手続き型マクロは原則、全く修正できないと言っていいでしょう。

例えば、この(わざとらしい)マクロ foo を含むクレートを 2015 から 2018 に移行しようとしても、foo は自動修復されません。


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! foo {
    () => {
        let dyn = 1;
        println!("it is {}", dyn);
              // "これは {} です"
    };
}
}

マクロが 2015 のクレートで定義されている場合、マクロの衛生性(後述)のおかげでこれは他のエディションのクレートからも使用することができます。 2015 では、dyn は通常の識別子で、制限なく使用できます。

一方で、2018 では、dyn はもはや正当な識別子ではありません。 cargo fix --edition で 2018 へ移行するとき、Cargo は警告やエラーを一切表示しません。 しかし、foo はどのクレートで呼び出されても動作しません。

あなたのコードにマクロがある場合、そのマクロの構文を十分に網羅するテストがあることが推奨されます。 また、そのマクロを複数のエディションのクレートの中でインポートして使用し、どこでも動くということを確認するのも良いでしょう。 問題に突き当たった場合、このガイドの本章に目を通して、全エディションで動作するようにするためにどうすればいいか理解する必要があります。

マクロの衛生性

マクロには、「エディション衛生性」と呼ばれる仕組みが使われています。このシステムでは、マクロ内のトークンがどのエディションから来たかが記録されています。 これにより、呼び出す側のエディションがどれであるかを気にせずに、外部のマクロを呼び出すことができます。

上で例に出した、macro_rules で定義されたマクロを詳しく見てみましょう。 このマクロは dyn を識別子に使用しています。 このマクロが定義されているのが 2015 エディションのクレート内なのならば、これは問題なく動きます。 さらに、2018 エディションでは dyn はキーワードで普通は識別子に使えませんが、2018 エディションのクレートからこのマクロを呼び出したとしても大丈夫です。 パーサーはトークンのエディションに着目して、どう解釈したらよいかを判断します。

問題が起こるのは、マクロが定義されている側のクレートのエディションを 2018 に変更したときです。 今や、これらのトークンは 2018 エディションのものとしてタグ付けされているために、パースに失敗します。 しかしながら、このマクロは自身のクレートから呼び出されてもいないために、cargo fix --edition はこれを検査することも修正することもできません。

ドキュメンテーションテスト

現在のところ、cargo fixドキュメンテーションテストを更新することはできません。 Cargo.toml でエディションを更新したら、cargo test を実行して全てのテストに通過することを確認すべきです。 新しいエディションでサポートされない構文がドキュメンテーションテストに使われていた場合は、手作業で修正する必要があります。

まれなケースですが、エディションをテストごとに設定することもできます。 例えば、バッククォート3つの後に edition2018 アノテーションをつければ、rustdoc が使うエディションを指定できます。

生成されるコード

自動移行が使えない場所がもう一つあります。それは、ビルドスクリプトが Rust コードをコンパイル時に生成する場合です(具体例は「コード生成」をご参照ください)。 このとき、出てくるコードが次のエディションで機能しない場合は、生成されるコードが新しいエディションに適合するように、ビルドスクリプトを自分で書き換える必要があります。

Cargo でないプロジェクトの移行

プロジェクトのビルドシステムが Cargo 以外の場合でも、次のエディションへの移行に自動リントが利用できるかもしれません。 適切なリントグループを使って、前述の移行リントを有効化できます。 例えば、#![warn(rust_2021_compatibility)] というアトリビュートを使ったり、-Wrust-2021-compatibility--force-warns=rust-2021-compatibility などの CLI フラグを使用するとよいです。

次に、これらのリントをコードに適用します。 これにはいくつかの手があります:

  • 警告を読んで、コンパイラに提案された変更を自分で加える。
  • エディタや IDE の機能で、コンパイラからの提案を適用する。 例えば、Visual Studio CodeRust Analyzer 拡張 を使えば、自動的に提案を受け入れるための「クイックフィックス」が使えます。 他にも多くのエディタで同様の機能が使えます。
  • rustfix ライブラリを用いて、移行ツールを自作する。 このライブラリは Cargo 内部でも使われており、コンパイラからの JSON メッセージを元にソースコードを編集します。 ライブラリの使用例は、examples ディレクトリをご覧ください。

新しいエディションで慣用的な書き方をする

エディションは、新機能の追加や古い機能の削除のためだけのものではありません。 どんなプログラミング言語でも、その言語らしい書き方は時代によって変化します。Rust も同じです。 古いプログラムはコンパイルには通るかもしれませんが、今は別の書き方があるかもしれません。

例えば、Rust 2015 では、外部クレートは以下のように extern crate で明示的に宣言される必要がありました:

// src/lib.rs
extern crate rand;

Rust 2018 では、外部クレートを使うのにextern crate は必要ありません

cargo fix には --edition-idioms オプションがあり、古い書き方(イディオム)の一部を新しい書き方に書き換えることができます。

警告: 現行の「イディオムリント」にはいくつか問題があることが知られています。 これらのリントはときどき、受け入れるとコンパイルできなくなるような誤った提案をすることがあります。 現在、以下のリントがあります。

以下の手順は、コンパイラや Cargo の多少のバグを厭わない恐れ知らずだけにしかお勧めできません!  不都合が発生する場合は、前述--broken-code オプションを使ってツールにやれるだけのことをさせ、残った問題を自分の手で解決してもよいでしょう。

ともあれ、先程の短いコードを Cargo に直してもらうにはこうすればよいです:

cargo fix --edition-idioms

すると、src/lib.rs に書かれた extern crate rand; が削除されます。

これで、自分でコードに手を下すことなく、コードを現代風にできました!