高级迁移策略

迁移如何工作

cargo fix --edition 的工作原理是使用特殊的lint 对项目运行等效于 cargo check 的操作,这些 lint 将检测可能在下一个版本中无法编译的代码。这些 lint 包括有关如何修改代码以使其与当前版本和下一个版本兼容的说明。cargo fix 将这些更改应用于源代码,然后再次运行 cargo check 以验证修复是否有效。如果修复失败,则会撤消更改并显示警告。

将代码更改为同时与当前版本和下一个版本兼容,可以更轻松地逐步迁移代码。如果自动迁移没有完全成功或需要手动帮助,则可以在更改 Cargo.toml 以使用下一个版本之前,在原始版本上迭代。

cargo fix --edition 应用的 lint 是lint 组的一部分。例如,在从 2018 迁移到 2021 时,Cargo 使用 rust-2021-compatibility lint 组来修复代码。请查看下面的部分迁移部分,了解有关使用单个 lint 来帮助迁移的提示。

cargo fix 可能会多次运行 cargo check。例如,在应用一组修复后,这可能会触发需要进一步修复的新警告。Cargo 会重复此过程,直到不再生成新的警告。

迁移多个配置

cargo fix 一次只能处理单个配置。如果您使用 Cargo 功能条件编译,则可能需要使用不同的标志多次运行 cargo fix

例如,如果您有使用 #[cfg] 属性为不同平台包含不同代码的代码,则可能需要使用 --target 选项运行 cargo fix 以修复不同的目标。如果您没有可用的交叉编译,这可能需要在机器之间移动您的代码。

同样,如果您对 Cargo 功能有条件,例如 #[cfg(feature = "my-optional-thing")],建议使用 --all-features 标志,以允许 cargo fix 迁移这些功能门后面的所有代码。如果您想单独迁移功能代码,可以使用 --features 标志一次迁移一个功能。

迁移大型项目或工作区

如果您遇到问题,可以逐步迁移大型项目,以使过程更容易。

Cargo 工作区中,每个包都定义了自己的版本,因此该过程自然涉及一次迁移一个包。

Cargo 包中,您可以一次迁移整个包,也可以一次迁移单个 Cargo 目标。例如,如果您有多个二进制文件、测试和示例,您可以使用带有 cargo fix --edition 的特定目标选择标志来仅迁移一个目标。默认情况下,cargo fix 使用 --all-targets

对于更高级的情况,您可以在 Cargo.toml 中为每个单独的目标指定版本,如下所示

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

这通常不是必需的,但如果您有很多目标并且难以将它们全部迁移,则可以使用此选项。

使用损坏代码进行部分迁移

有时,编译器建议的修复可能无法工作。发生这种情况时,Cargo 将报告一个警告,指出发生了什么以及错误是什么。但是,默认情况下,它会自动撤消所做的更改。将代码保留在损坏状态并手动解决问题可能会有所帮助。某些修复可能是正确的,而损坏的修复可能大部分正确,但只需要进行微小的调整。

在这种情况下,请使用带有 cargo fix--broken-code 选项,告诉 Cargo 不要撤消更改。然后,您可以手动检查错误并调查需要修复的内容。

逐步迁移项目的另一个选项是分别应用单个修复,一次一个。您可以通过将单个 lint 添加为警告,然后运行 cargo fix(不带 --edition 标志),或者如果您的编辑器或 IDE 支持“快速修复”,则使用您的编辑器或 IDE 来应用其建议。

例如,2018 版本使用 keyword-idents lint 来修复任何冲突的关键字。您可以将 #![warn(keyword_idents)] 添加到每个 crate 的顶部(例如在 src/lib.rssrc/main.rs 的顶部)。然后,运行 cargo fix 将仅应用该 lint 的建议。

您可以在 lint 组页面中查看每个版本启用的 lint 列表,或者运行 rustc -Whelp 命令。

迁移宏

某些宏可能需要手动工作才能修复它们以用于下一个版本。例如,cargo fix --edition 可能无法自动修复生成在下一个版本中不起作用的语法的宏。

对于 过程宏macro_rules 样式宏来说,这可能是一个问题。如果宏在同一 crate 中使用,macro_rules 宏有时可以自动更新,但存在一些无法更新的情况。通常,过程宏根本无法自动修复。

例如,如果我们从 2015 年将包含此(人为的)宏 foo 的 crate 迁移到 2018 年,则 foo 不会自动修复。

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

