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}
104
105#[derive(Clone, Copy)]
107pub enum EditionFixMode {
108 NextRelative,
112 OverrideSpecific(Edition),
117}
118
119impl EditionFixMode {
120 pub fn next_edition(&self, current_edition: Edition) -> Edition {
122 match self {
123 EditionFixMode::NextRelative => current_edition.saturating_next(),
124 EditionFixMode::OverrideSpecific(edition) => *edition,
125 }
126 }
127
128 fn to_string(&self) -> String {
130 match self {
131 EditionFixMode::NextRelative => "1".to_string(),
132 EditionFixMode::OverrideSpecific(edition) => edition.to_string(),
133 }
134 }
135
136 fn from_str(s: &str) -> EditionFixMode {
138 match s {
139 "1" => EditionFixMode::NextRelative,
140 edition => EditionFixMode::OverrideSpecific(edition.parse().unwrap()),
141 }
142 }
143}
144
145pub fn fix(
146 gctx: &GlobalContext,
147 original_ws: &Workspace<'_>,
148 opts: &mut FixOptions,
149) -> CargoResult<()> {
150 check_version_control(gctx, opts)?;
151
152 let mut target_data =
153 RustcTargetData::new(original_ws, &opts.compile_opts.build_config.requested_kinds)?;
154
155 let specs = opts.compile_opts.spec.to_package_id_specs(&original_ws)?;
156 let members: Vec<&Package> = original_ws
157 .members()
158 .filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
159 .collect();
160 if let Some(edition_mode) = opts.edition {
161 migrate_manifests(original_ws, &members, edition_mode)?;
162
163 check_resolver_change(&original_ws, &mut target_data, opts)?;
164 }
165 fix_manifests(original_ws, &members)?;
166 let ws = original_ws.reload(gctx)?;
167
168 let lock_server = LockServer::new()?;
170 let mut wrapper = ProcessBuilder::new(env::current_exe()?);
171 wrapper.env(FIX_ENV_INTERNAL, lock_server.addr().to_string());
172 let _started = lock_server.start()?;
173
174 opts.compile_opts.build_config.force_rebuild = true;
175
176 if opts.broken_code {
177 wrapper.env(BROKEN_CODE_ENV_INTERNAL, "1");
178 }
179
180 if let Some(mode) = &opts.edition {
181 wrapper.env(EDITION_ENV_INTERNAL, mode.to_string());
182 }
183 if opts.idioms {
184 wrapper.env(IDIOMS_ENV_INTERNAL, "1");
185 }
186
187 let sysroot = &target_data.info(CompileKind::Host).sysroot;
188 if sysroot.is_dir() {
189 wrapper.env(SYSROOT_INTERNAL, sysroot);
190 }
191
192 *opts
193 .compile_opts
194 .build_config
195 .rustfix_diagnostic_server
196 .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
197
198 if let Some(server) = opts
199 .compile_opts
200 .build_config
201 .rustfix_diagnostic_server
202 .borrow()
203 .as_ref()
204 {
205 server.configure(&mut wrapper);
206 }
207
208 let rustc = ws.gctx().load_global_rustc(Some(&ws))?;
209 wrapper.arg(&rustc.path);
210 wrapper.retry_with_argfile(true);
213
214 opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
217
218 ops::compile(&ws, &opts.compile_opts)?;
219 Ok(())
220}
221
222fn check_version_control(gctx: &GlobalContext, opts: &FixOptions) -> CargoResult<()> {
223 if opts.allow_no_vcs {
224 return Ok(());
225 }
226 if !existing_vcs_repo(gctx.cwd(), gctx.cwd()) {
227 bail!(
228 "no VCS found for this package and `cargo fix` can potentially \
229 perform destructive changes; if you'd like to suppress this \
230 error pass `--allow-no-vcs`"
231 )
232 }
233
234 if opts.allow_dirty && opts.allow_staged {
235 return Ok(());
236 }
237
238 let mut dirty_files = Vec::new();
239 let mut staged_files = Vec::new();
240 if let Ok(repo) = git2::Repository::discover(gctx.cwd()) {
241 let mut repo_opts = git2::StatusOptions::new();
242 repo_opts.include_ignored(false);
243 repo_opts.include_untracked(true);
244 for status in repo.statuses(Some(&mut repo_opts))?.iter() {
245 if let Ok(path) = status.path() {
246 match status.status() {
247 git2::Status::CURRENT => (),
248 git2::Status::INDEX_NEW
249 | git2::Status::INDEX_MODIFIED
250 | git2::Status::INDEX_DELETED
251 | git2::Status::INDEX_RENAMED
252 | git2::Status::INDEX_TYPECHANGE => {
253 if !opts.allow_staged {
254 staged_files.push(path.to_string())
255 }
256 }
257 _ => {
258 if !opts.allow_dirty {
259 dirty_files.push(path.to_string())
260 }
261 }
262 };
263 }
264 }
265 }
266
267 if dirty_files.is_empty() && staged_files.is_empty() {
268 return Ok(());
269 }
270
271 let mut files_list = String::new();
272 for file in dirty_files {
273 files_list.push_str(" * ");
274 files_list.push_str(&file);
275 files_list.push_str(" (dirty)\n");
276 }
277 for file in staged_files {
278 files_list.push_str(" * ");
279 files_list.push_str(&file);
280 files_list.push_str(" (staged)\n");
281 }
282
283 bail!(
284 "the working directory of this package has uncommitted changes, and \
285 `cargo fix` can potentially perform destructive changes; if you'd \
286 like to suppress this error pass `--allow-dirty`, \
287 or commit the changes to these files:\n\
288 \n\
289 {}\n\
290 ",
291 files_list
292 );
293}
294
295fn fix_manifests(ws: &Workspace<'_>, pkgs: &[&Package]) -> CargoResult<()> {
296 for pkg in pkgs {
297 let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
298 let mut fixes = 0;
299
300 if manifest_mut.ensure_edition() {
301 fixes += 1;
302 }
303
304 if 0 < fixes {
305 let file = pkg.manifest_path();
306 let file = file.strip_prefix(ws.root()).unwrap_or(file);
307 let file = file.display();
308 let verb = if fixes == 1 { "fix" } else { "fixes" };
309 let msg = format!("{file} ({fixes} {verb})");
310 ws.gctx().shell().status("Fixed", msg)?;
311
312 manifest_mut.write()?;
313 }
314 }
315
316 Ok(())
317}
318
319fn migrate_manifests(
320 ws: &Workspace<'_>,
321 pkgs: &[&Package],
322 edition_mode: EditionFixMode,
323) -> CargoResult<()> {
324 if matches!(ws.root_maybe(), MaybePackage::Virtual(_)) {
327 let highest_edition = pkgs
330 .iter()
331 .map(|p| p.manifest().edition())
332 .max()
333 .unwrap_or_default();
334 let prepare_for_edition = edition_mode.next_edition(highest_edition);
335 if highest_edition == prepare_for_edition
336 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
337 {
338 } else {
340 let mut manifest_mut = LocalManifest::try_new(ws.root_manifest())?;
341 let document = &mut manifest_mut.data;
342 let mut fixes = 0;
343
344 if Edition::Edition2024 <= prepare_for_edition {
345 let root = document.as_table_mut();
346
347 if let Some(workspace) = root
348 .get_mut("workspace")
349 .and_then(|t| t.as_table_like_mut())
350 {
351 fixes += rename_dep_fields_2024(workspace, "dependencies");
354 }
355 }
356
357 if 0 < fixes {
358 let file = ws.root_manifest();
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 {highest_edition} edition to {prepare_for_edition}"),
366 )?;
367
368 let verb = if fixes == 1 { "fix" } else { "fixes" };
369 let msg = format!("{file} ({fixes} {verb})");
370 ws.gctx().shell().status("Fixed", msg)?;
371
372 manifest_mut.write()?;
373 }
374 }
375 }
376
377 for pkg in pkgs {
378 let existing_edition = pkg.manifest().edition();
379 let prepare_for_edition = edition_mode.next_edition(existing_edition);
380 if existing_edition == prepare_for_edition
381 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
382 {
383 continue;
384 }
385 let file = pkg.manifest_path();
386 let file = file.strip_prefix(ws.root()).unwrap_or(file);
387 let file = file.display();
388 ws.gctx().shell().status(
389 "Migrating",
390 format!("{file} from {existing_edition} edition to {prepare_for_edition}"),
391 )?;
392
393 let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
394 let document = &mut manifest_mut.data;
395 let mut fixes = 0;
396
397 let ws_original_toml = match ws.root_maybe() {
398 MaybePackage::Package(package) => package.manifest().original_toml(),
399 MaybePackage::Virtual(manifest) => manifest.original_toml(),
400 };
401
402 if Edition::Edition2024 <= prepare_for_edition {
403 let root = document.as_table_mut();
404
405 if let Some(workspace) = root
406 .get_mut("workspace")
407 .and_then(|t| t.as_table_like_mut())
408 {
409 fixes += rename_dep_fields_2024(workspace, "dependencies");
412 }
413
414 fixes += rename_table(root, "project", "package");
415 if let Some(target) = root.get_mut("lib").and_then(|t| t.as_table_like_mut()) {
416 fixes += rename_target_fields_2024(target);
417 }
418 fixes += rename_array_of_target_fields_2024(root, "bin");
419 fixes += rename_array_of_target_fields_2024(root, "example");
420 fixes += rename_array_of_target_fields_2024(root, "test");
421 fixes += rename_array_of_target_fields_2024(root, "bench");
422 fixes += rename_dep_fields_2024(root, "dependencies");
423 fixes += remove_ignored_default_features_2024(root, "dependencies", ws_original_toml);
424 fixes += rename_table(root, "dev_dependencies", "dev-dependencies");
425 fixes += rename_dep_fields_2024(root, "dev-dependencies");
426 fixes +=
427 remove_ignored_default_features_2024(root, "dev-dependencies", ws_original_toml);
428 fixes += rename_table(root, "build_dependencies", "build-dependencies");
429 fixes += rename_dep_fields_2024(root, "build-dependencies");
430 fixes +=
431 remove_ignored_default_features_2024(root, "build-dependencies", ws_original_toml);
432 for target in root
433 .get_mut("target")
434 .and_then(|t| t.as_table_like_mut())
435 .iter_mut()
436 .flat_map(|t| t.iter_mut())
437 .filter_map(|(_k, t)| t.as_table_like_mut())
438 {
439 fixes += rename_dep_fields_2024(target, "dependencies");
440 fixes +=
441 remove_ignored_default_features_2024(target, "dependencies", ws_original_toml);
442 fixes += rename_table(target, "dev_dependencies", "dev-dependencies");
443 fixes += rename_dep_fields_2024(target, "dev-dependencies");
444 fixes += remove_ignored_default_features_2024(
445 target,
446 "dev-dependencies",
447 ws_original_toml,
448 );
449 fixes += rename_table(target, "build_dependencies", "build-dependencies");
450 fixes += rename_dep_fields_2024(target, "build-dependencies");
451 fixes += remove_ignored_default_features_2024(
452 target,
453 "build-dependencies",
454 ws_original_toml,
455 );
456 }
457 }
458
459 if 0 < fixes {
460 let verb = if fixes == 1 { "fix" } else { "fixes" };
461 let msg = format!("{file} ({fixes} {verb})");
462 ws.gctx().shell().status("Fixed", msg)?;
463
464 manifest_mut.write()?;
465 }
466 }
467
468 Ok(())
469}
470
471fn rename_dep_fields_2024(parent: &mut dyn toml_edit::TableLike, dep_kind: &str) -> usize {
472 let mut fixes = 0;
473 for target in parent
474 .get_mut(dep_kind)
475 .and_then(|t| t.as_table_like_mut())
476 .iter_mut()
477 .flat_map(|t| t.iter_mut())
478 .filter_map(|(_k, t)| t.as_table_like_mut())
479 {
480 fixes += rename_table(target, "default_features", "default-features");
481 }
482 fixes
483}
484
485fn remove_ignored_default_features_2024(
486 parent: &mut dyn toml_edit::TableLike,
487 dep_kind: &str,
488 ws_original_toml: Option<&TomlManifest>,
489) -> usize {
490 let Some(ws_original_toml) = ws_original_toml else {
491 return 0;
492 };
493
494 let mut fixes = 0;
495 for (name_in_toml, target) in parent
496 .get_mut(dep_kind)
497 .and_then(|t| t.as_table_like_mut())
498 .iter_mut()
499 .flat_map(|t| t.iter_mut())
500 .filter_map(|(k, t)| t.as_table_like_mut().map(|t| (k, t)))
501 {
502 let name_in_toml: &str = &name_in_toml;
503 let ws_deps = ws_original_toml
504 .workspace
505 .as_ref()
506 .and_then(|ws| ws.dependencies.as_ref());
507 if let Some(ws_dep) = ws_deps.and_then(|ws_deps| ws_deps.get(name_in_toml)) {
508 if ws_dep.default_features() == Some(false) {
509 continue;
510 }
511 }
512 if target
513 .get("workspace")
514 .and_then(|i| i.as_value())
515 .and_then(|i| i.as_bool())
516 == Some(true)
517 && target
518 .get("default-features")
519 .and_then(|i| i.as_value())
520 .and_then(|i| i.as_bool())
521 == Some(false)
522 {
523 target.remove("default-features");
524 fixes += 1;
525 }
526 }
527 fixes
528}
529
530fn rename_array_of_target_fields_2024(root: &mut dyn toml_edit::TableLike, kind: &str) -> usize {
531 let mut fixes = 0;
532 for target in root
533 .get_mut(kind)
534 .and_then(|t| t.as_array_of_tables_mut())
535 .iter_mut()
536 .flat_map(|t| t.iter_mut())
537 {
538 fixes += rename_target_fields_2024(target);
539 }
540 fixes
541}
542
543fn rename_target_fields_2024(target: &mut dyn toml_edit::TableLike) -> usize {
544 let mut fixes = 0;
545 fixes += rename_table(target, "crate_type", "crate-type");
546 fixes += rename_table(target, "proc_macro", "proc-macro");
547 fixes
548}
549
550fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> usize {
551 let Some(old_key) = parent.key(old).cloned() else {
552 return 0;
553 };
554
555 let project = parent.remove(old).expect("returned early");
556 if !parent.contains_key(new) {
557 parent.insert(new, project);
558 let mut new_key = parent.key_mut(new).expect("just inserted");
559 *new_key.dotted_decor_mut() = old_key.dotted_decor().clone();
560 *new_key.leaf_decor_mut() = old_key.leaf_decor().clone();
561 }
562 1
563}
564
565fn check_resolver_change<'gctx>(
566 ws: &Workspace<'gctx>,
567 target_data: &mut RustcTargetData<'gctx>,
568 opts: &FixOptions,
569) -> CargoResult<()> {
570 let root = ws.root_maybe();
571 match root {
572 MaybePackage::Package(root_pkg) => {
573 if root_pkg.manifest().resolve_behavior().is_some() {
574 return Ok(());
576 }
577 let pkgs = opts.compile_opts.spec.get_packages(ws)?;
579 if !pkgs.contains(&root_pkg) {
580 return Ok(());
582 }
583 if root_pkg.manifest().edition() != Edition::Edition2018 {
584 return Ok(());
586 }
587 }
588 MaybePackage::Virtual(_vm) => {
589 return Ok(());
591 }
592 }
593 assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
595 let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
596 let mut resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> {
597 let dry_run = false;
598 let ws_resolve = ops::resolve_ws_with_opts(
599 ws,
600 target_data,
601 &opts.compile_opts.build_config.requested_kinds,
602 &opts.compile_opts.cli_features,
603 &specs,
604 has_dev_units,
605 crate::core::resolver::features::ForceAllTargets::No,
606 dry_run,
607 )?;
608
609 let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units);
610 let v2_features = FeatureResolver::resolve(
611 ws,
612 target_data,
613 &ws_resolve.targeted_resolve,
614 &ws_resolve.pkg_set,
615 &opts.compile_opts.cli_features,
616 &specs,
617 &opts.compile_opts.build_config.requested_kinds,
618 feature_opts,
619 )?;
620
621 if ws_resolve.specs_and_features.len() != 1 {
622 bail!(r#"cannot fix edition when using `feature-unification = "package"`."#);
623 }
624 let resolved_features = &ws_resolve
625 .specs_and_features
626 .first()
627 .expect("We've already checked that there is exactly one.")
628 .resolved_features;
629 let diffs = v2_features.compare_legacy(resolved_features);
630 Ok((ws_resolve, diffs))
631 };
632 let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?;
633 let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?;
634 if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() {
635 return Ok(());
637 }
638 with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals));
640 let gctx = ws.gctx();
641 gctx.shell().note(
642 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
643 )?;
644 drop_eprintln!(
645 gctx,
646 "This may cause some dependencies to be built with fewer features enabled than previously."
647 );
648 drop_eprintln!(
649 gctx,
650 "More information about the resolver changes may be found \
651 at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
652 );
653 drop_eprintln!(
654 gctx,
655 "When building the following dependencies, \
656 the given features will no longer be used:\n"
657 );
658 let show_diffs = |differences: DiffMap| {
659 for ((pkg_id, features_for), removed) in differences {
660 drop_eprint!(gctx, " {}", pkg_id);
661 if let FeaturesFor::HostDep = features_for {
662 drop_eprint!(gctx, " (as host dependency)");
663 }
664 drop_eprint!(gctx, " removed features: ");
665 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
666 drop_eprintln!(gctx, "{}", joined.join(", "));
667 }
668 drop_eprint!(gctx, "\n");
669 };
670 if !without_dev_diffs.is_empty() {
671 show_diffs(without_dev_diffs);
672 }
673 if !with_dev_diffs.is_empty() {
674 drop_eprintln!(
675 gctx,
676 "The following differences only apply when building with dev-dependencies:\n"
677 );
678 show_diffs(with_dev_diffs);
679 }
680 report_maybe_diesel(gctx, &ws_resolve.targeted_resolve)?;
681 Ok(())
682}
683
684fn report_maybe_diesel(gctx: &GlobalContext, resolve: &Resolve) -> CargoResult<()> {
685 fn is_broken_diesel(pid: PackageId) -> bool {
686 pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8)
687 }
688
689 fn is_broken_diesel_migration(pid: PackageId) -> bool {
690 pid.name() == "diesel_migrations" && pid.version().major <= 1
691 }
692
693 if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) {
694 gctx.shell().note(
695 "\
696This project appears to use both diesel and diesel_migrations. These packages have
697a known issue where the build may fail due to the version 2 resolver preventing
698feature unification between those two packages. Please update to at least diesel 1.4.8
699to prevent this issue from happening.
700",
701 )?;
702 }
703 Ok(())
704}
705
706pub fn fix_get_proxy_lock_addr() -> Option<String> {
711 #[expect(
712 clippy::disallowed_methods,
713 reason = "internal only, no reason for config support"
714 )]
715 env::var(FIX_ENV_INTERNAL).ok()
716}
717
718pub fn fix_exec_rustc(gctx: &GlobalContext, lock_addr: &str) -> CargoResult<()> {
727 let args = FixArgs::get()?;
728 trace!("cargo-fix as rustc got file {:?}", args.file);
729
730 let workspace_rustc = gctx
731 .get_env("RUSTC_WORKSPACE_WRAPPER")
732 .map(PathBuf::from)
733 .ok();
734 let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
735 rustc.retry_with_argfile(true);
736 rustc.env_remove(FIX_ENV_INTERNAL);
737 args.apply(&mut rustc);
738 if let Some(client) = gctx.jobserver_from_env() {
741 rustc.inherit_jobserver(client);
742 }
743
744 trace!("start rustfixing {:?}", args.file);
745 let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, gctx)?;
746
747 if fixes.last_output.status.success() {
748 for (path, file) in fixes.files.iter() {
749 Message::Fixed {
750 file: path.clone(),
751 fixes: file.fixes_applied,
752 }
753 .post(gctx)?;
754 }
755 emit_output(&fixes.last_output)?;
757 return Ok(());
758 }
759
760 let allow_broken_code = gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some();
761
762 if !allow_broken_code {
766 for (path, file) in fixes.files.iter() {
767 debug!("reverting {:?} due to errors", path);
768 paths::write(path, &file.original_code)?;
769 }
770 }
771
772 if fixes.files.is_empty() {
778 emit_output(&fixes.last_output)?;
780 exit_with(fixes.last_output.status);
781 } else {
782 let krate = {
783 let mut iter = rustc.get_args();
784 let mut krate = None;
785 while let Some(arg) = iter.next() {
786 if arg == "--crate-name" {
787 krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
788 }
789 }
790 krate
791 };
792 log_failed_fix(
793 gctx,
794 krate,
795 &fixes.last_output.stderr,
796 fixes.last_output.status,
797 )?;
798 emit_output(&fixes.first_output)?;
802 exit_with(fixes.first_output.status);
806 }
807}
808
809fn emit_output(output: &Output) -> CargoResult<()> {
810 std::io::stderr().write_all(&output.stderr)?;
814 std::io::stdout().write_all(&output.stdout)?;
815 Ok(())
816}
817
818struct FixedCrate {
819 files: HashMap<String, FixedFile>,
821 first_output: Output,
827 last_output: Output,
832}
833
834#[derive(Debug)]
835struct FixedFile {
836 errors_applying_fixes: Vec<String>,
837 fixes_applied: u32,
838 original_code: String,
839}
840
841fn rustfix_crate(
846 lock_addr: &str,
847 rustc: &ProcessBuilder,
848 filename: &Path,
849 args: &FixArgs,
850 gctx: &GlobalContext,
851) -> CargoResult<FixedCrate> {
852 let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
862
863 let mut files = HashMap::new();
865
866 if !args.can_run_rustfix(gctx)? {
867 debug!("can't fix {filename:?}, running rustc: {rustc}");
871 let last_output = rustc.output()?;
872 let fixes = FixedCrate {
873 files,
874 first_output: last_output.clone(),
875 last_output,
876 };
877 return Ok(fixes);
878 }
879
880 let max_iterations = gctx
914 .get_env("CARGO_FIX_MAX_RETRIES")
915 .ok()
916 .and_then(|n| n.parse().ok())
917 .unwrap_or(4);
918 let mut last_output;
919 let mut last_made_changes;
920 let mut first_output = None;
921 let mut current_iteration = 0;
922 loop {
923 for file in files.values_mut() {
924 file.errors_applying_fixes.clear();
926 }
927 (last_output, last_made_changes) =
928 rustfix_and_fix(&mut files, rustc, filename, args, gctx)?;
929 if current_iteration == 0 {
930 first_output = Some(last_output.clone());
931 }
932 let mut progress_yet_to_be_made = false;
933 for (path, file) in files.iter_mut() {
934 if file.errors_applying_fixes.is_empty() {
935 continue;
936 }
937 debug!("had rustfix apply errors in {path:?} {file:?}");
938 if last_made_changes {
942 progress_yet_to_be_made = true;
943 }
944 }
945 if !progress_yet_to_be_made {
946 break;
947 }
948 current_iteration += 1;
949 if current_iteration >= max_iterations {
950 break;
951 }
952 }
953 if last_made_changes {
954 debug!("calling rustc one last time for final results: {rustc}");
955 last_output = rustc.output()?;
956 }
957
958 for (path, file) in files.iter_mut() {
961 for error in file.errors_applying_fixes.drain(..) {
962 Message::ReplaceFailed {
963 file: path.clone(),
964 message: error,
965 }
966 .post(gctx)?;
967 }
968 }
969
970 Ok(FixedCrate {
971 files,
972 first_output: first_output.expect("at least one iteration"),
973 last_output,
974 })
975}
976
977fn rustfix_and_fix(
982 files: &mut HashMap<String, FixedFile>,
983 rustc: &ProcessBuilder,
984 filename: &Path,
985 args: &FixArgs,
986 gctx: &GlobalContext,
987) -> CargoResult<(Output, bool)> {
988 let only = HashSet::new();
991
992 debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
993 let output = rustc.output()?;
994
995 if !output.status.success() && gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() {
1001 debug!(
1002 "rustfixing `{:?}` failed, rustc exited with {:?}",
1003 filename,
1004 output.status.code()
1005 );
1006 return Ok((output, false));
1007 }
1008
1009 let fix_mode = gctx
1010 .get_env_os("__CARGO_FIX_YOLO")
1011 .map(|_| rustfix::Filter::Everything)
1012 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
1013
1014 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
1017
1018 let suggestions = stderr
1019 .lines()
1020 .filter(|x| !x.is_empty())
1021 .inspect(|y| trace!("line: {}", y))
1022 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
1024 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
1026
1027 let mut file_map = HashMap::new();
1029 let mut num_suggestion = 0;
1030 let home_path = gctx.home().as_path_unlocked();
1032 for suggestion in suggestions {
1033 trace!("suggestion");
1034 let file_names = suggestion
1038 .solutions
1039 .iter()
1040 .flat_map(|s| s.replacements.iter())
1041 .map(|r| &r.snippet.file_name);
1042
1043 let file_name = if let Some(file_name) = file_names.clone().next() {
1044 file_name.clone()
1045 } else {
1046 trace!("rejecting as it has no solutions {:?}", suggestion);
1047 continue;
1048 };
1049
1050 let file_path = Path::new(&file_name);
1051 if file_path.starts_with(home_path) {
1053 continue;
1054 }
1055 if let Some(sysroot) = args.sysroot.as_deref() {
1057 if file_path.starts_with(sysroot) {
1058 continue;
1059 }
1060 }
1061
1062 if !file_names.clone().all(|f| f == &file_name) {
1063 trace!("rejecting as it changes multiple files: {:?}", suggestion);
1064 continue;
1065 }
1066
1067 trace!("adding suggestion for {:?}: {:?}", file_name, suggestion);
1068 file_map
1069 .entry(file_name)
1070 .or_insert_with(Vec::new)
1071 .push(suggestion);
1072 num_suggestion += 1;
1073 }
1074
1075 debug!(
1076 "collected {} suggestions for `{}`",
1077 num_suggestion,
1078 filename.display(),
1079 );
1080
1081 let mut made_changes = false;
1082 for (file, suggestions) in file_map {
1083 let code = match paths::read(file.as_ref()) {
1087 Ok(s) => s,
1088 Err(e) => {
1089 warn!("failed to read `{}`: {}", file, e);
1090 continue;
1091 }
1092 };
1093 let num_suggestions = suggestions.len();
1094 debug!("applying {} fixes to {}", num_suggestions, file);
1095
1096 let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile {
1101 errors_applying_fixes: Vec::new(),
1102 fixes_applied: 0,
1103 original_code: code.clone(),
1104 });
1105 let mut fixed = CodeFix::new(&code);
1106
1107 for suggestion in suggestions.iter().rev() {
1108 match fixed.apply(suggestion) {
1116 Ok(()) => fixed_file.fixes_applied += 1,
1117 Err(rustfix::Error::AlreadyReplaced {
1118 is_identical: true, ..
1119 }) => continue,
1120 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
1121 }
1122 }
1123 if fixed.modified() {
1124 made_changes = true;
1125 let new_code = fixed.finish()?;
1126 paths::write(&file, new_code)?;
1127 }
1128 }
1129
1130 Ok((output, made_changes))
1131}
1132
1133fn exit_with(status: ExitStatus) -> ! {
1134 #[cfg(unix)]
1135 {
1136 use std::os::unix::prelude::*;
1137 if let Some(signal) = status.signal() {
1138 drop(writeln!(
1139 std::io::stderr().lock(),
1140 "child failed with signal `{}`",
1141 signal
1142 ));
1143 process::exit(2);
1144 }
1145 }
1146 process::exit(status.code().unwrap_or(3));
1147}
1148
1149fn log_failed_fix(
1150 gctx: &GlobalContext,
1151 krate: Option<String>,
1152 stderr: &[u8],
1153 status: ExitStatus,
1154) -> CargoResult<()> {
1155 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
1156
1157 let diagnostics = stderr
1158 .lines()
1159 .filter(|x| !x.is_empty())
1160 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
1161 let mut files = BTreeSet::new();
1162 let mut errors = Vec::new();
1163 for diagnostic in diagnostics {
1164 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
1165 for span in diagnostic.spans.into_iter() {
1166 files.insert(span.file_name);
1167 }
1168 }
1169 errors.extend(
1171 stderr
1172 .lines()
1173 .filter(|x| !x.starts_with('{'))
1174 .map(|x| x.to_string()),
1175 );
1176
1177 let files = files.into_iter().collect();
1178 let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) {
1179 None
1180 } else {
1181 Some(exit_status_to_string(status))
1182 };
1183 Message::FixFailed {
1184 files,
1185 krate,
1186 errors,
1187 abnormal_exit,
1188 }
1189 .post(gctx)?;
1190
1191 Ok(())
1192}
1193
1194struct FixArgs {
1197 file: PathBuf,
1199 prepare_for_edition: Option<Edition>,
1202 idioms: bool,
1204 enabled_edition: Option<Edition>,
1208 other: Vec<OsString>,
1211 rustc: PathBuf,
1213 sysroot: Option<PathBuf>,
1215}
1216
1217impl FixArgs {
1218 fn get() -> CargoResult<FixArgs> {
1219 Self::from_args(env::args_os())
1220 }
1221
1222 fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> {
1224 let mut argv = argv.into_iter();
1225 let mut rustc = argv
1226 .nth(1)
1227 .map(PathBuf::from)
1228 .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?;
1229 let mut file = None;
1230 let mut enabled_edition = None;
1231 let mut other = Vec::new();
1232
1233 let mut handle_arg = |arg: OsString| -> CargoResult<()> {
1234 let path = PathBuf::from(arg);
1235 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
1236 file = Some(path);
1237 return Ok(());
1238 }
1239 if let Some(s) = path.to_str() {
1240 if let Some(edition) = s.strip_prefix("--edition=") {
1241 enabled_edition = Some(edition.parse()?);
1242 return Ok(());
1243 }
1244 }
1245 other.push(path.into());
1246 Ok(())
1247 };
1248
1249 if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") {
1250 if argv.next().is_some() {
1253 bail!("argfile `@path` cannot be combined with other arguments");
1254 }
1255 let contents = fs::read_to_string(argfile_path)
1256 .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?;
1257 let mut iter = contents.lines().map(OsString::from);
1258 rustc = iter
1259 .next()
1260 .map(PathBuf::from)
1261 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
1262 for arg in iter {
1263 handle_arg(arg)?;
1264 }
1265 } else {
1266 for arg in argv {
1267 handle_arg(arg)?;
1268 }
1269 }
1270
1271 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
1272 #[expect(
1273 clippy::disallowed_methods,
1274 reason = "internal only, no reason for config support"
1275 )]
1276 let idioms = env::var(IDIOMS_ENV_INTERNAL).is_ok();
1277
1278 #[expect(
1279 clippy::disallowed_methods,
1280 reason = "internal only, no reason for config support"
1281 )]
1282 let prepare_for_edition = env::var(EDITION_ENV_INTERNAL).ok().map(|v| {
1283 let enabled_edition = enabled_edition.unwrap_or(Edition::Edition2015);
1284 let mode = EditionFixMode::from_str(&v);
1285 mode.next_edition(enabled_edition)
1286 });
1287
1288 #[expect(
1289 clippy::disallowed_methods,
1290 reason = "internal only, no reason for config support"
1291 )]
1292 let sysroot = env::var_os(SYSROOT_INTERNAL).map(PathBuf::from);
1293
1294 Ok(FixArgs {
1295 file,
1296 prepare_for_edition,
1297 idioms,
1298 enabled_edition,
1299 other,
1300 rustc,
1301 sysroot,
1302 })
1303 }
1304
1305 fn apply(&self, cmd: &mut ProcessBuilder) {
1306 cmd.arg(&self.file);
1307 cmd.args(&self.other);
1308 if self.prepare_for_edition.is_some() {
1309 cmd.arg("--cap-lints=allow");
1314 } else {
1315 cmd.arg("--cap-lints=warn");
1317 }
1318 if let Some(edition) = self.enabled_edition {
1319 cmd.arg("--edition").arg(edition.to_string());
1320 if self.idioms && edition.supports_idiom_lint() {
1321 cmd.arg(format!("-Wrust-{}-idioms", edition));
1322 }
1323 }
1324
1325 if let Some(edition) = self.prepare_for_edition {
1326 edition.force_warn_arg(cmd);
1327 }
1328 }
1329
1330 fn can_run_rustfix(&self, gctx: &GlobalContext) -> CargoResult<bool> {
1333 let Some(to_edition) = self.prepare_for_edition else {
1334 return Message::Fixing {
1335 file: self.file.display().to_string(),
1336 }
1337 .post(gctx)
1338 .and(Ok(true));
1339 };
1340 if !to_edition.is_stable() && !gctx.nightly_features_allowed {
1351 let message = format!(
1352 "`{file}` is on the latest edition, but trying to \
1353 migrate to edition {to_edition}.\n\
1354 Edition {to_edition} is unstable and not allowed in \
1355 this release, consider trying the nightly release channel.",
1356 file = self.file.display(),
1357 to_edition = to_edition
1358 );
1359 return Message::EditionAlreadyEnabled {
1360 message,
1361 edition: to_edition.previous().unwrap(),
1362 }
1363 .post(gctx)
1364 .and(Ok(false)); }
1366 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
1367 if from_edition == to_edition {
1368 let message = format!(
1369 "`{}` is already on the latest edition ({}), \
1370 unable to migrate further",
1371 self.file.display(),
1372 to_edition
1373 );
1374 Message::EditionAlreadyEnabled {
1375 message,
1376 edition: to_edition,
1377 }
1378 .post(gctx)
1379 } else {
1380 Message::Migrating {
1381 file: self.file.display().to_string(),
1382 from_edition,
1383 to_edition,
1384 }
1385 .post(gctx)
1386 }
1387 .and(Ok(true))
1388 }
1389}
1390
1391#[cfg(test)]
1392mod tests {
1393 use super::FixArgs;
1394 use std::ffi::OsString;
1395 use std::io::Write as _;
1396 use std::path::PathBuf;
1397
1398 #[test]
1399 fn get_fix_args_from_argfile() {
1400 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1401 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1402
1403 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1404 temp.write_all(content.as_bytes()).unwrap();
1405
1406 let argfile = format!("@{}", temp.path().display());
1407 let args = ["cargo", &argfile];
1408 let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap();
1409 assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc"));
1410 assert_eq!(fix_args.file, main_rs.path());
1411 assert_eq!(fix_args.other, vec![OsString::from("foobar")]);
1412 }
1413
1414 #[test]
1415 fn get_fix_args_from_argfile_with_extra_arg() {
1416 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1417 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1418
1419 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1420 temp.write_all(content.as_bytes()).unwrap();
1421
1422 let argfile = format!("@{}", temp.path().display());
1423 let args = ["cargo", &argfile, "boo!"];
1424 match FixArgs::from_args(args.map(|x| x.into())) {
1425 Err(e) => assert_eq!(
1426 e.to_string(),
1427 "argfile `@path` cannot be combined with other arguments"
1428 ),
1429 Ok(_) => panic!("should fail"),
1430 }
1431 }
1432}