Skip to main content

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