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