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::{bail, Context as _};
47use cargo_util::{exit_status_to_string, is_simple_exit_code, paths, ProcessBuilder};
48use cargo_util_schemas::manifest::TomlManifest;
49use rustfix::diagnostics::Diagnostic;
50use rustfix::CodeFix;
51use semver::Version;
52use tracing::{debug, trace, warn};
53
54use crate::core::compiler::CompileKind;
55use crate::core::compiler::RustcTargetData;
56use crate::core::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor};
57use crate::core::resolver::{HasDevUnits, Resolve, ResolveBehavior};
58use crate::core::PackageIdSpecQuery as _;
59use crate::core::{Edition, MaybePackage, Package, PackageId, Workspace};
60use crate::ops::resolve::WorkspaceResolve;
61use crate::ops::{self, CompileOptions};
62use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
63use crate::util::errors::CargoResult;
64use crate::util::toml_mut::manifest::LocalManifest;
65use crate::util::GlobalContext;
66use crate::util::{existing_vcs_repo, LockServer, LockServerClient};
67use crate::{drop_eprint, drop_eprintln};
68
69const FIX_ENV_INTERNAL: &str = "__CARGO_FIX_PLZ";
74const BROKEN_CODE_ENV_INTERNAL: &str = "__CARGO_FIX_BROKEN_CODE";
77const EDITION_ENV_INTERNAL: &str = "__CARGO_FIX_EDITION";
80const IDIOMS_ENV_INTERNAL: &str = "__CARGO_FIX_IDIOMS";
83const SYSROOT_INTERNAL: &str = "__CARGO_FIX_RUST_SRC";
91
92pub struct FixOptions {
93 pub edition: bool,
94 pub idioms: bool,
95 pub compile_opts: CompileOptions,
96 pub allow_dirty: bool,
97 pub allow_no_vcs: bool,
98 pub allow_staged: bool,
99 pub broken_code: bool,
100 pub requested_lockfile_path: Option<PathBuf>,
101}
102
103pub fn fix(
104 gctx: &GlobalContext,
105 original_ws: &Workspace<'_>,
106 root_manifest: &Path,
107 opts: &mut FixOptions,
108) -> CargoResult<()> {
109 check_version_control(gctx, opts)?;
110
111 let mut target_data =
112 RustcTargetData::new(original_ws, &opts.compile_opts.build_config.requested_kinds)?;
113 if opts.edition {
114 let specs = opts.compile_opts.spec.to_package_id_specs(&original_ws)?;
115 let members: Vec<&Package> = original_ws
116 .members()
117 .filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
118 .collect();
119 migrate_manifests(original_ws, &members)?;
120
121 check_resolver_change(&original_ws, &mut target_data, opts)?;
122 }
123 let mut ws = Workspace::new(&root_manifest, gctx)?;
124 ws.set_resolve_honors_rust_version(Some(original_ws.resolve_honors_rust_version()));
125 ws.set_resolve_feature_unification(original_ws.resolve_feature_unification());
126 ws.set_requested_lockfile_path(opts.requested_lockfile_path.clone());
127
128 let lock_server = LockServer::new()?;
130 let mut wrapper = ProcessBuilder::new(env::current_exe()?);
131 wrapper.env(FIX_ENV_INTERNAL, lock_server.addr().to_string());
132 let _started = lock_server.start()?;
133
134 opts.compile_opts.build_config.force_rebuild = true;
135
136 if opts.broken_code {
137 wrapper.env(BROKEN_CODE_ENV_INTERNAL, "1");
138 }
139
140 if opts.edition {
141 wrapper.env(EDITION_ENV_INTERNAL, "1");
142 }
143 if opts.idioms {
144 wrapper.env(IDIOMS_ENV_INTERNAL, "1");
145 }
146
147 let sysroot = &target_data.info(CompileKind::Host).sysroot;
148 if sysroot.is_dir() {
149 wrapper.env(SYSROOT_INTERNAL, sysroot);
150 }
151
152 *opts
153 .compile_opts
154 .build_config
155 .rustfix_diagnostic_server
156 .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
157
158 if let Some(server) = opts
159 .compile_opts
160 .build_config
161 .rustfix_diagnostic_server
162 .borrow()
163 .as_ref()
164 {
165 server.configure(&mut wrapper);
166 }
167
168 let rustc = ws.gctx().load_global_rustc(Some(&ws))?;
169 wrapper.arg(&rustc.path);
170 wrapper.retry_with_argfile(true);
173
174 opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
177
178 ops::compile(&ws, &opts.compile_opts)?;
179 Ok(())
180}
181
182fn check_version_control(gctx: &GlobalContext, opts: &FixOptions) -> CargoResult<()> {
183 if opts.allow_no_vcs {
184 return Ok(());
185 }
186 if !existing_vcs_repo(gctx.cwd(), gctx.cwd()) {
187 bail!(
188 "no VCS found for this package and `cargo fix` can potentially \
189 perform destructive changes; if you'd like to suppress this \
190 error pass `--allow-no-vcs`"
191 )
192 }
193
194 if opts.allow_dirty && opts.allow_staged {
195 return Ok(());
196 }
197
198 let mut dirty_files = Vec::new();
199 let mut staged_files = Vec::new();
200 if let Ok(repo) = git2::Repository::discover(gctx.cwd()) {
201 let mut repo_opts = git2::StatusOptions::new();
202 repo_opts.include_ignored(false);
203 repo_opts.include_untracked(true);
204 for status in repo.statuses(Some(&mut repo_opts))?.iter() {
205 if let Some(path) = status.path() {
206 match status.status() {
207 git2::Status::CURRENT => (),
208 git2::Status::INDEX_NEW
209 | git2::Status::INDEX_MODIFIED
210 | git2::Status::INDEX_DELETED
211 | git2::Status::INDEX_RENAMED
212 | git2::Status::INDEX_TYPECHANGE => {
213 if !opts.allow_staged {
214 staged_files.push(path.to_string())
215 }
216 }
217 _ => {
218 if !opts.allow_dirty {
219 dirty_files.push(path.to_string())
220 }
221 }
222 };
223 }
224 }
225 }
226
227 if dirty_files.is_empty() && staged_files.is_empty() {
228 return Ok(());
229 }
230
231 let mut files_list = String::new();
232 for file in dirty_files {
233 files_list.push_str(" * ");
234 files_list.push_str(&file);
235 files_list.push_str(" (dirty)\n");
236 }
237 for file in staged_files {
238 files_list.push_str(" * ");
239 files_list.push_str(&file);
240 files_list.push_str(" (staged)\n");
241 }
242
243 bail!(
244 "the working directory of this package has uncommitted changes, and \
245 `cargo fix` can potentially perform destructive changes; if you'd \
246 like to suppress this error pass `--allow-dirty`, \
247 or commit the changes to these files:\n\
248 \n\
249 {}\n\
250 ",
251 files_list
252 );
253}
254
255fn migrate_manifests(ws: &Workspace<'_>, pkgs: &[&Package]) -> CargoResult<()> {
256 if matches!(ws.root_maybe(), MaybePackage::Virtual(_)) {
259 let highest_edition = pkgs
262 .iter()
263 .map(|p| p.manifest().edition())
264 .max()
265 .unwrap_or_default();
266 let prepare_for_edition = highest_edition.saturating_next();
267 if highest_edition == prepare_for_edition
268 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
269 {
270 } else {
272 let mut manifest_mut = LocalManifest::try_new(ws.root_manifest())?;
273 let document = &mut manifest_mut.data;
274 let mut fixes = 0;
275
276 if Edition::Edition2024 <= prepare_for_edition {
277 let root = document.as_table_mut();
278
279 if let Some(workspace) = root
280 .get_mut("workspace")
281 .and_then(|t| t.as_table_like_mut())
282 {
283 fixes += rename_dep_fields_2024(workspace, "dependencies");
286 }
287 }
288
289 if 0 < fixes {
290 let file = ws.root_manifest();
293 let file = file.strip_prefix(ws.root()).unwrap_or(file);
294 let file = file.display();
295 ws.gctx().shell().status(
296 "Migrating",
297 format!("{file} from {highest_edition} edition to {prepare_for_edition}"),
298 )?;
299
300 let verb = if fixes == 1 { "fix" } else { "fixes" };
301 let msg = format!("{file} ({fixes} {verb})");
302 ws.gctx().shell().status("Fixed", msg)?;
303
304 manifest_mut.write()?;
305 }
306 }
307 }
308
309 for pkg in pkgs {
310 let existing_edition = pkg.manifest().edition();
311 let prepare_for_edition = existing_edition.saturating_next();
312 if existing_edition == prepare_for_edition
313 || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed)
314 {
315 continue;
316 }
317 let file = pkg.manifest_path();
318 let file = file.strip_prefix(ws.root()).unwrap_or(file);
319 let file = file.display();
320 ws.gctx().shell().status(
321 "Migrating",
322 format!("{file} from {existing_edition} edition to {prepare_for_edition}"),
323 )?;
324
325 let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?;
326 let document = &mut manifest_mut.data;
327 let mut fixes = 0;
328
329 let ws_original_toml = match ws.root_maybe() {
330 MaybePackage::Package(package) => package.manifest().original_toml(),
331 MaybePackage::Virtual(manifest) => manifest.original_toml(),
332 };
333 if Edition::Edition2024 <= prepare_for_edition {
334 let root = document.as_table_mut();
335
336 if let Some(workspace) = root
337 .get_mut("workspace")
338 .and_then(|t| t.as_table_like_mut())
339 {
340 fixes += rename_dep_fields_2024(workspace, "dependencies");
343 }
344
345 fixes += rename_table(root, "project", "package");
346 if let Some(target) = root.get_mut("lib").and_then(|t| t.as_table_like_mut()) {
347 fixes += rename_target_fields_2024(target);
348 }
349 fixes += rename_array_of_target_fields_2024(root, "bin");
350 fixes += rename_array_of_target_fields_2024(root, "example");
351 fixes += rename_array_of_target_fields_2024(root, "test");
352 fixes += rename_array_of_target_fields_2024(root, "bench");
353 fixes += rename_dep_fields_2024(root, "dependencies");
354 fixes += remove_ignored_default_features_2024(root, "dependencies", ws_original_toml);
355 fixes += rename_table(root, "dev_dependencies", "dev-dependencies");
356 fixes += rename_dep_fields_2024(root, "dev-dependencies");
357 fixes +=
358 remove_ignored_default_features_2024(root, "dev-dependencies", ws_original_toml);
359 fixes += rename_table(root, "build_dependencies", "build-dependencies");
360 fixes += rename_dep_fields_2024(root, "build-dependencies");
361 fixes +=
362 remove_ignored_default_features_2024(root, "build-dependencies", ws_original_toml);
363 for target in root
364 .get_mut("target")
365 .and_then(|t| t.as_table_like_mut())
366 .iter_mut()
367 .flat_map(|t| t.iter_mut())
368 .filter_map(|(_k, t)| t.as_table_like_mut())
369 {
370 fixes += rename_dep_fields_2024(target, "dependencies");
371 fixes +=
372 remove_ignored_default_features_2024(target, "dependencies", ws_original_toml);
373 fixes += rename_table(target, "dev_dependencies", "dev-dependencies");
374 fixes += rename_dep_fields_2024(target, "dev-dependencies");
375 fixes += remove_ignored_default_features_2024(
376 target,
377 "dev-dependencies",
378 ws_original_toml,
379 );
380 fixes += rename_table(target, "build_dependencies", "build-dependencies");
381 fixes += rename_dep_fields_2024(target, "build-dependencies");
382 fixes += remove_ignored_default_features_2024(
383 target,
384 "build-dependencies",
385 ws_original_toml,
386 );
387 }
388 }
389
390 if 0 < fixes {
391 let verb = if fixes == 1 { "fix" } else { "fixes" };
392 let msg = format!("{file} ({fixes} {verb})");
393 ws.gctx().shell().status("Fixed", msg)?;
394
395 manifest_mut.write()?;
396 }
397 }
398
399 Ok(())
400}
401
402fn rename_dep_fields_2024(parent: &mut dyn toml_edit::TableLike, dep_kind: &str) -> usize {
403 let mut fixes = 0;
404 for target in parent
405 .get_mut(dep_kind)
406 .and_then(|t| t.as_table_like_mut())
407 .iter_mut()
408 .flat_map(|t| t.iter_mut())
409 .filter_map(|(_k, t)| t.as_table_like_mut())
410 {
411 fixes += rename_table(target, "default_features", "default-features");
412 }
413 fixes
414}
415
416fn remove_ignored_default_features_2024(
417 parent: &mut dyn toml_edit::TableLike,
418 dep_kind: &str,
419 ws_original_toml: &TomlManifest,
420) -> usize {
421 let mut fixes = 0;
422 for (name_in_toml, target) in parent
423 .get_mut(dep_kind)
424 .and_then(|t| t.as_table_like_mut())
425 .iter_mut()
426 .flat_map(|t| t.iter_mut())
427 .filter_map(|(k, t)| t.as_table_like_mut().map(|t| (k, t)))
428 {
429 let name_in_toml: &str = &name_in_toml;
430 let ws_deps = ws_original_toml
431 .workspace
432 .as_ref()
433 .and_then(|ws| ws.dependencies.as_ref());
434 if let Some(ws_dep) = ws_deps.and_then(|ws_deps| ws_deps.get(name_in_toml)) {
435 if ws_dep.default_features() == Some(false) {
436 continue;
437 }
438 }
439 if target
440 .get("workspace")
441 .and_then(|i| i.as_value())
442 .and_then(|i| i.as_bool())
443 == Some(true)
444 && target
445 .get("default-features")
446 .and_then(|i| i.as_value())
447 .and_then(|i| i.as_bool())
448 == Some(false)
449 {
450 target.remove("default-features");
451 fixes += 1;
452 }
453 }
454 fixes
455}
456
457fn rename_array_of_target_fields_2024(root: &mut dyn toml_edit::TableLike, kind: &str) -> usize {
458 let mut fixes = 0;
459 for target in root
460 .get_mut(kind)
461 .and_then(|t| t.as_array_of_tables_mut())
462 .iter_mut()
463 .flat_map(|t| t.iter_mut())
464 {
465 fixes += rename_target_fields_2024(target);
466 }
467 fixes
468}
469
470fn rename_target_fields_2024(target: &mut dyn toml_edit::TableLike) -> usize {
471 let mut fixes = 0;
472 fixes += rename_table(target, "crate_type", "crate-type");
473 fixes += rename_table(target, "proc_macro", "proc-macro");
474 fixes
475}
476
477fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> usize {
478 let Some(old_key) = parent.key(old).cloned() else {
479 return 0;
480 };
481
482 let project = parent.remove(old).expect("returned early");
483 if !parent.contains_key(new) {
484 parent.insert(new, project);
485 let mut new_key = parent.key_mut(new).expect("just inserted");
486 *new_key.dotted_decor_mut() = old_key.dotted_decor().clone();
487 *new_key.leaf_decor_mut() = old_key.leaf_decor().clone();
488 }
489 1
490}
491
492fn check_resolver_change<'gctx>(
493 ws: &Workspace<'gctx>,
494 target_data: &mut RustcTargetData<'gctx>,
495 opts: &FixOptions,
496) -> CargoResult<()> {
497 let root = ws.root_maybe();
498 match root {
499 MaybePackage::Package(root_pkg) => {
500 if root_pkg.manifest().resolve_behavior().is_some() {
501 return Ok(());
503 }
504 let pkgs = opts.compile_opts.spec.get_packages(ws)?;
506 if !pkgs.iter().any(|&pkg| pkg == root_pkg) {
507 return Ok(());
509 }
510 if root_pkg.manifest().edition() != Edition::Edition2018 {
511 return Ok(());
513 }
514 }
515 MaybePackage::Virtual(_vm) => {
516 return Ok(());
518 }
519 }
520 assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
522 let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
523 let mut resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> {
524 let dry_run = false;
525 let ws_resolve = ops::resolve_ws_with_opts(
526 ws,
527 target_data,
528 &opts.compile_opts.build_config.requested_kinds,
529 &opts.compile_opts.cli_features,
530 &specs,
531 has_dev_units,
532 crate::core::resolver::features::ForceAllTargets::No,
533 dry_run,
534 )?;
535
536 let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units);
537 let v2_features = FeatureResolver::resolve(
538 ws,
539 target_data,
540 &ws_resolve.targeted_resolve,
541 &ws_resolve.pkg_set,
542 &opts.compile_opts.cli_features,
543 &specs,
544 &opts.compile_opts.build_config.requested_kinds,
545 feature_opts,
546 )?;
547
548 let diffs = v2_features.compare_legacy(&ws_resolve.resolved_features);
549 Ok((ws_resolve, diffs))
550 };
551 let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?;
552 let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?;
553 if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() {
554 return Ok(());
556 }
557 with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals));
559 let gctx = ws.gctx();
560 gctx.shell().note(
561 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
562 )?;
563 drop_eprintln!(
564 gctx,
565 "This may cause some dependencies to be built with fewer features enabled than previously."
566 );
567 drop_eprintln!(
568 gctx,
569 "More information about the resolver changes may be found \
570 at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
571 );
572 drop_eprintln!(
573 gctx,
574 "When building the following dependencies, \
575 the given features will no longer be used:\n"
576 );
577 let show_diffs = |differences: DiffMap| {
578 for ((pkg_id, features_for), removed) in differences {
579 drop_eprint!(gctx, " {}", pkg_id);
580 if let FeaturesFor::HostDep = features_for {
581 drop_eprint!(gctx, " (as host dependency)");
582 }
583 drop_eprint!(gctx, " removed features: ");
584 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
585 drop_eprintln!(gctx, "{}", joined.join(", "));
586 }
587 drop_eprint!(gctx, "\n");
588 };
589 if !without_dev_diffs.is_empty() {
590 show_diffs(without_dev_diffs);
591 }
592 if !with_dev_diffs.is_empty() {
593 drop_eprintln!(
594 gctx,
595 "The following differences only apply when building with dev-dependencies:\n"
596 );
597 show_diffs(with_dev_diffs);
598 }
599 report_maybe_diesel(gctx, &ws_resolve.targeted_resolve)?;
600 Ok(())
601}
602
603fn report_maybe_diesel(gctx: &GlobalContext, resolve: &Resolve) -> CargoResult<()> {
604 fn is_broken_diesel(pid: PackageId) -> bool {
605 pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8)
606 }
607
608 fn is_broken_diesel_migration(pid: PackageId) -> bool {
609 pid.name() == "diesel_migrations" && pid.version().major <= 1
610 }
611
612 if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) {
613 gctx.shell().note(
614 "\
615This project appears to use both diesel and diesel_migrations. These packages have
616a known issue where the build may fail due to the version 2 resolver preventing
617feature unification between those two packages. Please update to at least diesel 1.4.8
618to prevent this issue from happening.
619",
620 )?;
621 }
622 Ok(())
623}
624
625pub fn fix_get_proxy_lock_addr() -> Option<String> {
630 #[allow(clippy::disallowed_methods)]
633 env::var(FIX_ENV_INTERNAL).ok()
634}
635
636pub fn fix_exec_rustc(gctx: &GlobalContext, lock_addr: &str) -> CargoResult<()> {
645 let args = FixArgs::get()?;
646 trace!("cargo-fix as rustc got file {:?}", args.file);
647
648 let workspace_rustc = gctx
649 .get_env("RUSTC_WORKSPACE_WRAPPER")
650 .map(PathBuf::from)
651 .ok();
652 let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
653 rustc.retry_with_argfile(true);
654 rustc.env_remove(FIX_ENV_INTERNAL);
655 args.apply(&mut rustc);
656 if let Some(client) = gctx.jobserver_from_env() {
659 rustc.inherit_jobserver(client);
660 }
661
662 trace!("start rustfixing {:?}", args.file);
663 let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, gctx)?;
664
665 if fixes.last_output.status.success() {
666 for (path, file) in fixes.files.iter() {
667 Message::Fixed {
668 file: path.clone(),
669 fixes: file.fixes_applied,
670 }
671 .post(gctx)?;
672 }
673 emit_output(&fixes.last_output)?;
675 return Ok(());
676 }
677
678 let allow_broken_code = gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some();
679
680 if !allow_broken_code {
684 for (path, file) in fixes.files.iter() {
685 debug!("reverting {:?} due to errors", path);
686 paths::write(path, &file.original_code)?;
687 }
688 }
689
690 if fixes.files.is_empty() {
696 emit_output(&fixes.last_output)?;
698 exit_with(fixes.last_output.status);
699 } else {
700 let krate = {
701 let mut iter = rustc.get_args();
702 let mut krate = None;
703 while let Some(arg) = iter.next() {
704 if arg == "--crate-name" {
705 krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
706 }
707 }
708 krate
709 };
710 log_failed_fix(
711 gctx,
712 krate,
713 &fixes.last_output.stderr,
714 fixes.last_output.status,
715 )?;
716 emit_output(&fixes.first_output)?;
720 exit_with(fixes.first_output.status);
724 }
725}
726
727fn emit_output(output: &Output) -> CargoResult<()> {
728 std::io::stderr().write_all(&output.stderr)?;
732 std::io::stdout().write_all(&output.stdout)?;
733 Ok(())
734}
735
736struct FixedCrate {
737 files: HashMap<String, FixedFile>,
739 first_output: Output,
745 last_output: Output,
750}
751
752#[derive(Debug)]
753struct FixedFile {
754 errors_applying_fixes: Vec<String>,
755 fixes_applied: u32,
756 original_code: String,
757}
758
759fn rustfix_crate(
764 lock_addr: &str,
765 rustc: &ProcessBuilder,
766 filename: &Path,
767 args: &FixArgs,
768 gctx: &GlobalContext,
769) -> CargoResult<FixedCrate> {
770 let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
780
781 let mut files = HashMap::new();
783
784 if !args.can_run_rustfix(gctx)? {
785 debug!("can't fix {filename:?}, running rustc: {rustc}");
789 let last_output = rustc.output()?;
790 let fixes = FixedCrate {
791 files,
792 first_output: last_output.clone(),
793 last_output,
794 };
795 return Ok(fixes);
796 }
797
798 let max_iterations = gctx
832 .get_env("CARGO_FIX_MAX_RETRIES")
833 .ok()
834 .and_then(|n| n.parse().ok())
835 .unwrap_or(4);
836 let mut last_output;
837 let mut last_made_changes;
838 let mut first_output = None;
839 let mut current_iteration = 0;
840 loop {
841 for file in files.values_mut() {
842 file.errors_applying_fixes.clear();
844 }
845 (last_output, last_made_changes) =
846 rustfix_and_fix(&mut files, rustc, filename, args, gctx)?;
847 if current_iteration == 0 {
848 first_output = Some(last_output.clone());
849 }
850 let mut progress_yet_to_be_made = false;
851 for (path, file) in files.iter_mut() {
852 if file.errors_applying_fixes.is_empty() {
853 continue;
854 }
855 debug!("had rustfix apply errors in {path:?} {file:?}");
856 if last_made_changes {
860 progress_yet_to_be_made = true;
861 }
862 }
863 if !progress_yet_to_be_made {
864 break;
865 }
866 current_iteration += 1;
867 if current_iteration >= max_iterations {
868 break;
869 }
870 }
871 if last_made_changes {
872 debug!("calling rustc one last time for final results: {rustc}");
873 last_output = rustc.output()?;
874 }
875
876 for (path, file) in files.iter_mut() {
879 for error in file.errors_applying_fixes.drain(..) {
880 Message::ReplaceFailed {
881 file: path.clone(),
882 message: error,
883 }
884 .post(gctx)?;
885 }
886 }
887
888 Ok(FixedCrate {
889 files,
890 first_output: first_output.expect("at least one iteration"),
891 last_output,
892 })
893}
894
895fn rustfix_and_fix(
900 files: &mut HashMap<String, FixedFile>,
901 rustc: &ProcessBuilder,
902 filename: &Path,
903 args: &FixArgs,
904 gctx: &GlobalContext,
905) -> CargoResult<(Output, bool)> {
906 let only = HashSet::new();
909
910 debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
911 let output = rustc.output()?;
912
913 if !output.status.success() && gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() {
919 debug!(
920 "rustfixing `{:?}` failed, rustc exited with {:?}",
921 filename,
922 output.status.code()
923 );
924 return Ok((output, false));
925 }
926
927 let fix_mode = gctx
928 .get_env_os("__CARGO_FIX_YOLO")
929 .map(|_| rustfix::Filter::Everything)
930 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
931
932 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
935
936 let suggestions = stderr
937 .lines()
938 .filter(|x| !x.is_empty())
939 .inspect(|y| trace!("line: {}", y))
940 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
942 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
944
945 let mut file_map = HashMap::new();
947 let mut num_suggestion = 0;
948 let home_path = gctx.home().as_path_unlocked();
950 for suggestion in suggestions {
951 trace!("suggestion");
952 let file_names = suggestion
956 .solutions
957 .iter()
958 .flat_map(|s| s.replacements.iter())
959 .map(|r| &r.snippet.file_name);
960
961 let file_name = if let Some(file_name) = file_names.clone().next() {
962 file_name.clone()
963 } else {
964 trace!("rejecting as it has no solutions {:?}", suggestion);
965 continue;
966 };
967
968 let file_path = Path::new(&file_name);
969 if file_path.starts_with(home_path) {
971 continue;
972 }
973 if let Some(sysroot) = args.sysroot.as_deref() {
975 if file_path.starts_with(sysroot) {
976 continue;
977 }
978 }
979
980 if !file_names.clone().all(|f| f == &file_name) {
981 trace!("rejecting as it changes multiple files: {:?}", suggestion);
982 continue;
983 }
984
985 trace!("adding suggestion for {:?}: {:?}", file_name, suggestion);
986 file_map
987 .entry(file_name)
988 .or_insert_with(Vec::new)
989 .push(suggestion);
990 num_suggestion += 1;
991 }
992
993 debug!(
994 "collected {} suggestions for `{}`",
995 num_suggestion,
996 filename.display(),
997 );
998
999 let mut made_changes = false;
1000 for (file, suggestions) in file_map {
1001 let code = match paths::read(file.as_ref()) {
1005 Ok(s) => s,
1006 Err(e) => {
1007 warn!("failed to read `{}`: {}", file, e);
1008 continue;
1009 }
1010 };
1011 let num_suggestions = suggestions.len();
1012 debug!("applying {} fixes to {}", num_suggestions, file);
1013
1014 let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile {
1019 errors_applying_fixes: Vec::new(),
1020 fixes_applied: 0,
1021 original_code: code.clone(),
1022 });
1023 let mut fixed = CodeFix::new(&code);
1024
1025 for suggestion in suggestions.iter().rev() {
1026 match fixed.apply(suggestion) {
1034 Ok(()) => fixed_file.fixes_applied += 1,
1035 Err(rustfix::Error::AlreadyReplaced {
1036 is_identical: true, ..
1037 }) => continue,
1038 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
1039 }
1040 }
1041 if fixed.modified() {
1042 made_changes = true;
1043 let new_code = fixed.finish()?;
1044 paths::write(&file, new_code)?;
1045 }
1046 }
1047
1048 Ok((output, made_changes))
1049}
1050
1051fn exit_with(status: ExitStatus) -> ! {
1052 #[cfg(unix)]
1053 {
1054 use std::os::unix::prelude::*;
1055 if let Some(signal) = status.signal() {
1056 drop(writeln!(
1057 std::io::stderr().lock(),
1058 "child failed with signal `{}`",
1059 signal
1060 ));
1061 process::exit(2);
1062 }
1063 }
1064 process::exit(status.code().unwrap_or(3));
1065}
1066
1067fn log_failed_fix(
1068 gctx: &GlobalContext,
1069 krate: Option<String>,
1070 stderr: &[u8],
1071 status: ExitStatus,
1072) -> CargoResult<()> {
1073 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
1074
1075 let diagnostics = stderr
1076 .lines()
1077 .filter(|x| !x.is_empty())
1078 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
1079 let mut files = BTreeSet::new();
1080 let mut errors = Vec::new();
1081 for diagnostic in diagnostics {
1082 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
1083 for span in diagnostic.spans.into_iter() {
1084 files.insert(span.file_name);
1085 }
1086 }
1087 errors.extend(
1089 stderr
1090 .lines()
1091 .filter(|x| !x.starts_with('{'))
1092 .map(|x| x.to_string()),
1093 );
1094
1095 let files = files.into_iter().collect();
1096 let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) {
1097 None
1098 } else {
1099 Some(exit_status_to_string(status))
1100 };
1101 Message::FixFailed {
1102 files,
1103 krate,
1104 errors,
1105 abnormal_exit,
1106 }
1107 .post(gctx)?;
1108
1109 Ok(())
1110}
1111
1112struct FixArgs {
1115 file: PathBuf,
1117 prepare_for_edition: Option<Edition>,
1120 idioms: bool,
1122 enabled_edition: Option<Edition>,
1126 other: Vec<OsString>,
1129 rustc: PathBuf,
1131 sysroot: Option<PathBuf>,
1133}
1134
1135impl FixArgs {
1136 fn get() -> CargoResult<FixArgs> {
1137 Self::from_args(env::args_os())
1138 }
1139
1140 fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> {
1142 let mut argv = argv.into_iter();
1143 let mut rustc = argv
1144 .nth(1)
1145 .map(PathBuf::from)
1146 .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?;
1147 let mut file = None;
1148 let mut enabled_edition = None;
1149 let mut other = Vec::new();
1150
1151 let mut handle_arg = |arg: OsString| -> CargoResult<()> {
1152 let path = PathBuf::from(arg);
1153 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
1154 file = Some(path);
1155 return Ok(());
1156 }
1157 if let Some(s) = path.to_str() {
1158 if let Some(edition) = s.strip_prefix("--edition=") {
1159 enabled_edition = Some(edition.parse()?);
1160 return Ok(());
1161 }
1162 }
1163 other.push(path.into());
1164 Ok(())
1165 };
1166
1167 if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") {
1168 if argv.next().is_some() {
1171 bail!("argfile `@path` cannot be combined with other arguments");
1172 }
1173 let contents = fs::read_to_string(argfile_path)
1174 .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?;
1175 let mut iter = contents.lines().map(OsString::from);
1176 rustc = iter
1177 .next()
1178 .map(PathBuf::from)
1179 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
1180 for arg in iter {
1181 handle_arg(arg)?;
1182 }
1183 } else {
1184 for arg in argv {
1185 handle_arg(arg)?;
1186 }
1187 }
1188
1189 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
1190 #[allow(clippy::disallowed_methods)]
1193 let idioms = env::var(IDIOMS_ENV_INTERNAL).is_ok();
1194
1195 #[allow(clippy::disallowed_methods)]
1198 let prepare_for_edition = env::var(EDITION_ENV_INTERNAL).ok().map(|_| {
1199 enabled_edition
1200 .unwrap_or(Edition::Edition2015)
1201 .saturating_next()
1202 });
1203
1204 #[allow(clippy::disallowed_methods)]
1207 let sysroot = env::var_os(SYSROOT_INTERNAL).map(PathBuf::from);
1208
1209 Ok(FixArgs {
1210 file,
1211 prepare_for_edition,
1212 idioms,
1213 enabled_edition,
1214 other,
1215 rustc,
1216 sysroot,
1217 })
1218 }
1219
1220 fn apply(&self, cmd: &mut ProcessBuilder) {
1221 cmd.arg(&self.file);
1222 cmd.args(&self.other);
1223 if self.prepare_for_edition.is_some() {
1224 cmd.arg("--cap-lints=allow");
1229 } else {
1230 cmd.arg("--cap-lints=warn");
1232 }
1233 if let Some(edition) = self.enabled_edition {
1234 cmd.arg("--edition").arg(edition.to_string());
1235 if self.idioms && edition.supports_idiom_lint() {
1236 cmd.arg(format!("-Wrust-{}-idioms", edition));
1237 }
1238 }
1239
1240 if let Some(edition) = self.prepare_for_edition {
1241 if edition.supports_compat_lint() {
1242 cmd.arg("--force-warn")
1243 .arg(format!("rust-{}-compatibility", edition));
1244 }
1245 }
1246 }
1247
1248 fn can_run_rustfix(&self, gctx: &GlobalContext) -> CargoResult<bool> {
1251 let Some(to_edition) = self.prepare_for_edition else {
1252 return Message::Fixing {
1253 file: self.file.display().to_string(),
1254 }
1255 .post(gctx)
1256 .and(Ok(true));
1257 };
1258 if !to_edition.is_stable() && !gctx.nightly_features_allowed {
1269 let message = format!(
1270 "`{file}` is on the latest edition, but trying to \
1271 migrate to edition {to_edition}.\n\
1272 Edition {to_edition} is unstable and not allowed in \
1273 this release, consider trying the nightly release channel.",
1274 file = self.file.display(),
1275 to_edition = to_edition
1276 );
1277 return Message::EditionAlreadyEnabled {
1278 message,
1279 edition: to_edition.previous().unwrap(),
1280 }
1281 .post(gctx)
1282 .and(Ok(false)); }
1284 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
1285 if from_edition == to_edition {
1286 let message = format!(
1287 "`{}` is already on the latest edition ({}), \
1288 unable to migrate further",
1289 self.file.display(),
1290 to_edition
1291 );
1292 Message::EditionAlreadyEnabled {
1293 message,
1294 edition: to_edition,
1295 }
1296 .post(gctx)
1297 } else {
1298 Message::Migrating {
1299 file: self.file.display().to_string(),
1300 from_edition,
1301 to_edition,
1302 }
1303 .post(gctx)
1304 }
1305 .and(Ok(true))
1306 }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311 use super::FixArgs;
1312 use std::ffi::OsString;
1313 use std::io::Write as _;
1314 use std::path::PathBuf;
1315
1316 #[test]
1317 fn get_fix_args_from_argfile() {
1318 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1319 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1320
1321 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1322 temp.write_all(content.as_bytes()).unwrap();
1323
1324 let argfile = format!("@{}", temp.path().display());
1325 let args = ["cargo", &argfile];
1326 let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap();
1327 assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc"));
1328 assert_eq!(fix_args.file, main_rs.path());
1329 assert_eq!(fix_args.other, vec![OsString::from("foobar")]);
1330 }
1331
1332 #[test]
1333 fn get_fix_args_from_argfile_with_extra_arg() {
1334 let mut temp = tempfile::Builder::new().tempfile().unwrap();
1335 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
1336
1337 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
1338 temp.write_all(content.as_bytes()).unwrap();
1339
1340 let argfile = format!("@{}", temp.path().display());
1341 let args = ["cargo", &argfile, "boo!"];
1342 match FixArgs::from_args(args.map(|x| x.into())) {
1343 Err(e) => assert_eq!(
1344 e.to_string(),
1345 "argfile `@path` cannot be combined with other arguments"
1346 ),
1347 Ok(_) => panic!("should fail"),
1348 }
1349 }
1350}