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