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