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 Some(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 if !pkg.manifest().is_embedded()
298 || pkg
299 .manifest()
300 .original_toml()
301 .and_then(|m| m.package())
302 .map(|pkg| pkg.edition.is_some())
303 .unwrap_or(false)
304 {
305 continue;
306 }
307 let file = pkg.manifest_path();
308 let file = file.strip_prefix(ws.root()).unwrap_or(file);
309 let file = file.display();
310
311 let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
312 let document = &mut manifest_mut.data;
313 let mut fixes = 0;
314
315 let root = document.as_table_mut();
316
317 fixes += 1;
318 root.entry("package").or_insert_with(|| {
319 let mut t = toml_edit::Table::new();
320 t.set_position(Some(-1));
321 t.into()
322 });
323 root["package"]["edition"] = crate::core::features::Edition::LATEST_STABLE
324 .to_string()
325 .into();
326
327 if 0 < fixes {
328 let verb = if fixes == 1 { "fix" } else { "fixes" };
329 let msg = format!("{file} ({fixes} {verb})");
330 ws.gctx().shell().status("Fixed", msg)?;
331
332 manifest_mut.write()?;
333 }
334 }
335
336 Ok(())
337}
338
339fn migrate_manifests(
340 ws: &Workspace<'_>,
341 pkgs: &[&Package],
342 edition_mode: EditionFixMode,
343) -> CargoResult<()> {
344 if matches!(ws.root_maybe(), MaybePackage::Virtual(_)) {
347 let highest_edition = pkgs
350 .iter()
351 .map(|p| p.manifest().edition())
352 .max()
353 .unwrap_or_default();
354 let prepare_for_edition = edition_mode.next_edition(highest_edition);
355 if highest_edition == prepare_for_edition
356 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
357 {
358 } else {
360 let mut manifest_mut = LocalManifest::try_new(ws.root_manifest())?;
361 let document = &mut manifest_mut.data;
362 let mut fixes = 0;
363
364 if Edition::Edition2024 <= prepare_for_edition {
365 let root = document.as_table_mut();
366
367 if let Some(workspace) = root
368 .get_mut("workspace")
369 .and_then(|t| t.as_table_like_mut())
370 {
371 fixes += rename_dep_fields_2024(workspace, "dependencies");
374 }
375 }
376
377 if 0 < fixes {
378 let file = ws.root_manifest();
381 let file = file.strip_prefix(ws.root()).unwrap_or(file);
382 let file = file.display();
383 ws.gctx().shell().status(
384 "Migrating",
385 format!("{file} from {highest_edition} edition to {prepare_for_edition}"),
386 )?;
387
388 let verb = if fixes == 1 { "fix" } else { "fixes" };
389 let msg = format!("{file} ({fixes} {verb})");
390 ws.gctx().shell().status("Fixed", msg)?;
391
392 manifest_mut.write()?;
393 }
394 }
395 }
396
397 for pkg in pkgs {
398 let existing_edition = pkg.manifest().edition();
399 let prepare_for_edition = edition_mode.next_edition(existing_edition);
400 if existing_edition == prepare_for_edition
401 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
402 {
403 continue;
404 }
405 let file = pkg.manifest_path();
406 let file = file.strip_prefix(ws.root()).unwrap_or(file);
407 let file = file.display();
408 ws.gctx().shell().status(
409 "Migrating",
410 format!("{file} from {existing_edition} edition to {prepare_for_edition}"),
411 )?;
412
413 let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
414 let document = &mut manifest_mut.data;
415 let mut fixes = 0;
416
417 let ws_original_toml = match ws.root_maybe() {
418 MaybePackage::Package(package) => package.manifest().original_toml(),
419 MaybePackage::Virtual(manifest) => manifest.original_toml(),
420 };
421
422 if Edition::Edition2024 <= prepare_for_edition {
423 let root = document.as_table_mut();
424
425 if let Some(workspace) = root
426 .get_mut("workspace")
427 .and_then(|t| t.as_table_like_mut())
428 {
429 fixes += rename_dep_fields_2024(workspace, "dependencies");
432 }
433
434 fixes += rename_table(root, "project", "package");
435 if let Some(target) = root.get_mut("lib").and_then(|t| t.as_table_like_mut()) {
436 fixes += rename_target_fields_2024(target);
437 }
438 fixes += rename_array_of_target_fields_2024(root, "bin");
439 fixes += rename_array_of_target_fields_2024(root, "example");
440 fixes += rename_array_of_target_fields_2024(root, "test");
441 fixes += rename_array_of_target_fields_2024(root, "bench");
442 fixes += rename_dep_fields_2024(root, "dependencies");
443 fixes += remove_ignored_default_features_2024(root, "dependencies", ws_original_toml);
444 fixes += rename_table(root, "dev_dependencies", "dev-dependencies");
445 fixes += rename_dep_fields_2024(root, "dev-dependencies");
446 fixes +=
447 remove_ignored_default_features_2024(root, "dev-dependencies", ws_original_toml);
448 fixes += rename_table(root, "build_dependencies", "build-dependencies");
449 fixes += rename_dep_fields_2024(root, "build-dependencies");
450 fixes +=
451 remove_ignored_default_features_2024(root, "build-dependencies", ws_original_toml);
452 for target in root
453 .get_mut("target")
454 .and_then(|t| t.as_table_like_mut())
455 .iter_mut()
456 .flat_map(|t| t.iter_mut())
457 .filter_map(|(_k, t)| t.as_table_like_mut())
458 {
459 fixes += rename_dep_fields_2024(target, "dependencies");
460 fixes +=
461 remove_ignored_default_features_2024(target, "dependencies", ws_original_toml);
462 fixes += rename_table(target, "dev_dependencies", "dev-dependencies");
463 fixes += rename_dep_fields_2024(target, "dev-dependencies");
464 fixes += remove_ignored_default_features_2024(
465 target,
466 "dev-dependencies",
467 ws_original_toml,
468 );
469 fixes += rename_table(target, "build_dependencies", "build-dependencies");
470 fixes += rename_dep_fields_2024(target, "build-dependencies");
471 fixes += remove_ignored_default_features_2024(
472 target,
473 "build-dependencies",
474 ws_original_toml,
475 );
476 }
477 }
478
479 if 0 < fixes {
480 let verb = if fixes == 1 { "fix" } else { "fixes" };
481 let msg = format!("{file} ({fixes} {verb})");
482 ws.gctx().shell().status("Fixed", msg)?;
483
484 manifest_mut.write()?;
485 }
486 }
487
488 Ok(())
489}
490
491fn rename_dep_fields_2024(parent: &mut dyn toml_edit::TableLike, dep_kind: &str) -> usize {
492 let mut fixes = 0;
493 for target in parent
494 .get_mut(dep_kind)
495 .and_then(|t| t.as_table_like_mut())
496 .iter_mut()
497 .flat_map(|t| t.iter_mut())
498 .filter_map(|(_k, t)| t.as_table_like_mut())
499 {
500 fixes += rename_table(target, "default_features", "default-features");
501 }
502 fixes
503}
504
505fn remove_ignored_default_features_2024(
506 parent: &mut dyn toml_edit::TableLike,
507 dep_kind: &str,
508 ws_original_toml: Option<&TomlManifest>,
509) -> usize {
510 let Some(ws_original_toml) = ws_original_toml else {
511 return 0;
512 };
513
514 let mut fixes = 0;
515 for (name_in_toml, target) in parent
516 .get_mut(dep_kind)
517 .and_then(|t| t.as_table_like_mut())
518 .iter_mut()
519 .flat_map(|t| t.iter_mut())
520 .filter_map(|(k, t)| t.as_table_like_mut().map(|t| (k, t)))
521 {
522 let name_in_toml: &str = &name_in_toml;
523 let ws_deps = ws_original_toml
524 .workspace
525 .as_ref()
526 .and_then(|ws| ws.dependencies.as_ref());
527 if let Some(ws_dep) = ws_deps.and_then(|ws_deps| ws_deps.get(name_in_toml)) {
528 if ws_dep.default_features() == Some(false) {
529 continue;
530 }
531 }
532 if target
533 .get("workspace")
534 .and_then(|i| i.as_value())
535 .and_then(|i| i.as_bool())
536 == Some(true)
537 && target
538 .get("default-features")
539 .and_then(|i| i.as_value())
540 .and_then(|i| i.as_bool())
541 == Some(false)
542 {
543 target.remove("default-features");
544 fixes += 1;
545 }
546 }
547 fixes
548}
549
550fn rename_array_of_target_fields_2024(root: &mut dyn toml_edit::TableLike, kind: &str) -> usize {
551 let mut fixes = 0;
552 for target in root
553 .get_mut(kind)
554 .and_then(|t| t.as_array_of_tables_mut())
555 .iter_mut()
556 .flat_map(|t| t.iter_mut())
557 {
558 fixes += rename_target_fields_2024(target);
559 }
560 fixes
561}
562
563fn rename_target_fields_2024(target: &mut dyn toml_edit::TableLike) -> usize {
564 let mut fixes = 0;
565 fixes += rename_table(target, "crate_type", "crate-type");
566 fixes += rename_table(target, "proc_macro", "proc-macro");
567 fixes
568}
569
570fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> usize {
571 let Some(old_key) = parent.key(old).cloned() else {
572 return 0;
573 };
574
575 let project = parent.remove(old).expect("returned early");
576 if !parent.contains_key(new) {
577 parent.insert(new, project);
578 let mut new_key = parent.key_mut(new).expect("just inserted");
579 *new_key.dotted_decor_mut() = old_key.dotted_decor().clone();
580 *new_key.leaf_decor_mut() = old_key.leaf_decor().clone();
581 }
582 1
583}
584
585fn check_resolver_change<'gctx>(
586 ws: &Workspace<'gctx>,
587 target_data: &mut RustcTargetData<'gctx>,
588 opts: &FixOptions,
589) -> CargoResult<()> {
590 let root = ws.root_maybe();
591 match root {
592 MaybePackage::Package(root_pkg) => {
593 if root_pkg.manifest().resolve_behavior().is_some() {
594 return Ok(());
596 }
597 let pkgs = opts.compile_opts.spec.get_packages(ws)?;
599 if !pkgs.contains(&root_pkg) {
600 return Ok(());
602 }
603 if root_pkg.manifest().edition() != Edition::Edition2018 {
604 return Ok(());
606 }
607 }
608 MaybePackage::Virtual(_vm) => {
609 return Ok(());
611 }
612 }
613 assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
615 let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
616 let mut resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> {
617 let dry_run = false;
618 let ws_resolve = ops::resolve_ws_with_opts(
619 ws,
620 target_data,
621 &opts.compile_opts.build_config.requested_kinds,
622 &opts.compile_opts.cli_features,
623 &specs,
624 has_dev_units,
625 crate::core::resolver::features::ForceAllTargets::No,
626 dry_run,
627 )?;
628
629 let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units);
630 let v2_features = FeatureResolver::resolve(
631 ws,
632 target_data,
633 &ws_resolve.targeted_resolve,
634 &ws_resolve.pkg_set,
635 &opts.compile_opts.cli_features,
636 &specs,
637 &opts.compile_opts.build_config.requested_kinds,
638 feature_opts,
639 )?;
640
641 if ws_resolve.specs_and_features.len() != 1 {
642 bail!(r#"cannot fix edition when using `feature-unification = "package"`."#);
643 }
644 let resolved_features = &ws_resolve
645 .specs_and_features
646 .first()
647 .expect("We've already checked that there is exactly one.")
648 .resolved_features;
649 let diffs = v2_features.compare_legacy(resolved_features);
650 Ok((ws_resolve, diffs))
651 };
652 let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?;
653 let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?;
654 if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() {
655 return Ok(());
657 }
658 with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals));
660 let gctx = ws.gctx();
661 gctx.shell().note(
662 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
663 )?;
664 drop_eprintln!(
665 gctx,
666 "This may cause some dependencies to be built with fewer features enabled than previously."
667 );
668 drop_eprintln!(
669 gctx,
670 "More information about the resolver changes may be found \
671 at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
672 );
673 drop_eprintln!(
674 gctx,
675 "When building the following dependencies, \
676 the given features will no longer be used:\n"
677 );
678 let show_diffs = |differences: DiffMap| {
679 for ((pkg_id, features_for), removed) in differences {
680 drop_eprint!(gctx, " {}", pkg_id);
681 if let FeaturesFor::HostDep = features_for {
682 drop_eprint!(gctx, " (as host dependency)");
683 }
684 drop_eprint!(gctx, " removed features: ");
685 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
686 drop_eprintln!(gctx, "{}", joined.join(", "));
687 }
688 drop_eprint!(gctx, "\n");
689 };
690 if !without_dev_diffs.is_empty() {
691 show_diffs(without_dev_diffs);
692 }
693 if !with_dev_diffs.is_empty() {
694 drop_eprintln!(
695 gctx,
696 "The following differences only apply when building with dev-dependencies:\n"
697 );
698 show_diffs(with_dev_diffs);
699 }
700 report_maybe_diesel(gctx, &ws_resolve.targeted_resolve)?;
701 Ok(())
702}
703
704fn report_maybe_diesel(gctx: &GlobalContext, resolve: &Resolve) -> CargoResult<()> {
705 fn is_broken_diesel(pid: PackageId) -> bool {
706 pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8)
707 }
708
709 fn is_broken_diesel_migration(pid: PackageId) -> bool {
710 pid.name() == "diesel_migrations" && pid.version().major <= 1
711 }
712
713 if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) {
714 gctx.shell().note(
715 "\
716This project appears to use both diesel and diesel_migrations. These packages have
717a known issue where the build may fail due to the version 2 resolver preventing
718feature unification between those two packages. Please update to at least diesel 1.4.8
719to prevent this issue from happening.
720",
721 )?;
722 }
723 Ok(())
724}
725
726pub fn fix_get_proxy_lock_addr() -> Option<String> {
731 #[expect(
732 clippy::disallowed_methods,
733 reason = "internal only, no reason for config support"
734 )]
735 env::var(FIX_ENV_INTERNAL).ok()
736}
737
738pub fn fix_exec_rustc(gctx: &GlobalContext, lock_addr: &str) -> CargoResult<()> {
747 let args = FixArgs::get()?;
748 trace!("cargo-fix as rustc got file {:?}", args.file);
749
750 let workspace_rustc = gctx
751 .get_env("RUSTC_WORKSPACE_WRAPPER")
752 .map(PathBuf::from)
753 .ok();
754 let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
755 rustc.retry_with_argfile(true);
756 rustc.env_remove(FIX_ENV_INTERNAL);
757 args.apply(&mut rustc);
758 if let Some(client) = gctx.jobserver_from_env() {
761 rustc.inherit_jobserver(client);
762 }
763
764 trace!("start rustfixing {:?}", args.file);
765 let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, gctx)?;
766
767 if fixes.last_output.status.success() {
768 for (path, file) in fixes.files.iter() {
769 Message::Fixed {
770 file: path.clone(),
771 fixes: file.fixes_applied,
772 }
773 .post(gctx)?;
774 }
775 emit_output(&fixes.last_output)?;
777 return Ok(());
778 }
779
780 let allow_broken_code = gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some();
781
782 if !allow_broken_code {
786 for (path, file) in fixes.files.iter() {
787 debug!("reverting {:?} due to errors", path);
788 paths::write(path, &file.original_code)?;
789 }
790 }
791
792 if fixes.files.is_empty() {
798 emit_output(&fixes.last_output)?;
800 exit_with(fixes.last_output.status);
801 } else {
802 let krate = {
803 let mut iter = rustc.get_args();
804 let mut krate = None;
805 while let Some(arg) = iter.next() {
806 if arg == "--crate-name" {
807 krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
808 }
809 }
810 krate
811 };
812 log_failed_fix(
813 gctx,
814 krate,
815 &fixes.last_output.stderr,
816 fixes.last_output.status,
817 )?;
818 emit_output(&fixes.first_output)?;
822 exit_with(fixes.first_output.status);
826 }
827}
828
829fn emit_output(output: &Output) -> CargoResult<()> {
830 std::io::stderr().write_all(&output.stderr)?;
834 std::io::stdout().write_all(&output.stdout)?;
835 Ok(())
836}
837
838struct FixedCrate {
839 files: HashMap<String, FixedFile>,
841 first_output: Output,
847 last_output: Output,
852}
853
854#[derive(Debug)]
855struct FixedFile {
856 errors_applying_fixes: Vec<String>,
857 fixes_applied: u32,
858 original_code: String,
859}
860
861fn rustfix_crate(
866 lock_addr: &str,
867 rustc: &ProcessBuilder,
868 filename: &Path,
869 args: &FixArgs,
870 gctx: &GlobalContext,
871) -> CargoResult<FixedCrate> {
872 let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
882
883 let mut files = HashMap::new();
885
886 if !args.can_run_rustfix(gctx)? {
887 debug!("can't fix {filename:?}, running rustc: {rustc}");
891 let last_output = rustc.output()?;
892 let fixes = FixedCrate {
893 files,
894 first_output: last_output.clone(),
895 last_output,
896 };
897 return Ok(fixes);
898 }
899
900 let max_iterations = gctx
934 .get_env("CARGO_FIX_MAX_RETRIES")
935 .ok()
936 .and_then(|n| n.parse().ok())
937 .unwrap_or(4);
938 let mut last_output;
939 let mut last_made_changes;
940 let mut first_output = None;
941 let mut current_iteration = 0;
942 loop {
943 for file in files.values_mut() {
944 file.errors_applying_fixes.clear();
946 }
947 (last_output, last_made_changes) =
948 rustfix_and_fix(&mut files, rustc, filename, args, gctx)?;
949 if current_iteration == 0 {
950 first_output = Some(last_output.clone());
951 }
952 let mut progress_yet_to_be_made = false;
953 for (path, file) in files.iter_mut() {
954 if file.errors_applying_fixes.is_empty() {
955 continue;
956 }
957 debug!("had rustfix apply errors in {path:?} {file:?}");
958 if last_made_changes {
962 progress_yet_to_be_made = true;
963 }
964 }
965 if !progress_yet_to_be_made {
966 break;
967 }
968 current_iteration += 1;
969 if current_iteration >= max_iterations {
970 break;
971 }
972 }
973 if last_made_changes {
974 debug!("calling rustc one last time for final results: {rustc}");
975 last_output = rustc.output()?;
976 }
977
978 for (path, file) in files.iter_mut() {
981 for error in file.errors_applying_fixes.drain(..) {
982 Message::ReplaceFailed {
983 file: path.clone(),
984 message: error,
985 }
986 .post(gctx)?;
987 }
988 }
989
990 Ok(FixedCrate {
991 files,
992 first_output: first_output.expect("at least one iteration"),
993 last_output,
994 })
995}
996
997fn rustfix_and_fix(
1002 files: &mut HashMap<String, FixedFile>,
1003 rustc: &ProcessBuilder,
1004 filename: &Path,
1005 args: &FixArgs,
1006 gctx: &GlobalContext,
1007) -> CargoResult<(Output, bool)> {
1008 let only = HashSet::new();
1011
1012 debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
1013 let output = rustc.output()?;
1014
1015 if !output.status.success() && gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() {
1021 debug!(
1022 "rustfixing `{:?}` failed, rustc exited with {:?}",
1023 filename,
1024 output.status.code()
1025 );
1026 return Ok((output, false));
1027 }
1028
1029 let fix_mode = gctx
1030 .get_env_os("__CARGO_FIX_YOLO")
1031 .map(|_| rustfix::Filter::Everything)
1032 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
1033
1034 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
1037
1038 let suggestions = stderr
1039 .lines()
1040 .filter(|x| !x.is_empty())
1041 .inspect(|y| trace!("line: {}", y))
1042 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
1044 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
1046
1047 let mut file_map = HashMap::new();
1049 let mut num_suggestion = 0;
1050 let home_path = gctx.home().as_path_unlocked();
1052 for suggestion in suggestions {
1053 trace!("suggestion");
1054 let file_names = suggestion
1058 .solutions
1059 .iter()
1060 .flat_map(|s| s.replacements.iter())
1061 .map(|r| &r.snippet.file_name);
1062
1063 let file_name = if let Some(file_name) = file_names.clone().next() {
1064 file_name.clone()
1065 } else {
1066 trace!("rejecting as it has no solutions {:?}", suggestion);
1067 continue;
1068 };
1069
1070 let file_path = Path::new(&file_name);
1071 if file_path.starts_with(home_path) {
1073 continue;
1074 }
1075 if let Some(sysroot) = args.sysroot.as_deref() {
1077 if file_path.starts_with(sysroot) {
1078 continue;
1079 }
1080 }
1081
1082 if !file_names.clone().all(|f| f == &file_name) {
1083 trace!("rejecting as it changes multiple files: {:?}", suggestion);
1084 continue;
1085 }
1086
1087 trace!("adding suggestion for {:?}: {:?}", file_name, suggestion);
1088 file_map
1089 .entry(file_name)
1090 .or_insert_with(Vec::new)
1091 .push(suggestion);
1092 num_suggestion += 1;
1093 }
1094
1095 debug!(
1096 "collected {} suggestions for `{}`",
1097 num_suggestion,
1098 filename.display(),
1099 );
1100
1101 let mut made_changes = false;
1102 for (file, suggestions) in file_map {
1103 let code = match paths::read(file.as_ref()) {
1107 Ok(s) => s,
1108 Err(e) => {
1109 warn!("failed to read `{}`: {}", file, e);
1110 continue;
1111 }
1112 };
1113 let num_suggestions = suggestions.len();
1114 debug!("applying {} fixes to {}", num_suggestions, file);
1115
1116 let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile {
1121 errors_applying_fixes: Vec::new(),
1122 fixes_applied: 0,
1123 original_code: code.clone(),
1124 });
1125 let mut fixed = CodeFix::new(&code);
1126
1127 for suggestion in suggestions.iter().rev() {
1128 match fixed.apply(suggestion) {
1136 Ok(()) => fixed_file.fixes_applied += 1,
1137 Err(rustfix::Error::AlreadyReplaced {
1138 is_identical: true, ..
1139 }) => continue,
1140 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
1141 }
1142 }
1143 if fixed.modified() {
1144 made_changes = true;
1145 let new_code = fixed.finish()?;
1146 paths::write(&file, new_code)?;
1147 }
1148 }
1149
1150 Ok((output, made_changes))
1151}
1152
1153fn exit_with(status: ExitStatus) -> ! {
1154 #[cfg(unix)]
1155 {
1156 use std::os::unix::prelude::*;
1157 if let Some(signal) = status.signal() {
1158 drop(writeln!(
1159 std::io::stderr().lock(),
1160 "child failed with signal `{}`",
1161 signal
1162 ));
1163 process::exit(2);
1164 }
1165 }
1166 process::exit(status.code().unwrap_or(3));
1167}
1168
1169fn log_failed_fix(
1170 gctx: &GlobalContext,
1171 krate: Option<String>,
1172 stderr: &[u8],
1173 status: ExitStatus,
1174) -> CargoResult<()> {
1175 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
1176
1177 let diagnostics = stderr
1178 .lines()
1179 .filter(|x| !x.is_empty())
1180 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
1181 let mut files = BTreeSet::new();
1182 let mut errors = Vec::new();
1183 for diagnostic in diagnostics {
1184 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
1185 for span in diagnostic.spans.into_iter() {
1186 files.insert(span.file_name);
1187 }
1188 }
1189 errors.extend(
1191 stderr
1192 .lines()
1193 .filter(|x| !x.starts_with('{'))
1194 .map(|x| x.to_string()),
1195 );
1196
1197 let files = files.into_iter().collect();
1198 let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) {
1199 None
1200 } else {
1201 Some(exit_status_to_string(status))
1202 };
1203 Message::FixFailed {
1204 files,
1205 krate,
1206 errors,
1207 abnormal_exit,
1208 }
1209 .post(gctx)?;
1210
1211 Ok(())
1212}
1213
1214struct FixArgs {
1217 file: PathBuf,
1219 prepare_for_edition: Option<Edition>,
1222 idioms: bool,
1224 enabled_edition: Option<Edition>,
1228 other: Vec<OsString>,
1231 rustc: PathBuf,
1233 sysroot: Option<PathBuf>,
1235}
1236
1237impl FixArgs {
1238 fn get() -> CargoResult<FixArgs> {
1239 Self::from_args(env::args_os())
1240 }
1241
1242 fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> {
1244 let mut argv = argv.into_iter();
1245 let mut rustc = argv
1246 .nth(1)
1247 .map(PathBuf::from)
1248 .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?;
1249 let mut file = None;
1250 let mut enabled_edition = None;
1251 let mut other = Vec::new();
1252
1253 let mut handle_arg = |arg: OsString| -> CargoResult<()> {
1254 let path = PathBuf::from(arg);
1255 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
1256 file = Some(path);
1257 return Ok(());
1258 }
1259 if let Some(s) = path.to_str() {
1260 if let Some(edition) = s.strip_prefix("--edition=") {
1261 enabled_edition = Some(edition.parse()?);
1262 return Ok(());
1263 }
1264 }
1265 other.push(path.into());
1266 Ok(())
1267 };
1268
1269 if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") {
1270 if argv.next().is_some() {
1273 bail!("argfile `@path` cannot be combined with other arguments");
1274 }
1275 let contents = fs::read_to_string(argfile_path)
1276 .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?;
1277 let mut iter = contents.lines().map(OsString::from);
1278 rustc = iter
1279 .next()
1280 .map(PathBuf::from)
1281 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
1282 for arg in iter {
1283 handle_arg(arg)?;
1284 }
1285 } else {
1286 for arg in argv {
1287 handle_arg(arg)?;
1288 }
1289 }
1290
1291 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
1292 #[expect(
1293 clippy::disallowed_methods,
1294 reason = "internal only, no reason for config support"
1295 )]
1296 let idioms = env::var(IDIOMS_ENV_INTERNAL).is_ok();
1297
1298 #[expect(
1299 clippy::disallowed_methods,
1300 reason = "internal only, no reason for config support"
1301 )]
1302 let prepare_for_edition = env::var(EDITION_ENV_INTERNAL).ok().map(|v| {
1303 let enabled_edition = enabled_edition.unwrap_or(Edition::Edition2015);
1304 let mode = EditionFixMode::from_str(&v);
1305 mode.next_edition(enabled_edition)
1306 });
1307
1308 #[expect(
1309 clippy::disallowed_methods,
1310 reason = "internal only, no reason for config support"
1311 )]
1312 let sysroot = env::var_os(SYSROOT_INTERNAL).map(PathBuf::from);
1313
1314 Ok(FixArgs {
1315 file,
1316 prepare_for_edition,
1317 idioms,
1318 enabled_edition,
1319 other,
1320 rustc,
1321 sysroot,
1322 })
1323 }
1324
1325 fn apply(&self, cmd: &mut ProcessBuilder) {
1326 cmd.arg(&self.file);
1327 cmd.args(&self.other);
1328 if self.prepare_for_edition.is_some() {
1329 cmd.arg("--cap-lints=allow");
1334 } else {
1335 cmd.arg("--cap-lints=warn");
1337 }
1338 if let Some(edition) = self.enabled_edition {
1339 cmd.arg("--edition").arg(edition.to_string());
1340 if self.idioms && edition.supports_idiom_lint() {
1341 cmd.arg(format!("-Wrust-{}-idioms", edition));
1342 }
1343 }
1344
1345 if let Some(edition) = self.prepare_for_edition {
1346 edition.force_warn_arg(cmd);
1347 }
1348 }
1349
1350 fn can_run_rustfix(&self, gctx: &GlobalContext) -> CargoResult<bool> {
1353 let Some(to_edition) = self.prepare_for_edition else {
1354 return Message::Fixing {
1355 file: self.file.display().to_string(),
1356 }
1357 .post(gctx)
1358 .and(Ok(true));
1359 };
1360 if !to_edition.is_stable() && !gctx.nightly_features_allowed {
1371 let message = format!(
1372 "`{file}` is on the latest edition, but trying to \
1373 migrate to edition {to_edition}.\n\
1374 Edition {to_edition} is unstable and not allowed in \
1375 this release, consider trying the nightly release channel.",
1376 file = self.file.display(),
1377 to_edition = to_edition
1378 );
1379 return Message::EditionAlreadyEnabled {
1380 message,
1381 edition: to_edition.previous().unwrap(),
1382 }
1383 .post(gctx)
1384 .and(Ok(false)); }
1386 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
1387 if from_edition == to_edition {
1388 let message = format!(
1389 "`{}` is already on the latest edition ({}), \
1390 unable to migrate further",
1391 self.file.display(),
1392 to_edition
1393 );
1394 Message::EditionAlreadyEnabled {
1395 message,
1396 edition: to_edition,
1397 }
1398 .post(gctx)
1399 } else {
1400 Message::Migrating {
1401 file: self.file.display().to_string(),
1402 from_edition,
1403 to_edition,
1404 }
1405 .post(gctx)
1406 }
1407 .and(Ok(true))
1408 }
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::FixArgs;
1414 use std::ffi::OsString;
1415 use std::io::Write as _;
1416 use std::path::PathBuf;
1417
1418 #[test]
1419 fn get_fix_args_from_argfile() {
1420 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1421 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1422
1423 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1424 temp.write_all(content.as_bytes()).unwrap();
1425
1426 let argfile = format!("@{}", temp.path().display());
1427 let args = ["cargo", &argfile];
1428 let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap();
1429 assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc"));
1430 assert_eq!(fix_args.file, main_rs.path());
1431 assert_eq!(fix_args.other, vec![OsString::from("foobar")]);
1432 }
1433
1434 #[test]
1435 fn get_fix_args_from_argfile_with_extra_arg() {
1436 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1437 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1438
1439 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1440 temp.write_all(content.as_bytes()).unwrap();
1441
1442 let argfile = format!("@{}", temp.path().display());
1443 let args = ["cargo", &argfile, "boo!"];
1444 match FixArgs::from_args(args.map(|x| x.into())) {
1445 Err(e) => assert_eq!(
1446 e.to_string(),
1447 "argfile `@path` cannot be combined with other arguments"
1448 ),
1449 Ok(_) => panic!("should fail"),
1450 }
1451 }
1452}