cargo/ops/fix/
mod.rs

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