1use 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
72const FIX_ENV_INTERNAL: &str = "__CARGO_FIX_PLZ";
77const BROKEN_CODE_ENV_INTERNAL: &str = "__CARGO_FIX_BROKEN_CODE";
80const EDITION_ENV_INTERNAL: &str = "__CARGO_FIX_EDITION";
83const IDIOMS_ENV_INTERNAL: &str = "__CARGO_FIX_IDIOMS";
86const 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#[derive(Clone, Copy)]
108pub enum EditionFixMode {
109 NextRelative,
113 OverrideSpecific(Edition),
118}
119
120impl EditionFixMode {
121 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 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 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 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 wrapper.retry_with_argfile(true);
212
213 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 if matches!(ws.root_maybe(), MaybePackage::Virtual(_)) {
302 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 } 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 fixes += rename_dep_fields_2024(workspace, "dependencies");
329 }
330 }
331
332 if 0 < fixes {
333 let file = ws.root_manifest();
336 let file = file.strip_prefix(ws.root()).unwrap_or(file);
337 let file = file.display();
338 ws.gctx().shell().status(
339 "Migrating",
340 format!("{file} from {highest_edition} edition to {prepare_for_edition}"),
341 )?;
342
343 let verb = if fixes == 1 { "fix" } else { "fixes" };
344 let msg = format!("{file} ({fixes} {verb})");
345 ws.gctx().shell().status("Fixed", msg)?;
346
347 manifest_mut.write()?;
348 }
349 }
350 }
351
352 for pkg in pkgs {
353 let existing_edition = pkg.manifest().edition();
354 let prepare_for_edition = edition_mode.next_edition(existing_edition);
355 if existing_edition == prepare_for_edition
356 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
357 {
358 continue;
359 }
360 let file = pkg.manifest_path();
361 let file = file.strip_prefix(ws.root()).unwrap_or(file);
362 let file = file.display();
363 ws.gctx().shell().status(
364 "Migrating",
365 format!("{file} from {existing_edition} edition to {prepare_for_edition}"),
366 )?;
367
368 let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
369 let document = &mut manifest_mut.data;
370 let mut fixes = 0;
371
372 let ws_original_toml = match ws.root_maybe() {
373 MaybePackage::Package(package) => package.manifest().original_toml(),
374 MaybePackage::Virtual(manifest) => manifest.original_toml(),
375 };
376
377 if Edition::Edition2024 <= prepare_for_edition {
378 let root = document.as_table_mut();
379
380 if let Some(workspace) = root
381 .get_mut("workspace")
382 .and_then(|t| t.as_table_like_mut())
383 {
384 fixes += rename_dep_fields_2024(workspace, "dependencies");
387 }
388
389 fixes += rename_table(root, "project", "package");
390 if let Some(target) = root.get_mut("lib").and_then(|t| t.as_table_like_mut()) {
391 fixes += rename_target_fields_2024(target);
392 }
393 fixes += rename_array_of_target_fields_2024(root, "bin");
394 fixes += rename_array_of_target_fields_2024(root, "example");
395 fixes += rename_array_of_target_fields_2024(root, "test");
396 fixes += rename_array_of_target_fields_2024(root, "bench");
397 fixes += rename_dep_fields_2024(root, "dependencies");
398 fixes += remove_ignored_default_features_2024(root, "dependencies", ws_original_toml);
399 fixes += rename_table(root, "dev_dependencies", "dev-dependencies");
400 fixes += rename_dep_fields_2024(root, "dev-dependencies");
401 fixes +=
402 remove_ignored_default_features_2024(root, "dev-dependencies", ws_original_toml);
403 fixes += rename_table(root, "build_dependencies", "build-dependencies");
404 fixes += rename_dep_fields_2024(root, "build-dependencies");
405 fixes +=
406 remove_ignored_default_features_2024(root, "build-dependencies", ws_original_toml);
407 for target in root
408 .get_mut("target")
409 .and_then(|t| t.as_table_like_mut())
410 .iter_mut()
411 .flat_map(|t| t.iter_mut())
412 .filter_map(|(_k, t)| t.as_table_like_mut())
413 {
414 fixes += rename_dep_fields_2024(target, "dependencies");
415 fixes +=
416 remove_ignored_default_features_2024(target, "dependencies", ws_original_toml);
417 fixes += rename_table(target, "dev_dependencies", "dev-dependencies");
418 fixes += rename_dep_fields_2024(target, "dev-dependencies");
419 fixes += remove_ignored_default_features_2024(
420 target,
421 "dev-dependencies",
422 ws_original_toml,
423 );
424 fixes += rename_table(target, "build_dependencies", "build-dependencies");
425 fixes += rename_dep_fields_2024(target, "build-dependencies");
426 fixes += remove_ignored_default_features_2024(
427 target,
428 "build-dependencies",
429 ws_original_toml,
430 );
431 }
432 }
433
434 if 0 < fixes {
435 let verb = if fixes == 1 { "fix" } else { "fixes" };
436 let msg = format!("{file} ({fixes} {verb})");
437 ws.gctx().shell().status("Fixed", msg)?;
438
439 manifest_mut.write()?;
440 }
441 }
442
443 Ok(())
444}
445
446fn rename_dep_fields_2024(parent: &mut dyn toml_edit::TableLike, dep_kind: &str) -> usize {
447 let mut fixes = 0;
448 for target in parent
449 .get_mut(dep_kind)
450 .and_then(|t| t.as_table_like_mut())
451 .iter_mut()
452 .flat_map(|t| t.iter_mut())
453 .filter_map(|(_k, t)| t.as_table_like_mut())
454 {
455 fixes += rename_table(target, "default_features", "default-features");
456 }
457 fixes
458}
459
460fn remove_ignored_default_features_2024(
461 parent: &mut dyn toml_edit::TableLike,
462 dep_kind: &str,
463 ws_original_toml: Option<&TomlManifest>,
464) -> usize {
465 let Some(ws_original_toml) = ws_original_toml else {
466 return 0;
467 };
468
469 let mut fixes = 0;
470 for (name_in_toml, target) in parent
471 .get_mut(dep_kind)
472 .and_then(|t| t.as_table_like_mut())
473 .iter_mut()
474 .flat_map(|t| t.iter_mut())
475 .filter_map(|(k, t)| t.as_table_like_mut().map(|t| (k, t)))
476 {
477 let name_in_toml: &str = &name_in_toml;
478 let ws_deps = ws_original_toml
479 .workspace
480 .as_ref()
481 .and_then(|ws| ws.dependencies.as_ref());
482 if let Some(ws_dep) = ws_deps.and_then(|ws_deps| ws_deps.get(name_in_toml)) {
483 if ws_dep.default_features() == Some(false) {
484 continue;
485 }
486 }
487 if target
488 .get("workspace")
489 .and_then(|i| i.as_value())
490 .and_then(|i| i.as_bool())
491 == Some(true)
492 && target
493 .get("default-features")
494 .and_then(|i| i.as_value())
495 .and_then(|i| i.as_bool())
496 == Some(false)
497 {
498 target.remove("default-features");
499 fixes += 1;
500 }
501 }
502 fixes
503}
504
505fn rename_array_of_target_fields_2024(root: &mut dyn toml_edit::TableLike, kind: &str) -> usize {
506 let mut fixes = 0;
507 for target in root
508 .get_mut(kind)
509 .and_then(|t| t.as_array_of_tables_mut())
510 .iter_mut()
511 .flat_map(|t| t.iter_mut())
512 {
513 fixes += rename_target_fields_2024(target);
514 }
515 fixes
516}
517
518fn rename_target_fields_2024(target: &mut dyn toml_edit::TableLike) -> usize {
519 let mut fixes = 0;
520 fixes += rename_table(target, "crate_type", "crate-type");
521 fixes += rename_table(target, "proc_macro", "proc-macro");
522 fixes
523}
524
525fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> usize {
526 let Some(old_key) = parent.key(old).cloned() else {
527 return 0;
528 };
529
530 let project = parent.remove(old).expect("returned early");
531 if !parent.contains_key(new) {
532 parent.insert(new, project);
533 let mut new_key = parent.key_mut(new).expect("just inserted");
534 *new_key.dotted_decor_mut() = old_key.dotted_decor().clone();
535 *new_key.leaf_decor_mut() = old_key.leaf_decor().clone();
536 }
537 1
538}
539
540fn check_resolver_change<'gctx>(
541 ws: &Workspace<'gctx>,
542 target_data: &mut RustcTargetData<'gctx>,
543 opts: &FixOptions,
544) -> CargoResult<()> {
545 let root = ws.root_maybe();
546 match root {
547 MaybePackage::Package(root_pkg) => {
548 if root_pkg.manifest().resolve_behavior().is_some() {
549 return Ok(());
551 }
552 let pkgs = opts.compile_opts.spec.get_packages(ws)?;
554 if !pkgs.contains(&root_pkg) {
555 return Ok(());
557 }
558 if root_pkg.manifest().edition() != Edition::Edition2018 {
559 return Ok(());
561 }
562 }
563 MaybePackage::Virtual(_vm) => {
564 return Ok(());
566 }
567 }
568 assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
570 let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
571 let mut resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> {
572 let dry_run = false;
573 let ws_resolve = ops::resolve_ws_with_opts(
574 ws,
575 target_data,
576 &opts.compile_opts.build_config.requested_kinds,
577 &opts.compile_opts.cli_features,
578 &specs,
579 has_dev_units,
580 crate::core::resolver::features::ForceAllTargets::No,
581 dry_run,
582 )?;
583
584 let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units);
585 let v2_features = FeatureResolver::resolve(
586 ws,
587 target_data,
588 &ws_resolve.targeted_resolve,
589 &ws_resolve.pkg_set,
590 &opts.compile_opts.cli_features,
591 &specs,
592 &opts.compile_opts.build_config.requested_kinds,
593 feature_opts,
594 )?;
595
596 if ws_resolve.specs_and_features.len() != 1 {
597 bail!(r#"cannot fix edition when using `feature-unification = "package"`."#);
598 }
599 let resolved_features = &ws_resolve
600 .specs_and_features
601 .first()
602 .expect("We've already checked that there is exactly one.")
603 .resolved_features;
604 let diffs = v2_features.compare_legacy(resolved_features);
605 Ok((ws_resolve, diffs))
606 };
607 let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?;
608 let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?;
609 if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() {
610 return Ok(());
612 }
613 with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals));
615 let gctx = ws.gctx();
616 gctx.shell().note(
617 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
618 )?;
619 drop_eprintln!(
620 gctx,
621 "This may cause some dependencies to be built with fewer features enabled than previously."
622 );
623 drop_eprintln!(
624 gctx,
625 "More information about the resolver changes may be found \
626 at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
627 );
628 drop_eprintln!(
629 gctx,
630 "When building the following dependencies, \
631 the given features will no longer be used:\n"
632 );
633 let show_diffs = |differences: DiffMap| {
634 for ((pkg_id, features_for), removed) in differences {
635 drop_eprint!(gctx, " {}", pkg_id);
636 if let FeaturesFor::HostDep = features_for {
637 drop_eprint!(gctx, " (as host dependency)");
638 }
639 drop_eprint!(gctx, " removed features: ");
640 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
641 drop_eprintln!(gctx, "{}", joined.join(", "));
642 }
643 drop_eprint!(gctx, "\n");
644 };
645 if !without_dev_diffs.is_empty() {
646 show_diffs(without_dev_diffs);
647 }
648 if !with_dev_diffs.is_empty() {
649 drop_eprintln!(
650 gctx,
651 "The following differences only apply when building with dev-dependencies:\n"
652 );
653 show_diffs(with_dev_diffs);
654 }
655 report_maybe_diesel(gctx, &ws_resolve.targeted_resolve)?;
656 Ok(())
657}
658
659fn report_maybe_diesel(gctx: &GlobalContext, resolve: &Resolve) -> CargoResult<()> {
660 fn is_broken_diesel(pid: PackageId) -> bool {
661 pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8)
662 }
663
664 fn is_broken_diesel_migration(pid: PackageId) -> bool {
665 pid.name() == "diesel_migrations" && pid.version().major <= 1
666 }
667
668 if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) {
669 gctx.shell().note(
670 "\
671This project appears to use both diesel and diesel_migrations. These packages have
672a known issue where the build may fail due to the version 2 resolver preventing
673feature unification between those two packages. Please update to at least diesel 1.4.8
674to prevent this issue from happening.
675",
676 )?;
677 }
678 Ok(())
679}
680
681pub fn fix_get_proxy_lock_addr() -> Option<String> {
686 #[expect(
687 clippy::disallowed_methods,
688 reason = "internal only, no reason for config support"
689 )]
690 env::var(FIX_ENV_INTERNAL).ok()
691}
692
693pub fn fix_exec_rustc(gctx: &GlobalContext, lock_addr: &str) -> CargoResult<()> {
702 let args = FixArgs::get()?;
703 trace!("cargo-fix as rustc got file {:?}", args.file);
704
705 let workspace_rustc = gctx
706 .get_env("RUSTC_WORKSPACE_WRAPPER")
707 .map(PathBuf::from)
708 .ok();
709 let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
710 rustc.retry_with_argfile(true);
711 rustc.env_remove(FIX_ENV_INTERNAL);
712 args.apply(&mut rustc);
713 if let Some(client) = gctx.jobserver_from_env() {
716 rustc.inherit_jobserver(client);
717 }
718
719 trace!("start rustfixing {:?}", args.file);
720 let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, gctx)?;
721
722 if fixes.last_output.status.success() {
723 for (path, file) in fixes.files.iter() {
724 Message::Fixed {
725 file: path.clone(),
726 fixes: file.fixes_applied,
727 }
728 .post(gctx)?;
729 }
730 emit_output(&fixes.last_output)?;
732 return Ok(());
733 }
734
735 let allow_broken_code = gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some();
736
737 if !allow_broken_code {
741 for (path, file) in fixes.files.iter() {
742 debug!("reverting {:?} due to errors", path);
743 paths::write(path, &file.original_code)?;
744 }
745 }
746
747 if fixes.files.is_empty() {
753 emit_output(&fixes.last_output)?;
755 exit_with(fixes.last_output.status);
756 } else {
757 let krate = {
758 let mut iter = rustc.get_args();
759 let mut krate = None;
760 while let Some(arg) = iter.next() {
761 if arg == "--crate-name" {
762 krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
763 }
764 }
765 krate
766 };
767 log_failed_fix(
768 gctx,
769 krate,
770 &fixes.last_output.stderr,
771 fixes.last_output.status,
772 )?;
773 emit_output(&fixes.first_output)?;
777 exit_with(fixes.first_output.status);
781 }
782}
783
784fn emit_output(output: &Output) -> CargoResult<()> {
785 std::io::stderr().write_all(&output.stderr)?;
789 std::io::stdout().write_all(&output.stdout)?;
790 Ok(())
791}
792
793struct FixedCrate {
794 files: HashMap<String, FixedFile>,
796 first_output: Output,
802 last_output: Output,
807}
808
809#[derive(Debug)]
810struct FixedFile {
811 errors_applying_fixes: Vec<String>,
812 fixes_applied: u32,
813 original_code: String,
814}
815
816fn rustfix_crate(
821 lock_addr: &str,
822 rustc: &ProcessBuilder,
823 filename: &Path,
824 args: &FixArgs,
825 gctx: &GlobalContext,
826) -> CargoResult<FixedCrate> {
827 let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
837
838 let mut files = HashMap::new();
840
841 if !args.can_run_rustfix(gctx)? {
842 debug!("can't fix {filename:?}, running rustc: {rustc}");
846 let last_output = rustc.output()?;
847 let fixes = FixedCrate {
848 files,
849 first_output: last_output.clone(),
850 last_output,
851 };
852 return Ok(fixes);
853 }
854
855 let max_iterations = gctx
889 .get_env("CARGO_FIX_MAX_RETRIES")
890 .ok()
891 .and_then(|n| n.parse().ok())
892 .unwrap_or(4);
893 let mut last_output;
894 let mut last_made_changes;
895 let mut first_output = None;
896 let mut current_iteration = 0;
897 loop {
898 for file in files.values_mut() {
899 file.errors_applying_fixes.clear();
901 }
902 (last_output, last_made_changes) =
903 rustfix_and_fix(&mut files, rustc, filename, args, gctx)?;
904 if current_iteration == 0 {
905 first_output = Some(last_output.clone());
906 }
907 let mut progress_yet_to_be_made = false;
908 for (path, file) in files.iter_mut() {
909 if file.errors_applying_fixes.is_empty() {
910 continue;
911 }
912 debug!("had rustfix apply errors in {path:?} {file:?}");
913 if last_made_changes {
917 progress_yet_to_be_made = true;
918 }
919 }
920 if !progress_yet_to_be_made {
921 break;
922 }
923 current_iteration += 1;
924 if current_iteration >= max_iterations {
925 break;
926 }
927 }
928 if last_made_changes {
929 debug!("calling rustc one last time for final results: {rustc}");
930 last_output = rustc.output()?;
931 }
932
933 for (path, file) in files.iter_mut() {
936 for error in file.errors_applying_fixes.drain(..) {
937 Message::ReplaceFailed {
938 file: path.clone(),
939 message: error,
940 }
941 .post(gctx)?;
942 }
943 }
944
945 Ok(FixedCrate {
946 files,
947 first_output: first_output.expect("at least one iteration"),
948 last_output,
949 })
950}
951
952fn rustfix_and_fix(
957 files: &mut HashMap<String, FixedFile>,
958 rustc: &ProcessBuilder,
959 filename: &Path,
960 args: &FixArgs,
961 gctx: &GlobalContext,
962) -> CargoResult<(Output, bool)> {
963 let only = HashSet::new();
966
967 debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
968 let output = rustc.output()?;
969
970 if !output.status.success() && gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() {
976 debug!(
977 "rustfixing `{:?}` failed, rustc exited with {:?}",
978 filename,
979 output.status.code()
980 );
981 return Ok((output, false));
982 }
983
984 let fix_mode = gctx
985 .get_env_os("__CARGO_FIX_YOLO")
986 .map(|_| rustfix::Filter::Everything)
987 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
988
989 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
992
993 let suggestions = stderr
994 .lines()
995 .filter(|x| !x.is_empty())
996 .inspect(|y| trace!("line: {}", y))
997 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
999 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
1001
1002 let mut file_map = HashMap::new();
1004 let mut num_suggestion = 0;
1005 let home_path = gctx.home().as_path_unlocked();
1007 for suggestion in suggestions {
1008 trace!("suggestion");
1009 let file_names = suggestion
1013 .solutions
1014 .iter()
1015 .flat_map(|s| s.replacements.iter())
1016 .map(|r| &r.snippet.file_name);
1017
1018 let file_name = if let Some(file_name) = file_names.clone().next() {
1019 file_name.clone()
1020 } else {
1021 trace!("rejecting as it has no solutions {:?}", suggestion);
1022 continue;
1023 };
1024
1025 let file_path = Path::new(&file_name);
1026 if file_path.starts_with(home_path) {
1028 continue;
1029 }
1030 if let Some(sysroot) = args.sysroot.as_deref() {
1032 if file_path.starts_with(sysroot) {
1033 continue;
1034 }
1035 }
1036
1037 if !file_names.clone().all(|f| f == &file_name) {
1038 trace!("rejecting as it changes multiple files: {:?}", suggestion);
1039 continue;
1040 }
1041
1042 trace!("adding suggestion for {:?}: {:?}", file_name, suggestion);
1043 file_map
1044 .entry(file_name)
1045 .or_insert_with(Vec::new)
1046 .push(suggestion);
1047 num_suggestion += 1;
1048 }
1049
1050 debug!(
1051 "collected {} suggestions for `{}`",
1052 num_suggestion,
1053 filename.display(),
1054 );
1055
1056 let mut made_changes = false;
1057 for (file, suggestions) in file_map {
1058 let code = match paths::read(file.as_ref()) {
1062 Ok(s) => s,
1063 Err(e) => {
1064 warn!("failed to read `{}`: {}", file, e);
1065 continue;
1066 }
1067 };
1068 let num_suggestions = suggestions.len();
1069 debug!("applying {} fixes to {}", num_suggestions, file);
1070
1071 let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile {
1076 errors_applying_fixes: Vec::new(),
1077 fixes_applied: 0,
1078 original_code: code.clone(),
1079 });
1080 let mut fixed = CodeFix::new(&code);
1081
1082 for suggestion in suggestions.iter().rev() {
1083 match fixed.apply(suggestion) {
1091 Ok(()) => fixed_file.fixes_applied += 1,
1092 Err(rustfix::Error::AlreadyReplaced {
1093 is_identical: true, ..
1094 }) => continue,
1095 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
1096 }
1097 }
1098 if fixed.modified() {
1099 made_changes = true;
1100 let new_code = fixed.finish()?;
1101 paths::write(&file, new_code)?;
1102 }
1103 }
1104
1105 Ok((output, made_changes))
1106}
1107
1108fn exit_with(status: ExitStatus) -> ! {
1109 #[cfg(unix)]
1110 {
1111 use std::os::unix::prelude::*;
1112 if let Some(signal) = status.signal() {
1113 drop(writeln!(
1114 std::io::stderr().lock(),
1115 "child failed with signal `{}`",
1116 signal
1117 ));
1118 process::exit(2);
1119 }
1120 }
1121 process::exit(status.code().unwrap_or(3));
1122}
1123
1124fn log_failed_fix(
1125 gctx: &GlobalContext,
1126 krate: Option<String>,
1127 stderr: &[u8],
1128 status: ExitStatus,
1129) -> CargoResult<()> {
1130 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
1131
1132 let diagnostics = stderr
1133 .lines()
1134 .filter(|x| !x.is_empty())
1135 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
1136 let mut files = BTreeSet::new();
1137 let mut errors = Vec::new();
1138 for diagnostic in diagnostics {
1139 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
1140 for span in diagnostic.spans.into_iter() {
1141 files.insert(span.file_name);
1142 }
1143 }
1144 errors.extend(
1146 stderr
1147 .lines()
1148 .filter(|x| !x.starts_with('{'))
1149 .map(|x| x.to_string()),
1150 );
1151
1152 let files = files.into_iter().collect();
1153 let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) {
1154 None
1155 } else {
1156 Some(exit_status_to_string(status))
1157 };
1158 Message::FixFailed {
1159 files,
1160 krate,
1161 errors,
1162 abnormal_exit,
1163 }
1164 .post(gctx)?;
1165
1166 Ok(())
1167}
1168
1169struct FixArgs {
1172 file: PathBuf,
1174 prepare_for_edition: Option<Edition>,
1177 idioms: bool,
1179 enabled_edition: Option<Edition>,
1183 other: Vec<OsString>,
1186 rustc: PathBuf,
1188 sysroot: Option<PathBuf>,
1190}
1191
1192impl FixArgs {
1193 fn get() -> CargoResult<FixArgs> {
1194 Self::from_args(env::args_os())
1195 }
1196
1197 fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> {
1199 let mut argv = argv.into_iter();
1200 let mut rustc = argv
1201 .nth(1)
1202 .map(PathBuf::from)
1203 .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?;
1204 let mut file = None;
1205 let mut enabled_edition = None;
1206 let mut other = Vec::new();
1207
1208 let mut handle_arg = |arg: OsString| -> CargoResult<()> {
1209 let path = PathBuf::from(arg);
1210 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
1211 file = Some(path);
1212 return Ok(());
1213 }
1214 if let Some(s) = path.to_str() {
1215 if let Some(edition) = s.strip_prefix("--edition=") {
1216 enabled_edition = Some(edition.parse()?);
1217 return Ok(());
1218 }
1219 }
1220 other.push(path.into());
1221 Ok(())
1222 };
1223
1224 if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") {
1225 if argv.next().is_some() {
1228 bail!("argfile `@path` cannot be combined with other arguments");
1229 }
1230 let contents = fs::read_to_string(argfile_path)
1231 .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?;
1232 let mut iter = contents.lines().map(OsString::from);
1233 rustc = iter
1234 .next()
1235 .map(PathBuf::from)
1236 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
1237 for arg in iter {
1238 handle_arg(arg)?;
1239 }
1240 } else {
1241 for arg in argv {
1242 handle_arg(arg)?;
1243 }
1244 }
1245
1246 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
1247 #[expect(
1248 clippy::disallowed_methods,
1249 reason = "internal only, no reason for config support"
1250 )]
1251 let idioms = env::var(IDIOMS_ENV_INTERNAL).is_ok();
1252
1253 #[expect(
1254 clippy::disallowed_methods,
1255 reason = "internal only, no reason for config support"
1256 )]
1257 let prepare_for_edition = env::var(EDITION_ENV_INTERNAL).ok().map(|v| {
1258 let enabled_edition = enabled_edition.unwrap_or(Edition::Edition2015);
1259 let mode = EditionFixMode::from_str(&v);
1260 mode.next_edition(enabled_edition)
1261 });
1262
1263 #[expect(
1264 clippy::disallowed_methods,
1265 reason = "internal only, no reason for config support"
1266 )]
1267 let sysroot = env::var_os(SYSROOT_INTERNAL).map(PathBuf::from);
1268
1269 Ok(FixArgs {
1270 file,
1271 prepare_for_edition,
1272 idioms,
1273 enabled_edition,
1274 other,
1275 rustc,
1276 sysroot,
1277 })
1278 }
1279
1280 fn apply(&self, cmd: &mut ProcessBuilder) {
1281 cmd.arg(&self.file);
1282 cmd.args(&self.other);
1283 if self.prepare_for_edition.is_some() {
1284 cmd.arg("--cap-lints=allow");
1289 } else {
1290 cmd.arg("--cap-lints=warn");
1292 }
1293 if let Some(edition) = self.enabled_edition {
1294 cmd.arg("--edition").arg(edition.to_string());
1295 if self.idioms && edition.supports_idiom_lint() {
1296 cmd.arg(format!("-Wrust-{}-idioms", edition));
1297 }
1298 }
1299
1300 if let Some(edition) = self.prepare_for_edition {
1301 edition.force_warn_arg(cmd);
1302 }
1303 }
1304
1305 fn can_run_rustfix(&self, gctx: &GlobalContext) -> CargoResult<bool> {
1308 let Some(to_edition) = self.prepare_for_edition else {
1309 return Message::Fixing {
1310 file: self.file.display().to_string(),
1311 }
1312 .post(gctx)
1313 .and(Ok(true));
1314 };
1315 if !to_edition.is_stable() && !gctx.nightly_features_allowed {
1326 let message = format!(
1327 "`{file}` is on the latest edition, but trying to \
1328 migrate to edition {to_edition}.\n\
1329 Edition {to_edition} is unstable and not allowed in \
1330 this release, consider trying the nightly release channel.",
1331 file = self.file.display(),
1332 to_edition = to_edition
1333 );
1334 return Message::EditionAlreadyEnabled {
1335 message,
1336 edition: to_edition.previous().unwrap(),
1337 }
1338 .post(gctx)
1339 .and(Ok(false)); }
1341 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
1342 if from_edition == to_edition {
1343 let message = format!(
1344 "`{}` is already on the latest edition ({}), \
1345 unable to migrate further",
1346 self.file.display(),
1347 to_edition
1348 );
1349 Message::EditionAlreadyEnabled {
1350 message,
1351 edition: to_edition,
1352 }
1353 .post(gctx)
1354 } else {
1355 Message::Migrating {
1356 file: self.file.display().to_string(),
1357 from_edition,
1358 to_edition,
1359 }
1360 .post(gctx)
1361 }
1362 .and(Ok(true))
1363 }
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368 use super::FixArgs;
1369 use std::ffi::OsString;
1370 use std::io::Write as _;
1371 use std::path::PathBuf;
1372
1373 #[test]
1374 fn get_fix_args_from_argfile() {
1375 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1376 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1377
1378 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1379 temp.write_all(content.as_bytes()).unwrap();
1380
1381 let argfile = format!("@{}", temp.path().display());
1382 let args = ["cargo", &argfile];
1383 let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap();
1384 assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc"));
1385 assert_eq!(fix_args.file, main_rs.path());
1386 assert_eq!(fix_args.other, vec![OsString::from("foobar")]);
1387 }
1388
1389 #[test]
1390 fn get_fix_args_from_argfile_with_extra_arg() {
1391 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1392 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1393
1394 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1395 temp.write_all(content.as_bytes()).unwrap();
1396
1397 let argfile = format!("@{}", temp.path().display());
1398 let args = ["cargo", &argfile, "boo!"];
1399 match FixArgs::from_args(args.map(|x| x.into())) {
1400 Err(e) => assert_eq!(
1401 e.to_string(),
1402 "argfile `@path` cannot be combined with other arguments"
1403 ),
1404 Ok(_) => panic!("should fail"),
1405 }
1406 }
1407}