From 21629670f418d141555aff056d808cd9ae7de8b7 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sat, 24 May 2025 14:47:54 -0700 Subject: [PATCH] Implement `-Zfix-edition` This adds the implementation for the behavior of `cargo fix -Zfix-edition`. --- src/bin/cargo/commands/fix.rs | 35 +++++---- src/cargo/ops/fix/fix_edition.rs | 108 +++++++++++++++++++++++++++ src/cargo/ops/{fix.rs => fix/mod.rs} | 3 + src/cargo/ops/mod.rs | 4 +- tests/testsuite/fix.rs | 100 +++++++++++++++++++++++++ triagebot.toml | 2 +- 6 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 src/cargo/ops/fix/fix_edition.rs rename src/cargo/ops/{fix.rs => fix/mod.rs} (99%) diff --git a/src/bin/cargo/commands/fix.rs b/src/bin/cargo/commands/fix.rs index 7dd86d27c..f04800e8e 100644 --- a/src/bin/cargo/commands/fix.rs +++ b/src/bin/cargo/commands/fix.rs @@ -91,21 +91,24 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { let allow_dirty = args.flag("allow-dirty"); - ops::fix( - gctx, - &ws, - &mut ops::FixOptions { - edition: args - .flag("edition") - .then_some(ops::EditionFixMode::NextRelative), - idioms: args.flag("edition-idioms"), - compile_opts: opts, - allow_dirty, - allow_staged: allow_dirty || args.flag("allow-staged"), - allow_no_vcs: args.flag("allow-no-vcs"), - broken_code: args.flag("broken-code"), - requested_lockfile_path: lockfile_path, - }, - )?; + let mut opts = ops::FixOptions { + edition: args + .flag("edition") + .then_some(ops::EditionFixMode::NextRelative), + idioms: args.flag("edition-idioms"), + compile_opts: opts, + allow_dirty, + allow_staged: allow_dirty || args.flag("allow-staged"), + allow_no_vcs: args.flag("allow-no-vcs"), + broken_code: args.flag("broken-code"), + requested_lockfile_path: lockfile_path, + }; + + if let Some(fe) = &gctx.cli_unstable().fix_edition { + ops::fix_edition(gctx, &ws, &mut opts, fe)?; + } else { + ops::fix(gctx, &ws, &mut opts)?; + } + Ok(()) } diff --git a/src/cargo/ops/fix/fix_edition.rs b/src/cargo/ops/fix/fix_edition.rs new file mode 100644 index 000000000..966ddf948 --- /dev/null +++ b/src/cargo/ops/fix/fix_edition.rs @@ -0,0 +1,108 @@ +//! Support for the permanently unstable `-Zfix-edition` flag. + +use super::{EditionFixMode, FixOptions}; +use crate::core::features::{Edition, FixEdition}; +use crate::core::{Package, Workspace}; +use crate::ops; +use crate::util::toml_mut::manifest::LocalManifest; +use crate::{CargoResult, GlobalContext}; +use toml_edit::{Formatted, Item, Value}; + +/// Performs the actions for the `-Zfix-edition` flag. +pub fn fix_edition( + gctx: &GlobalContext, + original_ws: &Workspace<'_>, + opts: &mut FixOptions, + fix_edition: &FixEdition, +) -> CargoResult<()> { + let packages = opts.compile_opts.spec.get_packages(original_ws)?; + let skip_if_not_edition = |edition| -> CargoResult { + if !packages.iter().all(|p| p.manifest().edition() == edition) { + gctx.shell().status( + "Skipping", + &format!("not all packages are at edition {edition}"), + )?; + Ok(true) + } else { + Ok(false) + } + }; + + match fix_edition { + FixEdition::Start(edition) => { + // The start point just runs `cargo check` if the edition is the + // starting edition. This is so that crater can set a baseline of + // whether or not the package builds at all. For other editions, + // we skip entirely since they are not of interest since we can't + // migrate them. + if skip_if_not_edition(*edition)? { + return Ok(()); + } + ops::compile(&original_ws, &opts.compile_opts)?; + } + FixEdition::End { initial, next } => { + // Skip packages that are not the starting edition, since we can + // only migrate from one edition to the next. + if skip_if_not_edition(*initial)? { + return Ok(()); + } + // Do the edition fix. + opts.edition = Some(EditionFixMode::OverrideSpecific(*next)); + opts.allow_dirty = true; + opts.allow_no_vcs = true; + opts.allow_staged = true; + ops::fix(gctx, original_ws, opts)?; + // Do `cargo check` with the new edition so that we can verify + // that it also works on the next edition. + replace_edition(&packages, *next)?; + gctx.shell() + .status("Updated", &format!("edition to {next}"))?; + let ws = original_ws.reload(gctx)?; + // Unset these since we just want to do a normal `cargo check`. + *opts + .compile_opts + .build_config + .rustfix_diagnostic_server + .borrow_mut() = None; + opts.compile_opts.build_config.primary_unit_rustc = None; + + ops::compile(&ws, &opts.compile_opts)?; + } + } + Ok(()) +} + +/// Modifies the `edition` value of the given packages to the new edition. +fn replace_edition(packages: &[&Package], to_edition: Edition) -> CargoResult<()> { + for package in packages { + let mut manifest_mut = LocalManifest::try_new(package.manifest_path())?; + let document = &mut manifest_mut.data; + let root = document.as_table_mut(); + // Update edition to the new value. + if let Some(package) = root.get_mut("package").and_then(|t| t.as_table_like_mut()) { + package.insert( + "edition", + Item::Value(Value::String(Formatted::new(to_edition.to_string()))), + ); + } + // If the edition is unstable, add it to cargo-features. + if !to_edition.is_stable() { + let feature = "unstable-editions"; + + if let Some(features) = root + .entry("cargo-features") + .or_insert_with(|| Item::Value(Value::Array(toml_edit::Array::new()))) + .as_array_mut() + { + if !features + .iter() + .any(|f| f.as_str().map_or(false, |f| f == feature)) + { + features.push(feature); + } + } + } + manifest_mut.write()?; + } + Ok(()) +} diff --git a/src/cargo/ops/fix.rs b/src/cargo/ops/fix/mod.rs similarity index 99% rename from src/cargo/ops/fix.rs rename to src/cargo/ops/fix/mod.rs index d739006ef..b2723e601 100644 --- a/src/cargo/ops/fix.rs +++ b/src/cargo/ops/fix/mod.rs @@ -51,6 +51,7 @@ use rustfix::CodeFix; use semver::Version; use tracing::{debug, trace, warn}; +pub use self::fix_edition::fix_edition; use crate::core::compiler::CompileKind; use crate::core::compiler::RustcTargetData; use crate::core::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor}; @@ -66,6 +67,8 @@ use crate::util::GlobalContext; use crate::util::{existing_vcs_repo, LockServer, LockServerClient}; use crate::{drop_eprint, drop_eprintln}; +mod fix_edition; + /// **Internal only.** /// Indicates Cargo is in fix-proxy-mode if presents. /// The value of it is the socket address of the [`LockServer`] being used. diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 15a69968c..2431b73d0 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -26,7 +26,9 @@ pub use self::cargo_update::upgrade_manifests; pub use self::cargo_update::write_manifest_upgrades; pub use self::cargo_update::UpdateOptions; pub use self::common_for_install_and_uninstall::{resolve_root, InstallTracker}; -pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, EditionFixMode, FixOptions}; +pub use self::fix::{ + fix, fix_edition, fix_exec_rustc, fix_get_proxy_lock_addr, EditionFixMode, FixOptions, +}; pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile}; pub use self::registry::info; pub use self::registry::modify_owners; diff --git a/tests/testsuite/fix.rs b/tests/testsuite/fix.rs index 6597a55bd..f6fe79934 100644 --- a/tests/testsuite/fix.rs +++ b/tests/testsuite/fix.rs @@ -2963,3 +2963,103 @@ dep_df_false = { workspace = true, default-features = false } "#]], ); } + +#[cargo_test] +fn fix_edition_skips_old_editions() { + // Checks that -Zfix-edition will skip things that are not 2024. + let p = project() + .file( + "Cargo.toml", + r#"[workspace] + members = ["e2021", "e2024"] + resolver = "3" + "#, + ) + .file( + "e2021/Cargo.toml", + r#" + [package] + name = "e2021" + edition = "2021" + "#, + ) + .file("e2021/src/lib.rs", "") + .file( + "e2024/Cargo.toml", + r#" + [package] + name = "e2024" + edition = "2024" + "#, + ) + .file("e2024/src/lib.rs", "") + .build(); + + // Doing the whole workspace should skip since there is a 2021 in the mix. + p.cargo("fix -Zfix-edition=start=2024 -v") + .masquerade_as_nightly_cargo(&["fix-edition"]) + .with_stderr_data(str![[r#" +[SKIPPING] not all packages are at edition 2024 + +"#]]) + .run(); + + // Same with `end`. + p.cargo("fix -Zfix-edition=end=2024,future -v") + .masquerade_as_nightly_cargo(&["fix-edition"]) + .with_stderr_data(str![[r#" +[SKIPPING] not all packages are at edition 2024 + +"#]]) + .run(); + + // Doing an individual package at the correct edition should check it. + p.cargo("fix -Zfix-edition=start=2024 -p e2024") + .masquerade_as_nightly_cargo(&["fix-edition"]) + .with_stderr_data(str![[r#" +[CHECKING] e2024 v0.0.0 ([ROOT]/foo/e2024) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test(nightly, reason = "future edition is always unstable")] +fn fix_edition_future() { + // Checks that the -Zfix-edition can work for the future. + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + edition = "2024""#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("fix -Zfix-edition=end=2024,future") + .masquerade_as_nightly_cargo(&["fix-edition"]) + .with_stderr_data(str![[r#" +[MIGRATING] Cargo.toml from 2024 edition to future +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[MIGRATING] src/lib.rs from 2024 edition to future +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + Updated edition to future +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + assert_e2e().eq( + p.read_file("Cargo.toml"), + str![[r#" +cargo-features = ["unstable-editions"] + + [package] + name = "foo" +edition = "future" + +"#]], + ); +} diff --git a/triagebot.toml b/triagebot.toml index 7d5244e14..8dcef7b27 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -279,7 +279,7 @@ trigger_files = ["src/bin/cargo/commands/fetch.rs", "src/cargo/ops/cargo_fetch.r trigger_files = [ "crates/rustfix/", "src/bin/cargo/commands/fix.rs", - "src/cargo/ops/fix.rs", + "src/cargo/ops/fix/", "src/cargo/util/diagnostic_server.rs", "src/cargo/util/lockserver.rs", ]