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