#
cargo/ops/fix/
mod.rs

    1//! High-level overview of how `fix` works:
    2//!
    3//! The main goal is to run `cargo check` to get rustc to emit JSON
    4//! diagnostics with suggested fixes that can be applied to the files on the
    5//! filesystem, and validate that those changes didn't break anything.
    6//!
    7//! Cargo begins by launching a [`LockServer`] thread in the background to
    8//! listen for network connections to coordinate locking when multiple targets
    9//! are built simultaneously. It ensures each package has only one fix running
    10//! at once.
    11//!
    12//! The [`RustfixDiagnosticServer`] is launched in a background thread (in
    13//! `JobQueue`) to listen for network connections to coordinate displaying
    14//! messages to the user on the console (so that multiple processes don't try
    15//! to print at the same time).
    16//!
    17//! Cargo begins a normal `cargo check` operation with itself set as a proxy
    18//! for rustc by setting `BuildConfig::primary_unit_rustc` in the build config. When
    19//! cargo launches rustc to check a crate, it is actually launching itself.
    20//! The `FIX_ENV_INTERNAL` environment variable is set to the value of the [`LockServer`]'s
    21//! address so that cargo knows it is in fix-proxy-mode.
    22//!
    23//! Each proxied cargo-as-rustc detects it is in fix-proxy-mode (via `FIX_ENV_INTERNAL`
    24//! environment variable in `main`) and does the following:
    25//!
    26//! - Acquire a lock from the [`LockServer`] from the master cargo process.
    27//! - Launches the real rustc ([`rustfix_and_fix`]), looking at the JSON output
    28//!   for suggested fixes.
    29//! - Uses the `rustfix` crate to apply the suggestions to the files on the
    30//!   file system.
    31//! - If rustfix fails to apply any suggestions (for example, they are
    32//!   overlapping), but at least some suggestions succeeded, it will try the
    33//!   previous two steps up to 4 times as long as some suggestions succeed.
    34//! - Assuming there's at least one suggestion applied, and the suggestions
    35//!   applied cleanly, rustc is run again to verify the suggestions didn't
    36//!   break anything. The change will be backed out if it fails (unless
    37//!   `--broken-code` is used).
    38
    39use std::collections::{BTreeSet, HashMap, HashSet};
    40use std::ffi::OsString;
    41use std::io::Write;
    42use std::path::{Path, PathBuf};
    43use std::process::{self, ExitStatus, Output};
    44use std::{env, fs, str};
    45
    46use anyhow::{Context as _, bail};
    47use cargo_util::{ProcessBuilder, exit_status_to_string, is_simple_exit_code, paths};
    48use cargo_util_schemas::manifest::TomlManifest;
    49use rustfix::CodeFix;
    50use rustfix::diagnostics::Diagnostic;
    51use semver::Version;
    52use tracing::{debug, trace, warn};
    53
    54pub use self::fix_edition::fix_edition;
    55use crate::core::PackageIdSpecQuery as _;
    56use crate::core::compiler::CompileKind;
    57use crate::core::compiler::RustcTargetData;
    58use crate::core::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor};
    59use crate::core::resolver::{HasDevUnits, Resolve, ResolveBehavior};
    60use crate::core::{Edition, MaybePackage, Package, PackageId, Workspace};
    61use crate::ops::resolve::WorkspaceResolve;
    62use crate::ops::{self, CompileOptions};
    63use crate::util::GlobalContext;
    64use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
    65use crate::util::errors::CargoResult;
    66use crate::util::toml_mut::manifest::LocalManifest;
    67use crate::util::{LockServer, LockServerClient, existing_vcs_repo};
    68use crate::{drop_eprint, drop_eprintln};
    69
    70mod fix_edition;
    71
    72/// **Internal only.**
    73/// Indicates Cargo is in fix-proxy-mode if presents.
    74/// The value of it is the socket address of the [`LockServer`] being used.
    75/// See the [module-level documentation](mod@super::fix) for more.
    76const FIX_ENV_INTERNAL: &str = "__CARGO_FIX_PLZ";
    77/// **Internal only.**
    78/// For passing [`FixOptions::broken_code`] through to cargo running in proxy mode.
    79const BROKEN_CODE_ENV_INTERNAL: &str = "__CARGO_FIX_BROKEN_CODE";
    80/// **Internal only.**
    81/// For passing [`FixOptions::edition`] through to cargo running in proxy mode.
    82const EDITION_ENV_INTERNAL: &str = "__CARGO_FIX_EDITION";
    83/// **Internal only.**
    84/// For passing [`FixOptions::idioms`] through to cargo running in proxy mode.
    85const IDIOMS_ENV_INTERNAL: &str = "__CARGO_FIX_IDIOMS";
    86/// **Internal only.**
    87/// The sysroot path.
    88///
    89/// This is for preventing `cargo fix` from fixing rust std/core libs. See
    90///
    91/// * <https://github.com/rust-lang/cargo/issues/9857>
    92/// * <https://github.com/rust-lang/rust/issues/88514#issuecomment-2043469384>
    93const SYSROOT_INTERNAL: &str = "__CARGO_FIX_RUST_SRC";
    94
    95pub struct FixOptions {
    96    pub edition: Option<EditionFixMode>,
    97    pub idioms: bool,
    98    pub compile_opts: CompileOptions,
    99    pub allow_dirty: bool,
    100    pub allow_no_vcs: bool,
    101    pub allow_staged: bool,
    102    pub broken_code: bool,
    103    pub requested_lockfile_path: Option<PathBuf>,
    104}
    105
    106/// The behavior of `--edition` migration.
    107#[derive(Clone, Copy)]
    108pub enum EditionFixMode {
    109    /// Migrates the package from the current edition to the next.
    110    ///
    111    /// This is the normal (stable) behavior of `--edition`.
    112    NextRelative,
    113    /// Migrates to a specific edition.
    114    ///
    115    /// This is used by `-Zfix-edition` to force a specific edition like
    116    /// `future`, which does not have a relative value.
    117    OverrideSpecific(Edition),
    118}
    119
    120impl EditionFixMode {
    121    /// Returns the edition to use for the given current edition.
    122    pub fn next_edition(&self, current_edition: Edition) -> Edition {
    123        match self {
    124            EditionFixMode::NextRelative => current_edition.saturating_next(),
    125            EditionFixMode::OverrideSpecific(edition) => *edition,
    126        }
    127    }
    128
    129    /// Serializes to a string.
    130    fn to_string(&self) -> String {
    131        match self {
    132            EditionFixMode::NextRelative => "1".to_string(),
    133            EditionFixMode::OverrideSpecific(edition) => edition.to_string(),
    134        }
    135    }
    136
    137    /// Deserializes from the given string.
    138    fn from_str(s: &str) -> EditionFixMode {
    139        match s {
    140            "1" => EditionFixMode::NextRelative,
    141            edition => EditionFixMode::OverrideSpecific(edition.parse().unwrap()),
    142        }
    143    }
    144}
    145
    146pub fn fix(
    147    gctx: &GlobalContext,
    148    original_ws: &Workspace<'_>,
    149    opts: &mut FixOptions,
    150) -> CargoResult<()> {
    151    check_version_control(gctx, opts)?;
    152
    153    let mut target_data =
    154        RustcTargetData::new(original_ws, &opts.compile_opts.build_config.requested_kinds)?;
    155    if let Some(edition_mode) = opts.edition {
    156        let specs = opts.compile_opts.spec.to_package_id_specs(&original_ws)?;
    157        let members: Vec<&Package> = original_ws
    158            .members()
    159            .filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
    160            .collect();
    161        migrate_manifests(original_ws, &members, edition_mode)?;
    162
    163        check_resolver_change(&original_ws, &mut target_data, opts)?;
    164    }
    165    let ws = original_ws.reload(gctx)?;
    166
    167    // Spin up our lock server, which our subprocesses will use to synchronize fixes.
    168    let lock_server = LockServer::new()?;
    169    let mut wrapper = ProcessBuilder::new(env::current_exe()?);
    170    wrapper.env(FIX_ENV_INTERNAL, lock_server.addr().to_string());
    171    let _started = lock_server.start()?;
    172
    173    opts.compile_opts.build_config.force_rebuild = true;
    174
    175    if opts.broken_code {
    176        wrapper.env(BROKEN_CODE_ENV_INTERNAL, "1");
    177    }
    178
    179    if let Some(mode) = &opts.edition {
    180        wrapper.env(EDITION_ENV_INTERNAL, mode.to_string());
    181    }
    182    if opts.idioms {
    183        wrapper.env(IDIOMS_ENV_INTERNAL, "1");
    184    }
    185
    186    let sysroot = &target_data.info(CompileKind::Host).sysroot;
    187    if sysroot.is_dir() {
    188        wrapper.env(SYSROOT_INTERNAL, sysroot);
    189    }
    190
    191    *opts
    192        .compile_opts
    193        .build_config
    194        .rustfix_diagnostic_server
    195        .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
    196
    197    if let Some(server) = opts
    198        .compile_opts
    199        .build_config
    200        .rustfix_diagnostic_server
    201        .borrow()
    202        .as_ref()
    203    {
    204        server.configure(&mut wrapper);
    205    }
    206
    207    let rustc = ws.gctx().load_global_rustc(Some(&ws))?;
    208    wrapper.arg(&rustc.path);
    209    // This is calling rustc in cargo fix-proxy-mode, so it also need to retry.
    210    // The argfile handling are located at `FixArgs::from_args`.
    211    wrapper.retry_with_argfile(true);
    212
    213    // primary crates are compiled using a cargo subprocess to do extra work of applying fixes and
    214    // repeating build until there are no more changes to be applied
    215    opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
    216
    217    ops::compile(&ws, &opts.compile_opts)?;
    218    Ok(())
    219}
    220
    221fn check_version_control(gctx: &GlobalContext, opts: &FixOptions) -> CargoResult<()> {
    222    if opts.allow_no_vcs {
    223        return Ok(());
    224    }
    225    if !existing_vcs_repo(gctx.cwd(), gctx.cwd()) {
    226        bail!(
    227            "no VCS found for this package and `cargo fix` can potentially \
    228             perform destructive changes; if you'd like to suppress this \
    229             error pass `--allow-no-vcs`"
    230        )
    231    }
    232
    233    if opts.allow_dirty && opts.allow_staged {
    234        return Ok(());
    235    }
    236
    237    let mut dirty_files = Vec::new();
    238    let mut staged_files = Vec::new();
    239    if let Ok(repo) = git2::Repository::discover(gctx.cwd()) {
    240        let mut repo_opts = git2::StatusOptions::new();
    241        repo_opts.include_ignored(false);
    242        repo_opts.include_untracked(true);
    243        for status in repo.statuses(Some(&mut repo_opts))?.iter() {
    244            if let Some(path) = status.path() {
    245                match status.status() {
    246                    git2::Status::CURRENT => (),
    247                    git2::Status::INDEX_NEW
    248                    | git2::Status::INDEX_MODIFIED
    249                    | git2::Status::INDEX_DELETED
    250                    | git2::Status::INDEX_RENAMED
    251                    | git2::Status::INDEX_TYPECHANGE => {
    252                        if !opts.allow_staged {
    253                            staged_files.push(path.to_string())
    254                        }
    255                    }
    256                    _ => {
    257                        if !opts.allow_dirty {
    258                            dirty_files.push(path.to_string())
    259                        }
    260                    }
    261                };
    262            }
    263        }
    264    }
    265
    266    if dirty_files.is_empty() && staged_files.is_empty() {
    267        return Ok(());
    268    }
    269
    270    let mut files_list = String::new();
    271    for file in dirty_files {
    272        files_list.push_str("  * ");
    273        files_list.push_str(&file);
    274        files_list.push_str(" (dirty)\n");
    275    }
    276    for file in staged_files {
    277        files_list.push_str("  * ");
    278        files_list.push_str(&file);
    279        files_list.push_str(" (staged)\n");
    280    }
    281
    282    bail!(
    283        "the working directory of this package has uncommitted changes, and \
    284         `cargo fix` can potentially perform destructive changes; if you'd \
    285         like to suppress this error pass `--allow-dirty`, \
    286         or commit the changes to these files:\n\
    287         \n\
    288         {}\n\
    289         ",
    290        files_list
    291    );
    292}
    293
    294fn migrate_manifests(
    295    ws: &Workspace<'_>,
    296    pkgs: &[&Package],
    297    edition_mode: EditionFixMode,
    298) -> CargoResult<()> {
    299    // HACK: Duplicate workspace migration logic between virtual manifests and real manifests to
    300    // reduce multiple Migrating messages being reported for the same file to the user
    301    if matches!(ws.root_maybe(), MaybePackage::Virtual(_)) {
    302        // Warning: workspaces do not have an edition so this should only include changes needed by
    303        // packages that preserve the behavior of the workspace on all editions
    304        let highest_edition = pkgs
    305            .iter()
    306            .map(|p| p.manifest().edition())
    307            .max()
    308            .unwrap_or_default();
    309        let prepare_for_edition = edition_mode.next_edition(highest_edition);
    310        if highest_edition == prepare_for_edition
    311            || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
    312        {
    313            //
    314        } else {
    315            let mut manifest_mut = LocalManifest::try_new(ws.root_manifest())?;
    316            let document = &mut manifest_mut.data;
    317            let mut fixes = 0;
    318
    319            if Edition::Edition2024 <= prepare_for_edition {
    320                let root = document.as_table_mut();
    321
    322                if let Some(workspace) = root
    323                    .get_mut("workspace")
    324                    .and_then(|t| t.as_table_like_mut())
    325                {
    326                    // strictly speaking, the edition doesn't apply to this table but it should be safe
    327                    // enough
    328                    fixes += rename_dep_fields_2024(workspace, "dependencies");
    329                }
    330            }
    331
    332            if 0 < fixes {
    333                // HACK: As workspace migration is a special case, only report it if something
    334                // happened
    335                let file = ws.root_manifest();
    336                let file = file.strip_prefix(ws.root()).unwrap_or(file);
    337                let file = file.display();
    338                ws.gctx().shell().status(
    339                    "Migrating",
    340                    format!("{file} from {highest_edition} edition to {prepare_for_edition}"),
    341                )?;
    342
    343                let verb = if fixes == 1 { "fix" } else { "fixes" };
    344                let msg = format!("{file} ({fixes} {verb})");
    345                ws.gctx().shell().status("Fixed", msg)?;
    346
    347                manifest_mut.write()?;
    348            }
    349        }
    350    }
    351
    352    for pkg in pkgs {
    353        let existing_edition = pkg.manifest().edition();
    354        let prepare_for_edition = edition_mode.next_edition(existing_edition);
    355        if existing_edition == prepare_for_edition
    356            || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
    357        {
    358            continue;
    359        }
    360        let file = pkg.manifest_path();
    361        let file = file.strip_prefix(ws.root()).unwrap_or(file);
    362        let file = file.display();
    363        ws.gctx().shell().status(
    364            "Migrating",
    365            format!("{file} from {existing_edition} edition to {prepare_for_edition}"),
    366        )?;
    367
    368        let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
    369        let document = &mut manifest_mut.data;
    370        let mut fixes = 0;
    371
    372        let ws_original_toml = match ws.root_maybe() {
    373            MaybePackage::Package(package) => package.manifest().original_toml(),
    374            MaybePackage::Virtual(manifest) => manifest.original_toml(),
    375        };
    376
    377        if Edition::Edition2024 <= prepare_for_edition {
    378            let root = document.as_table_mut();
    379
    380            if let Some(workspace) = root
    381                .get_mut("workspace")
    382                .and_then(|t| t.as_table_like_mut())
    383            {
    384                // strictly speaking, the edition doesn't apply to this table but it should be safe
    385                // enough
    386                fixes += rename_dep_fields_2024(workspace, "dependencies");
    387            }
    388
    389            fixes += rename_table(root, "project", "package");
    390            if let Some(target) = root.get_mut("lib").and_then(|t| t.as_table_like_mut()) {
    391                fixes += rename_target_fields_2024(target);
    392            }
    393            fixes += rename_array_of_target_fields_2024(root, "bin");
    394            fixes += rename_array_of_target_fields_2024(root, "example");
    395            fixes += rename_array_of_target_fields_2024(root, "test");
    396            fixes += rename_array_of_target_fields_2024(root, "bench");
    397            fixes += rename_dep_fields_2024(root, "dependencies");
    398            fixes += remove_ignored_default_features_2024(root, "dependencies", ws_original_toml);
    399            fixes += rename_table(root, "dev_dependencies", "dev-dependencies");
    400            fixes += rename_dep_fields_2024(root, "dev-dependencies");
    401            fixes +=
    402                remove_ignored_default_features_2024(root, "dev-dependencies", ws_original_toml);
    403            fixes += rename_table(root, "build_dependencies", "build-dependencies");
    404            fixes += rename_dep_fields_2024(root, "build-dependencies");
    405            fixes +=
    406                remove_ignored_default_features_2024(root, "build-dependencies", ws_original_toml);
    407            for target in root
    408                .get_mut("target")
    409                .and_then(|t| t.as_table_like_mut())
    410                .iter_mut()
    411                .flat_map(|t| t.iter_mut())
    412                .filter_map(|(_k, t)| t.as_table_like_mut())
    413            {
    414                fixes += rename_dep_fields_2024(target, "dependencies");
    415                fixes +=
    416                    remove_ignored_default_features_2024(target, "dependencies", ws_original_toml);
    417                fixes += rename_table(target, "dev_dependencies", "dev-dependencies");
    418                fixes += rename_dep_fields_2024(target, "dev-dependencies");
    419                fixes += remove_ignored_default_features_2024(
    420                    target,
    421                    "dev-dependencies",
    422                    ws_original_toml,
    423                );
    424                fixes += rename_table(target, "build_dependencies", "build-dependencies");
    425                fixes += rename_dep_fields_2024(target, "build-dependencies");
    426                fixes += remove_ignored_default_features_2024(
    427                    target,
    428                    "build-dependencies",
    429                    ws_original_toml,
    430                );
    431            }
    432        }
    433
    434        if 0 < fixes {
    435            let verb = if fixes == 1 { "fix" } else { "fixes" };
    436            let msg = format!("{file} ({fixes} {verb})");
    437            ws.gctx().shell().status("Fixed", msg)?;
    438
    439            manifest_mut.write()?;
    440        }
    441    }
    442
    443    Ok(())
    444}
    445
    446fn rename_dep_fields_2024(parent: &mut dyn toml_edit::TableLike, dep_kind: &str) -> usize {
    447    let mut fixes = 0;
    448    for target in parent
    449        .get_mut(dep_kind)
    450        .and_then(|t| t.as_table_like_mut())
    451        .iter_mut()
    452        .flat_map(|t| t.iter_mut())
    453        .filter_map(|(_k, t)| t.as_table_like_mut())
    454    {
    455        fixes += rename_table(target, "default_features", "default-features");
    456    }
    457    fixes
    458}
    459
    460fn remove_ignored_default_features_2024(
    461    parent: &mut dyn toml_edit::TableLike,
    462    dep_kind: &str,
    463    ws_original_toml: Option<&TomlManifest>,
    464) -> usize {
    465    let Some(ws_original_toml) = ws_original_toml else {
    466        return 0;
    467    };
    468
    469    let mut fixes = 0;
    470    for (name_in_toml, target) in parent
    471        .get_mut(dep_kind)
    472        .and_then(|t| t.as_table_like_mut())
    473        .iter_mut()
    474        .flat_map(|t| t.iter_mut())
    475        .filter_map(|(k, t)| t.as_table_like_mut().map(|t| (k, t)))
    476    {
    477        let name_in_toml: &str = &name_in_toml;
    478        let ws_deps = ws_original_toml
    479            .workspace
    480            .as_ref()
    481            .and_then(|ws| ws.dependencies.as_ref());
    482        if let Some(ws_dep) = ws_deps.and_then(|ws_deps| ws_deps.get(name_in_toml)) {
    483            if ws_dep.default_features() == Some(false) {
    484                continue;
    485            }
    486        }
    487        if target
    488            .get("workspace")
    489            .and_then(|i| i.as_value())
    490            .and_then(|i| i.as_bool())
    491            == Some(true)
    492            && target
    493                .get("default-features")
    494                .and_then(|i| i.as_value())
    495                .and_then(|i| i.as_bool())
    496                == Some(false)
    497        {
    498            target.remove("default-features");
    499            fixes += 1;
    500        }
    501    }
    502    fixes
    503}
    504
    505fn rename_array_of_target_fields_2024(root: &mut dyn toml_edit::TableLike, kind: &str) -> usize {
    506    let mut fixes = 0;
    507    for target in root
    508        .get_mut(kind)
    509        .and_then(|t| t.as_array_of_tables_mut())
    510        .iter_mut()
    511        .flat_map(|t| t.iter_mut())
    512    {
    513        fixes += rename_target_fields_2024(target);
    514    }
    515    fixes
    516}
    517
    518fn rename_target_fields_2024(target: &mut dyn toml_edit::TableLike) -> usize {
    519    let mut fixes = 0;
    520    fixes += rename_table(target, "crate_type", "crate-type");
    521    fixes += rename_table(target, "proc_macro", "proc-macro");
    522    fixes
    523}
    524
    525fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> usize {
    526    let Some(old_key) = parent.key(old).cloned() else {
    527        return 0;
    528    };
    529
    530    let project = parent.remove(old).expect("returned early");
    531    if !parent.contains_key(new) {
    532        parent.insert(new, project);
    533        let mut new_key = parent.key_mut(new).expect("just inserted");
    534        *new_key.dotted_decor_mut() = old_key.dotted_decor().clone();
    535        *new_key.leaf_decor_mut() = old_key.leaf_decor().clone();
    536    }
    537    1
    538}
    539
    540fn check_resolver_change<'gctx>(
    541    ws: &Workspace<'gctx>,
    542    target_data: &mut RustcTargetData<'gctx>,
    543    opts: &FixOptions,
    544) -> CargoResult<()> {
    545    let root = ws.root_maybe();
    546    match root {
    547        MaybePackage::Package(root_pkg) => {
    548            if root_pkg.manifest().resolve_behavior().is_some() {
    549                // If explicitly specified by the user, no need to check.
    550                return Ok(());
    551            }
    552            // Only trigger if updating the root package from 2018.
    553            let pkgs = opts.compile_opts.spec.get_packages(ws)?;
    554            if !pkgs.contains(&root_pkg) {
    555                // The root is not being migrated.
    556                return Ok(());
    557            }
    558            if root_pkg.manifest().edition() != Edition::Edition2018 {
    559                // V1 to V2 only happens on 2018 to 2021.
    560                return Ok(());
    561            }
    562        }
    563        MaybePackage::Virtual(_vm) => {
    564            // Virtual workspaces don't have a global edition to set (yet).
    565            return Ok(());
    566        }
    567    }
    568    // 2018 without `resolver` set must be V1
    569    assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
    570    let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
    571    let mut resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> {
    572        let dry_run = false;
    573        let ws_resolve = ops::resolve_ws_with_opts(
    574            ws,
    575            target_data,
    576            &opts.compile_opts.build_config.requested_kinds,
    577            &opts.compile_opts.cli_features,
    578            &specs,
    579            has_dev_units,
    580            crate::core::resolver::features::ForceAllTargets::No,
    581            dry_run,
    582        )?;
    583
    584        let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units);
    585        let v2_features = FeatureResolver::resolve(
    586            ws,
    587            target_data,
    588            &ws_resolve.targeted_resolve,
    589            &ws_resolve.pkg_set,
    590            &opts.compile_opts.cli_features,
    591            &specs,
    592            &opts.compile_opts.build_config.requested_kinds,
    593            feature_opts,
    594        )?;
    595
    596        if ws_resolve.specs_and_features.len() != 1 {
    597            bail!(r#"cannot fix edition when using `feature-unification = "package"`."#);
    598        }
    599        let resolved_features = &ws_resolve
    600            .specs_and_features
    601            .first()
    602            .expect("We've already checked that there is exactly one.")
    603            .resolved_features;
    604        let diffs = v2_features.compare_legacy(resolved_features);
    605        Ok((ws_resolve, diffs))
    606    };
    607    let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?;
    608    let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?;
    609    if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() {
    610        // Nothing is different, nothing to report.
    611        return Ok(());
    612    }
    613    // Only display unique changes with dev-dependencies.
    614    with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals));
    615    let gctx = ws.gctx();
    616    gctx.shell().note(
    617        "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
    618    )?;
    619    drop_eprintln!(
    620        gctx,
    621        "This may cause some dependencies to be built with fewer features enabled than previously."
    622    );
    623    drop_eprintln!(
    624        gctx,
    625        "More information about the resolver changes may be found \
    626         at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
    627    );
    628    drop_eprintln!(
    629        gctx,
    630        "When building the following dependencies, \
    631         the given features will no longer be used:\n"
    632    );
    633    let show_diffs = |differences: DiffMap| {
    634        for ((pkg_id, features_for), removed) in differences {
    635            drop_eprint!(gctx, "  {}", pkg_id);
    636            if let FeaturesFor::HostDep = features_for {
    637                drop_eprint!(gctx, " (as host dependency)");
    638            }
    639            drop_eprint!(gctx, " removed features: ");
    640            let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
    641            drop_eprintln!(gctx, "{}", joined.join(", "));
    642        }
    643        drop_eprint!(gctx, "\n");
    644    };
    645    if !without_dev_diffs.is_empty() {
    646        show_diffs(without_dev_diffs);
    647    }
    648    if !with_dev_diffs.is_empty() {
    649        drop_eprintln!(
    650            gctx,
    651            "The following differences only apply when building with dev-dependencies:\n"
    652        );
    653        show_diffs(with_dev_diffs);
    654    }
    655    report_maybe_diesel(gctx, &ws_resolve.targeted_resolve)?;
    656    Ok(())
    657}
    658
    659fn report_maybe_diesel(gctx: &GlobalContext, resolve: &Resolve) -> CargoResult<()> {
    660    fn is_broken_diesel(pid: PackageId) -> bool {
    661        pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8)
    662    }
    663
    664    fn is_broken_diesel_migration(pid: PackageId) -> bool {
    665        pid.name() == "diesel_migrations" && pid.version().major <= 1
    666    }
    667
    668    if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) {
    669        gctx.shell().note(
    670            "\
    671This project appears to use both diesel and diesel_migrations. These packages have
    672a known issue where the build may fail due to the version 2 resolver preventing
    673feature unification between those two packages. Please update to at least diesel 1.4.8
    674to prevent this issue from happening.
    675",
    676        )?;
    677    }
    678    Ok(())
    679}
    680
    681/// Provide the lock address when running in proxy mode
    682///
    683/// Returns `None` if `fix` is not being run (not in proxy mode). Returns
    684/// `Some(...)` if in `fix` proxy mode
    685pub fn fix_get_proxy_lock_addr() -> Option<String> {
    686    #[expect(
    687        clippy::disallowed_methods,
    688        reason = "internal only, no reason for config support"
    689    )]
    690    env::var(FIX_ENV_INTERNAL).ok()
    691}
    692
    693/// Entry point for `cargo` running as a proxy for `rustc`.
    694///
    695/// This is called every time `cargo` is run to check if it is in proxy mode.
    696///
    697/// If there are warnings or errors, this does not return,
    698/// and the process exits with the corresponding `rustc` exit code.
    699///
    700/// See [`fix_get_proxy_lock_addr`]
    701pub fn fix_exec_rustc(gctx: &GlobalContext, lock_addr: &str) -> CargoResult<()> {
    702    let args = FixArgs::get()?;
    703    trace!("cargo-fix as rustc got file {:?}", args.file);
    704
    705    let workspace_rustc = gctx
    706        .get_env("RUSTC_WORKSPACE_WRAPPER")
    707        .map(PathBuf::from)
    708        .ok();
    709    let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
    710    rustc.retry_with_argfile(true);
    711    rustc.env_remove(FIX_ENV_INTERNAL);
    712    args.apply(&mut rustc);
    713    // Removes `FD_CLOEXEC` set by `jobserver::Client` to ensure that the
    714    // compiler can access the jobserver.
    715    if let Some(client) = gctx.jobserver_from_env() {
    716        rustc.inherit_jobserver(client);
    717    }
    718
    719    trace!("start rustfixing {:?}", args.file);
    720    let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, gctx)?;
    721
    722    if fixes.last_output.status.success() {
    723        for (path, file) in fixes.files.iter() {
    724            Message::Fixed {
    725                file: path.clone(),
    726                fixes: file.fixes_applied,
    727            }
    728            .post(gctx)?;
    729        }
    730        // Display any remaining diagnostics.
    731        emit_output(&fixes.last_output)?;
    732        return Ok(());
    733    }
    734
    735    let allow_broken_code = gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some();
    736
    737    // There was an error running rustc during the last run.
    738    //
    739    // Back out all of the changes unless --broken-code was used.
    740    if !allow_broken_code {
    741        for (path, file) in fixes.files.iter() {
    742            debug!("reverting {:?} due to errors", path);
    743            paths::write(path, &file.original_code)?;
    744        }
    745    }
    746
    747    // If there were any fixes, let the user know that there was a failure
    748    // attempting to apply them, and to ask for a bug report.
    749    //
    750    // FIXME: The error message here is not correct with --broken-code.
    751    //        https://github.com/rust-lang/cargo/issues/10955
    752    if fixes.files.is_empty() {
    753        // No fixes were available. Display whatever errors happened.
    754        emit_output(&fixes.last_output)?;
    755        exit_with(fixes.last_output.status);
    756    } else {
    757        let krate = {
    758            let mut iter = rustc.get_args();
    759            let mut krate = None;
    760            while let Some(arg) = iter.next() {
    761                if arg == "--crate-name" {
    762                    krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
    763                }
    764            }
    765            krate
    766        };
    767        log_failed_fix(
    768            gctx,
    769            krate,
    770            &fixes.last_output.stderr,
    771            fixes.last_output.status,
    772        )?;
    773        // Display the diagnostics that appeared at the start, before the
    774        // fixes failed. This can help with diagnosing which suggestions
    775        // caused the failure.
    776        emit_output(&fixes.first_output)?;
    777        // Exit with whatever exit code we initially started with. `cargo fix`
    778        // treats this as a warning, and shouldn't return a failure code
    779        // unless the code didn't compile in the first place.
    780        exit_with(fixes.first_output.status);
    781    }
    782}
    783
    784fn emit_output(output: &Output) -> CargoResult<()> {
    785    // Unfortunately if there is output on stdout, this does not preserve the
    786    // order of output relative to stderr. In practice, rustc should never
    787    // print to stdout unless some proc-macro does it.
    788    std::io::stderr().write_all(&output.stderr)?;
    789    std::io::stdout().write_all(&output.stdout)?;
    790    Ok(())
    791}
    792
    793struct FixedCrate {
    794    /// Map of file path to some information about modifications made to that file.
    795    files: HashMap<String, FixedFile>,
    796    /// The output from rustc from the first time it was called.
    797    ///
    798    /// This is needed when fixes fail to apply, so that it can display the
    799    /// original diagnostics to the user which can help with diagnosing which
    800    /// suggestions caused the failure.
    801    first_output: Output,
    802    /// The output from rustc from the last time it was called.
    803    ///
    804    /// This will be displayed to the user to show any remaining diagnostics
    805    /// or errors.
    806    last_output: Output,
    807}
    808
    809#[derive(Debug)]
    810struct FixedFile {
    811    errors_applying_fixes: Vec<String>,
    812    fixes_applied: u32,
    813    original_code: String,
    814}
    815
    816/// Attempts to apply fixes to a single crate.
    817///
    818/// This runs `rustc` (possibly multiple times) to gather suggestions from the
    819/// compiler and applies them to the files on disk.
    820fn rustfix_crate(
    821    lock_addr: &str,
    822    rustc: &ProcessBuilder,
    823    filename: &Path,
    824    args: &FixArgs,
    825    gctx: &GlobalContext,
    826) -> CargoResult<FixedCrate> {
    827    // First up, we want to make sure that each crate is only checked by one
    828    // process at a time. If two invocations concurrently check a crate then
    829    // it's likely to corrupt it.
    830    //
    831    // Historically this used per-source-file locking, then per-package
    832    // locking. It now uses a single, global lock as some users do things like
    833    // #[path] or include!() of shared files between packages. Serializing
    834    // makes it slower, but is the only safe way to prevent concurrent
    835    // modification.
    836    let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
    837
    838    // Map of files that have been modified.
    839    let mut files = HashMap::new();
    840
    841    if !args.can_run_rustfix(gctx)? {
    842        // This fix should not be run. Skipping...
    843        // We still need to run rustc at least once to make sure any potential
    844        // rmeta gets generated, and diagnostics get displayed.
    845        debug!("can't fix {filename:?}, running rustc: {rustc}");
    846        let last_output = rustc.output()?;
    847        let fixes = FixedCrate {
    848            files,
    849            first_output: last_output.clone(),
    850            last_output,
    851        };
    852        return Ok(fixes);
    853    }
    854
    855    // Next up, this is a bit suspicious, but we *iteratively* execute rustc and
    856    // collect suggestions to feed to rustfix. Once we hit our limit of times to
    857    // execute rustc or we appear to be reaching a fixed point we stop running
    858    // rustc.
    859    //
    860    // This is currently done to handle code like:
    861    //
    862    //      ::foo::<::Bar>();
    863    //
    864    // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
    865    // The spans for these two suggestions are overlapping and its difficult in
    866    // the compiler to **not** have overlapping spans here. As a result, a naive
    867    // implementation would feed the two compiler suggestions for the above fix
    868    // into `rustfix`, but one would be rejected because it overlaps with the
    869    // other.
    870    //
    871    // In this case though, both suggestions are valid and can be automatically
    872    // applied! To handle this case we execute rustc multiple times, collecting
    873    // fixes each time we do so. Along the way we discard any suggestions that
    874    // failed to apply, assuming that they can be fixed the next time we run
    875    // rustc.
    876    //
    877    // Naturally, we want a few protections in place here though to avoid looping
    878    // forever or otherwise losing data. To that end we have a few termination
    879    // conditions:
    880    //
    881    // * Do this whole process a fixed number of times. In theory we probably
    882    //   need an infinite number of times to apply fixes, but we're not gonna
    883    //   sit around waiting for that.
    884    // * If it looks like a fix genuinely can't be applied we need to bail out.
    885    //   Detect this when a fix fails to get applied *and* no suggestions
    886    //   successfully applied to the same file. In that case looks like we
    887    //   definitely can't make progress, so bail out.
    888    let max_iterations = gctx
    889        .get_env("CARGO_FIX_MAX_RETRIES")
    890        .ok()
    891        .and_then(|n| n.parse().ok())
    892        .unwrap_or(4);
    893    let mut last_output;
    894    let mut last_made_changes;
    895    let mut first_output = None;
    896    let mut current_iteration = 0;
    897    loop {
    898        for file in files.values_mut() {
    899            // We'll generate new errors below.
    900            file.errors_applying_fixes.clear();
    901        }
    902        (last_output, last_made_changes) =
    903            rustfix_and_fix(&mut files, rustc, filename, args, gctx)?;
    904        if current_iteration == 0 {
    905            first_output = Some(last_output.clone());
    906        }
    907        let mut progress_yet_to_be_made = false;
    908        for (path, file) in files.iter_mut() {
    909            if file.errors_applying_fixes.is_empty() {
    910                continue;
    911            }
    912            debug!("had rustfix apply errors in {path:?} {file:?}");
    913            // If anything was successfully fixed *and* there's at least one
    914            // error, then assume the error was spurious and we'll try again on
    915            // the next iteration.
    916            if last_made_changes {
    917                progress_yet_to_be_made = true;
    918            }
    919        }
    920        if !progress_yet_to_be_made {
    921            break;
    922        }
    923        current_iteration += 1;
    924        if current_iteration >= max_iterations {
    925            break;
    926        }
    927    }
    928    if last_made_changes {
    929        debug!("calling rustc one last time for final results: {rustc}");
    930        last_output = rustc.output()?;
    931    }
    932
    933    // Any errors still remaining at this point need to be reported as probably
    934    // bugs in Cargo and/or rustfix.
    935    for (path, file) in files.iter_mut() {
    936        for error in file.errors_applying_fixes.drain(..) {
    937            Message::ReplaceFailed {
    938                file: path.clone(),
    939                message: error,
    940            }
    941            .post(gctx)?;
    942        }
    943    }
    944
    945    Ok(FixedCrate {
    946        files,
    947        first_output: first_output.expect("at least one iteration"),
    948        last_output,
    949    })
    950}
    951
    952/// Executes `rustc` to apply one round of suggestions to the crate in question.
    953///
    954/// This will fill in the `fixes` map with original code, suggestions applied,
    955/// and any errors encountered while fixing files.
    956fn rustfix_and_fix(
    957    files: &mut HashMap<String, FixedFile>,
    958    rustc: &ProcessBuilder,
    959    filename: &Path,
    960    args: &FixArgs,
    961    gctx: &GlobalContext,
    962) -> CargoResult<(Output, bool)> {
    963    // If not empty, filter by these lints.
    964    // TODO: implement a way to specify this.
    965    let only = HashSet::new();
    966
    967    debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
    968    let output = rustc.output()?;
    969
    970    // If rustc didn't succeed for whatever reasons then we're very likely to be
    971    // looking at otherwise broken code. Let's not make things accidentally
    972    // worse by applying fixes where a bug could cause *more* broken code.
    973    // Instead, punt upwards which will reexec rustc over the original code,
    974    // displaying pretty versions of the diagnostics we just read out.
    975    if !output.status.success() && gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() {
    976        debug!(
    977            "rustfixing `{:?}` failed, rustc exited with {:?}",
    978            filename,
    979            output.status.code()
    980        );
    981        return Ok((output, false));
    982    }
    983
    984    let fix_mode = gctx
    985        .get_env_os("__CARGO_FIX_YOLO")
    986        .map(|_| rustfix::Filter::Everything)
    987        .unwrap_or(rustfix::Filter::MachineApplicableOnly);
    988
    989    // Sift through the output of the compiler to look for JSON messages.
    990    // indicating fixes that we can apply.
    991    let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
    992
    993    let suggestions = stderr
    994        .lines()
    995        .filter(|x| !x.is_empty())
    996        .inspect(|y| trace!("line: {}", y))
    997        // Parse each line of stderr, ignoring errors, as they may not all be JSON.
    998        .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
    999        // From each diagnostic, try to extract suggestions from rustc.
    1000        .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
    1001
    1002    // Collect suggestions by file so we can apply them one at a time later.
    1003    let mut file_map = HashMap::new();
    1004    let mut num_suggestion = 0;
    1005    // It's safe since we won't read any content under home dir.
    1006    let home_path = gctx.home().as_path_unlocked();
    1007    for suggestion in suggestions {
    1008        trace!("suggestion");
    1009        // Make sure we've got a file associated with this suggestion and all
    1010        // snippets point to the same file. Right now it's not clear what
    1011        // we would do with multiple files.
    1012        let file_names = suggestion
    1013            .solutions
    1014            .iter()
    1015            .flat_map(|s| s.replacements.iter())
    1016            .map(|r| &r.snippet.file_name);
    1017
    1018        let file_name = if let Some(file_name) = file_names.clone().next() {
    1019            file_name.clone()
    1020        } else {
    1021            trace!("rejecting as it has no solutions {:?}", suggestion);
    1022            continue;
    1023        };
    1024
    1025        let file_path = Path::new(&file_name);
    1026        // Do not write into registry cache. See rust-lang/cargo#9857.
    1027        if file_path.starts_with(home_path) {
    1028            continue;
    1029        }
    1030        // Do not write into standard library source. See rust-lang/cargo#9857.
    1031        if let Some(sysroot) = args.sysroot.as_deref() {
    1032            if file_path.starts_with(sysroot) {
    1033                continue;
    1034            }
    1035        }
    1036
    1037        if !file_names.clone().all(|f| f == &file_name) {
    1038            trace!("rejecting as it changes multiple files: {:?}", suggestion);
    1039            continue;
    1040        }
    1041
    1042        trace!("adding suggestion for {:?}: {:?}", file_name, suggestion);
    1043        file_map
    1044            .entry(file_name)
    1045            .or_insert_with(Vec::new)
    1046            .push(suggestion);
    1047        num_suggestion += 1;
    1048    }
    1049
    1050    debug!(
    1051        "collected {} suggestions for `{}`",
    1052        num_suggestion,
    1053        filename.display(),
    1054    );
    1055
    1056    let mut made_changes = false;
    1057    for (file, suggestions) in file_map {
    1058        // Attempt to read the source code for this file. If this fails then
    1059        // that'd be pretty surprising, so log a message and otherwise keep
    1060        // going.
    1061        let code = match paths::read(file.as_ref()) {
    1062            Ok(s) => s,
    1063            Err(e) => {
    1064                warn!("failed to read `{}`: {}", file, e);
    1065                continue;
    1066            }
    1067        };
    1068        let num_suggestions = suggestions.len();
    1069        debug!("applying {} fixes to {}", num_suggestions, file);
    1070
    1071        // If this file doesn't already exist then we just read the original
    1072        // code, so save it. If the file already exists then the original code
    1073        // doesn't need to be updated as we've just read an interim state with
    1074        // some fixes but perhaps not all.
    1075        let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile {
    1076            errors_applying_fixes: Vec::new(),
    1077            fixes_applied: 0,
    1078            original_code: code.clone(),
    1079        });
    1080        let mut fixed = CodeFix::new(&code);
    1081
    1082        for suggestion in suggestions.iter().rev() {
    1083            // As mentioned above in `rustfix_crate`,
    1084            // we don't immediately warn about suggestions that fail to apply here,
    1085            // and instead we save them off for later processing.
    1086            //
    1087            // However, we don't bother reporting conflicts that exactly match prior replacements.
    1088            // This is currently done to reduce noise for things like rust-lang/rust#51211,
    1089            // although it may be removed if that's fixed deeper in the compiler.
    1090            match fixed.apply(suggestion) {
    1091                Ok(()) => fixed_file.fixes_applied += 1,
    1092                Err(rustfix::Error::AlreadyReplaced {
    1093                    is_identical: true, ..
    1094                }) => continue,
    1095                Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
    1096            }
    1097        }
    1098        if fixed.modified() {
    1099            made_changes = true;
    1100            let new_code = fixed.finish()?;
    1101            paths::write(&file, new_code)?;
    1102        }
    1103    }
    1104
    1105    Ok((output, made_changes))
    1106}
    1107
    1108fn exit_with(status: ExitStatus) -> ! {
    1109    #[cfg(unix)]
    1110    {
    1111        use std::os::unix::prelude::*;
    1112        if let Some(signal) = status.signal() {
    1113            drop(writeln!(
    1114                std::io::stderr().lock(),
    1115                "child failed with signal `{}`",
    1116                signal
    1117            ));
    1118            process::exit(2);
    1119        }
    1120    }
    1121    process::exit(status.code().unwrap_or(3));
    1122}
    1123
    1124fn log_failed_fix(
    1125    gctx: &GlobalContext,
    1126    krate: Option<String>,
    1127    stderr: &[u8],
    1128    status: ExitStatus,
    1129) -> CargoResult<()> {
    1130    let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
    1131
    1132    let diagnostics = stderr
    1133        .lines()
    1134        .filter(|x| !x.is_empty())
    1135        .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
    1136    let mut files = BTreeSet::new();
    1137    let mut errors = Vec::new();
    1138    for diagnostic in diagnostics {
    1139        errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
    1140        for span in diagnostic.spans.into_iter() {
    1141            files.insert(span.file_name);
    1142        }
    1143    }
    1144    // Include any abnormal messages (like an ICE or whatever).
    1145    errors.extend(
    1146        stderr
    1147            .lines()
    1148            .filter(|x| !x.starts_with('{'))
    1149            .map(|x| x.to_string()),
    1150    );
    1151
    1152    let files = files.into_iter().collect();
    1153    let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) {
    1154        None
    1155    } else {
    1156        Some(exit_status_to_string(status))
    1157    };
    1158    Message::FixFailed {
    1159        files,
    1160        krate,
    1161        errors,
    1162        abnormal_exit,
    1163    }
    1164    .post(gctx)?;
    1165
    1166    Ok(())
    1167}
    1168
    1169/// Various command-line options and settings used when `cargo` is running as
    1170/// a proxy for `rustc` during the fix operation.
    1171struct FixArgs {
    1172    /// This is the `.rs` file that is being fixed.
    1173    file: PathBuf,
    1174    /// If `--edition` is used to migrate to the next edition, this is the
    1175    /// edition we are migrating towards.
    1176    prepare_for_edition: Option<Edition>,
    1177    /// `true` if `--edition-idioms` is enabled.
    1178    idioms: bool,
    1179    /// The current edition.
    1180    ///
    1181    /// `None` if on 2015.
    1182    enabled_edition: Option<Edition>,
    1183    /// Other command-line arguments not reflected by other fields in
    1184    /// `FixArgs`.
    1185    other: Vec<OsString>,
    1186    /// Path to the `rustc` executable.
    1187    rustc: PathBuf,
    1188    /// Path to host sysroot.
    1189    sysroot: Option<PathBuf>,
    1190}
    1191
    1192impl FixArgs {
    1193    fn get() -> CargoResult<FixArgs> {
    1194        Self::from_args(env::args_os())
    1195    }
    1196
    1197    // This is a separate function so that we can use it in tests.
    1198    fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> {
    1199        let mut argv = argv.into_iter();
    1200        let mut rustc = argv
    1201            .nth(1)
    1202            .map(PathBuf::from)
    1203            .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?;
    1204        let mut file = None;
    1205        let mut enabled_edition = None;
    1206        let mut other = Vec::new();
    1207
    1208        let mut handle_arg = |arg: OsString| -> CargoResult<()> {
    1209            let path = PathBuf::from(arg);
    1210            if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
    1211                file = Some(path);
    1212                return Ok(());
    1213            }
    1214            if let Some(s) = path.to_str() {
    1215                if let Some(edition) = s.strip_prefix("--edition=") {
    1216                    enabled_edition = Some(edition.parse()?);
    1217                    return Ok(());
    1218                }
    1219            }
    1220            other.push(path.into());
    1221            Ok(())
    1222        };
    1223
    1224        if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") {
    1225            // Because cargo in fix-proxy-mode might hit the command line size limit,
    1226            // cargo fix need handle `@path` argfile for this special case.
    1227            if argv.next().is_some() {
    1228                bail!("argfile `@path` cannot be combined with other arguments");
    1229            }
    1230            let contents = fs::read_to_string(argfile_path)
    1231                .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?;
    1232            let mut iter = contents.lines().map(OsString::from);
    1233            rustc = iter
    1234                .next()
    1235                .map(PathBuf::from)
    1236                .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
    1237            for arg in iter {
    1238                handle_arg(arg)?;
    1239            }
    1240        } else {
    1241            for arg in argv {
    1242                handle_arg(arg)?;
    1243            }
    1244        }
    1245
    1246        let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
    1247        #[expect(
    1248            clippy::disallowed_methods,
    1249            reason = "internal only, no reason for config support"
    1250        )]
    1251        let idioms = env::var(IDIOMS_ENV_INTERNAL).is_ok();
    1252
    1253        #[expect(
    1254            clippy::disallowed_methods,
    1255            reason = "internal only, no reason for config support"
    1256        )]
    1257        let prepare_for_edition = env::var(EDITION_ENV_INTERNAL).ok().map(|v| {
    1258            let enabled_edition = enabled_edition.unwrap_or(Edition::Edition2015);
    1259            let mode = EditionFixMode::from_str(&v);
    1260            mode.next_edition(enabled_edition)
    1261        });
    1262
    1263        #[expect(
    1264            clippy::disallowed_methods,
    1265            reason = "internal only, no reason for config support"
    1266        )]
    1267        let sysroot = env::var_os(SYSROOT_INTERNAL).map(PathBuf::from);
    1268
    1269        Ok(FixArgs {
    1270            file,
    1271            prepare_for_edition,
    1272            idioms,
    1273            enabled_edition,
    1274            other,
    1275            rustc,
    1276            sysroot,
    1277        })
    1278    }
    1279
    1280    fn apply(&self, cmd: &mut ProcessBuilder) {
    1281        cmd.arg(&self.file);
    1282        cmd.args(&self.other);
    1283        if self.prepare_for_edition.is_some() {
    1284            // When migrating an edition, we don't want to fix other lints as
    1285            // they can sometimes add suggestions that fail to apply, causing
    1286            // the entire migration to fail. But those lints aren't needed to
    1287            // migrate.
    1288            cmd.arg("--cap-lints=allow");
    1289        } else {
    1290            // This allows `cargo fix` to work even if the crate has #[deny(warnings)].
    1291            cmd.arg("--cap-lints=warn");
    1292        }
    1293        if let Some(edition) = self.enabled_edition {
    1294            cmd.arg("--edition").arg(edition.to_string());
    1295            if self.idioms && edition.supports_idiom_lint() {
    1296                cmd.arg(format!("-Wrust-{}-idioms", edition));
    1297            }
    1298        }
    1299
    1300        if let Some(edition) = self.prepare_for_edition {
    1301            edition.force_warn_arg(cmd);
    1302        }
    1303    }
    1304
    1305    /// Validates the edition, and sends a message indicating what is being
    1306    /// done. Returns a flag indicating whether this fix should be run.
    1307    fn can_run_rustfix(&self, gctx: &GlobalContext) -> CargoResult<bool> {
    1308        let Some(to_edition) = self.prepare_for_edition else {
    1309            return Message::Fixing {
    1310                file: self.file.display().to_string(),
    1311            }
    1312            .post(gctx)
    1313            .and(Ok(true));
    1314        };
    1315        // Unfortunately determining which cargo targets are being built
    1316        // isn't easy, and each target can be a different edition. The
    1317        // cargo-as-rustc fix wrapper doesn't know anything about the
    1318        // workspace, so it can't check for the `cargo-features` unstable
    1319        // opt-in. As a compromise, this just restricts to the nightly
    1320        // toolchain.
    1321        //
    1322        // Unfortunately this results in a pretty poor error message when
    1323        // multiple jobs run in parallel (the error appears multiple
    1324        // times). Hopefully this doesn't happen often in practice.
    1325        if !to_edition.is_stable() && !gctx.nightly_features_allowed {
    1326            let message = format!(
    1327                "`{file}` is on the latest edition, but trying to \
    1328                 migrate to edition {to_edition}.\n\
    1329                 Edition {to_edition} is unstable and not allowed in \
    1330                 this release, consider trying the nightly release channel.",
    1331                file = self.file.display(),
    1332                to_edition = to_edition
    1333            );
    1334            return Message::EditionAlreadyEnabled {
    1335                message,
    1336                edition: to_edition.previous().unwrap(),
    1337            }
    1338            .post(gctx)
    1339            .and(Ok(false)); // Do not run rustfix for this the edition.
    1340        }
    1341        let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
    1342        if from_edition == to_edition {
    1343            let message = format!(
    1344                "`{}` is already on the latest edition ({}), \
    1345                 unable to migrate further",
    1346                self.file.display(),
    1347                to_edition
    1348            );
    1349            Message::EditionAlreadyEnabled {
    1350                message,
    1351                edition: to_edition,
    1352            }
    1353            .post(gctx)
    1354        } else {
    1355            Message::Migrating {
    1356                file: self.file.display().to_string(),
    1357                from_edition,
    1358                to_edition,
    1359            }
    1360            .post(gctx)
    1361        }
    1362        .and(Ok(true))
    1363    }
    1364}
    1365
    1366#[cfg(test)]
    1367mod tests {
    1368    use super::FixArgs;
    1369    use std::ffi::OsString;
    1370    use std::io::Write as _;
    1371    use std::path::PathBuf;
    1372
    1373    #[test]
    1374    fn get_fix_args_from_argfile() {
    1375        let mut temp = tempfile::Builder::new().tempfile().unwrap();
    1376        let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
    1377
    1378        let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
    1379        temp.write_all(content.as_bytes()).unwrap();
    1380
    1381        let argfile = format!("@{}", temp.path().display());
    1382        let args = ["cargo", &argfile];
    1383        let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap();
    1384        assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc"));
    1385        assert_eq!(fix_args.file, main_rs.path());
    1386        assert_eq!(fix_args.other, vec![OsString::from("foobar")]);
    1387    }
    1388
    1389    #[test]
    1390    fn get_fix_args_from_argfile_with_extra_arg() {
    1391        let mut temp = tempfile::Builder::new().tempfile().unwrap();
    1392        let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
    1393
    1394        let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
    1395        temp.write_all(content.as_bytes()).unwrap();
    1396
    1397        let argfile = format!("@{}", temp.path().display());
    1398        let args = ["cargo", &argfile, "boo!"];
    1399        match FixArgs::from_args(args.map(|x| x.into())) {
    1400            Err(e) => assert_eq!(
    1401                e.to_string(),
    1402                "argfile `@path` cannot be combined with other arguments"
    1403            ),
    1404            Ok(_) => panic!("should fail"),
    1405        }
    1406    }
    1407}