当此宏在 2015 年的 crate 中定义时,由于宏卫生(如下所述),它可以从任何其他版本的 crate 中使用。在 2015 年,dyn 是一个普通的标识符,可以不受限制地使用。

但是,在 2018 年,dyn 不再是有效的标识符。当使用 cargo fix --edition 迁移到 2018 年时,Cargo 不会显示任何警告或错误。但是,从任何 crate 调用时,foo 都将不起作用。

如果您有宏,建议您确保进行充分涵盖宏语法的测试。您可能还需要通过从多个版本导入并在 crate 中使用它们来测试宏,以确保它在任何地方都能正常工作。如果您遇到问题,则需要通读本指南的章节,以了解如何更改代码以在所有版本中都能工作。

宏卫生

宏使用一个名为“版本卫生”的系统,其中宏中的 token 被标记了它们来自哪个版本。这允许从不同版本的 crate 中调用外部宏,而无需担心它是从哪个版本调用的。

让我们仔细看看上面的示例,该示例使用 dyn 作为标识符定义 macro_rules 宏。如果该宏是在使用 2015 版本的 crate 中定义的,则该宏可以正常工作,即使它从 2018 年的 crate 中调用,其中 dyn 是一个关键字,并且通常是语法错误。let dyn = 1; token 被标记为来自 2015 年,并且编译器会记住代码扩展到的任何地方。解析器会查看 token 的版本,以了解如何解释它。

问题在于将定义它的 crate 中的版本更改为 2018。现在,这些 token 被标记为 2018 版本,并且这些 token 将无法解析。但是,由于我们从未从我们的 crate 调用宏,因此 cargo fix --edition 从未有机会检查宏并对其进行修复。

文档测试

目前,cargo fix 无法更新文档测试。在更新 Cargo.toml 中的版本后,您应该运行 cargo test 以确保一切仍然通过。如果您的文档测试使用新版本不支持的语法,则需要手动更新它们。

在极少数情况下,您可以手动设置每个测试的版本。例如,您可以使用三引号上的 edition2018 注解来告诉 rustdoc 使用哪个版本。

生成的代码

自动修复无法应用的另一个领域是,如果您有一个构建脚本在编译时生成 Rust 代码(有关示例,请参见代码生成)。在这种情况下,如果最终得到的代码在新版本中不起作用,则需要手动更改构建脚本以生成兼容的代码。

迁移非 Cargo 项目

如果您的项目没有使用 Cargo 作为构建系统,则仍然可以使用自动 lint 来帮助迁移到下一个版本。您可以通过启用相应的 lint 组来启用上述迁移 lint。例如,您可以使用 #![warn(rust_2021_compatibility)] 属性或 -Wrust-2021-compatibility--force-warns=rust-2021-compatibility CLI 标志

下一步是将这些 lint 应用于您的代码。这里有几个选项

  • 手动读取警告并应用编译器建议的建议。
  • 使用支持自动应用建议的编辑器或 IDE。例如,使用 Rust Analyzer 扩展Visual Studio Code 能够使用“快速修复”链接来自动应用建议。许多其他编辑器和 IDE 都具有类似的功能。
  • 使用 rustfix 库编写迁移工具。这是 Cargo 内部用来获取来自编译器的 JSON 消息 并修改源代码的库。检查 examples 目录以获取有关如何使用该库的示例。

在新版本中编写惯用代码

版本不仅关乎新功能和删除旧功能。在任何编程语言中,习惯用法都会随着时间的推移而变化,Rust 也不例外。虽然旧代码将继续编译,但今天的习惯用法可能不同。

例如,在 Rust 2015 中,外部 crate 必须使用 extern crate 像这样列出

// src/lib.rs
extern crate rand;

在 Rust 2018 中,不再需要包含这些项。

cargo fix--edition-idioms 选项可以自动将一些这些习惯用法转换为新的语法。

警告:当前“习惯用法 lint”已知存在一些问题。它们可能会提出不正确的建议,从而导致编译失败。当前的 lint 有

以下说明仅推荐给那些愿意克服一些编译器/Cargo 错误的有胆识的人!如果您遇到问题,可以尝试使用 上面描述的 --broken-code 选项,尽可能多地取得进展,然后手动解决剩余的问题。

解决完这些问题后,我们可以指示 Cargo 使用以下命令修复我们的代码片段

cargo fix --edition-idioms

之后,src/lib.rs 中包含 extern crate rand; 的行将被删除。

现在我们的代码更加符合习惯用法了,而且我们不必手动修复它!