1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::env;
4use std::io::BufRead;
5use std::io::BufReader;
6use std::io::Cursor;
7use std::path::Path;
8use std::process::Command;
9use std::process::Stdio;
10
11use cargo_metadata::MetadataCommand;
12use cargo_util::paths;
13use clap::Parser;
14use indexmap::{IndexMap, IndexSet};
15use rustfix::{collect_suggestions, CodeFix, Suggestion};
16use tracing::{trace, warn};
17
18use crate::{
19 core::{shell, sysroot::get_sysroot},
20 ops::check::{BuildUnit, CheckOutput, DiagnosticLevel, Message, MessageDiagnostic},
21 util::{
22 cli::CheckFlags, messages::gen_please_report_this_bug_text, package::format_package_id,
23 vcs::VcsOpts,
24 },
25 CargoResult,
26};
27
28#[derive(Debug, Parser)]
29pub struct FixitArgs {
30 #[arg(long)]
32 clippy: bool,
33
34 #[arg(long)]
36 broken_code: bool,
37
38 #[arg(long = "Zdangerous-parallel-fixes")]
40 dangerous_parallel_fixes: bool,
41
42 #[command(flatten)]
43 color: colorchoice_clap::Color,
44
45 #[command(flatten)]
46 vcs_opts: VcsOpts,
47
48 #[command(flatten)]
49 check_flags: CheckFlags,
50}
51
52impl FixitArgs {
53 pub fn exec(self) -> CargoResult<()> {
54 exec(self)
55 }
56}
57
58#[derive(Debug, Default)]
59struct File {
60 fixes: u32,
61 original_source: String,
62}
63
64#[tracing::instrument(skip_all)]
65fn exec(args: FixitArgs) -> CargoResult<()> {
66 args.color.write_global();
67
68 args.vcs_opts.valid_vcs()?;
69
70 let mut active_targets = IndexMap::new();
71 match fix(&args, &mut active_targets) {
72 Ok(()) => Ok(()),
73 Err(error) => {
74 for (file, original) in active_targets.values().flat_map(|files| files.iter()) {
75 paths::write(file, &original.original_source)?;
76 }
77 Err(error)
78 }
79 }
80}
81
82fn fix(
83 args: &FixitArgs,
84 active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
85) -> CargoResult<()> {
86 let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
87 .ok()
88 .and_then(|i| i.parse().ok())
89 .unwrap_or(4);
90 let mut iteration = 0;
91 let mut lint_cap = false;
92
93 let mut last_errors = IndexMap::new();
94 let mut claimed_files: HashMap<same_file::Handle, BuildUnit> = HashMap::new();
95 let mut package_graph_cache: Option<Option<PackageGraph>> = None;
96 let mut seen = HashSet::new();
97
98 loop {
99 trace!("iteration={iteration}");
100 trace!("active_targets={active_targets:?}");
101 let (messages, exit_code) = check(args, &mut lint_cap)?;
102
103 if !args.broken_code && exit_code != Some(0) {
104 let mut out = String::new();
105
106 if !active_targets.is_empty() {
107 out.push_str(
108 "failed to automatically apply fixes suggested by rustc\n\n\
109 after fixes were automatically applied the \
110 compiler reported errors within these files:\n\n",
111 );
112
113 for (
114 file,
115 File {
116 fixes: _,
117 original_source,
118 },
119 ) in active_targets.values().flat_map(|files| files.iter())
120 {
121 out.push_str(&format!(" * {file}\n"));
122 shell::note(format!("reverting `{file}` to its original state"))?;
123 paths::write(file, original_source)?;
124 }
125 active_targets.clear();
126 out.push('\n');
127
128 out.push_str(&gen_please_report_this_bug_text(args.clippy));
129
130 let mut errors = messages
131 .into_iter()
132 .filter_map(|e| match e {
133 CheckOutput::Message(m) => m.message.diagnostic.rendered,
134 _ => None,
135 })
136 .peekable();
137 if errors.peek().is_some() {
138 out.push_str("The errors reported are:\n");
139 }
140
141 for e in errors {
142 out.push_str(&format!("{}\n\n", e.trim_end()));
143 }
144
145 let (messages, _) = check(args, &mut lint_cap)?;
146 let mut errors = messages
147 .into_iter()
148 .filter_map(|e| match e {
149 CheckOutput::Message(m) => m.message.diagnostic.rendered,
150 _ => None,
151 })
152 .peekable();
153
154 if errors.peek().is_some() {
155 out.push_str("The original errors are:\n");
156 }
157
158 for e in errors {
159 out.push_str(&format!("{}\n\n", e.trim_end()));
160 }
161
162 shell::warn(out)?;
163 } else {
164 for e in messages.into_iter().filter_map(|e| match e {
165 CheckOutput::Message(m) => m.message.diagnostic.rendered,
166 _ => None,
167 }) {
168 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
169 }
170 }
171
172 shell::note("try using `--broken-code` to fix errors")?;
173 anyhow::bail!("could not compile");
174 }
175
176 let (mut errors, mut build_unit_map) = collect_errors(messages.into_iter(), &seen);
177
178 if iteration >= max_iterations {
179 if active_targets.is_empty() {
180 break;
181 }
182 let targets: Vec<_> = active_targets.keys().cloned().collect();
183 for target in targets {
184 if let Some(file_map) = build_unit_map.get(&target) {
185 let target_errors = errors.entry(target.clone()).or_default();
186 target_errors.extend(
187 file_map
188 .values()
189 .flatten()
190 .filter_map(|(_, diagnostic)| diagnostic.clone()),
191 );
192 }
193 finish_target(target, active_targets, &mut errors, &mut seen)?;
194 }
195 claimed_files.clear();
196 iteration = 0;
197 }
198
199 let mut finalized_targets = false;
200 if !active_targets.is_empty()
201 && active_targets
202 .keys()
203 .all(|target| build_unit_map.get(target).is_none_or(IndexMap::is_empty))
204 {
205 let targets: Vec<_> = active_targets.keys().cloned().collect();
206 for target in targets {
207 build_unit_map.shift_remove(&target);
208 finish_target(target, active_targets, &mut errors, &mut seen)?;
209 }
210 debug_assert!(active_targets.is_empty());
211 claimed_files.clear();
212 iteration = 0;
213 finalized_targets = true;
214 }
215
216 let mut made_changes = false;
217 let continuing_batch = !active_targets.is_empty();
220
221 for (build_unit, file_map) in build_unit_map {
222 if seen.contains(&build_unit) {
223 continue;
224 }
225
226 let build_unit_errors = errors
227 .entry(build_unit.clone())
228 .or_insert_with(IndexSet::new);
229
230 if active_targets.is_empty() && file_map.is_empty() {
231 if finalized_targets && build_unit_errors.is_empty() {
232 continue;
233 }
234 if seen.iter().all(|b| b.package_id != build_unit.package_id) {
235 shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
236 }
237 for e in build_unit_errors.iter() {
238 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
239 }
240 errors.shift_remove(&build_unit);
241
242 seen.insert(build_unit);
243 } else if !file_map.is_empty() {
244 let was_active = active_targets.contains_key(&build_unit);
245 if continuing_batch && !was_active {
246 continue;
247 }
248
249 if !args.dangerous_parallel_fixes && !was_active && !active_targets.is_empty() {
250 if active_targets
251 .keys()
252 .any(|active| active.package_id == build_unit.package_id)
253 {
254 continue;
255 }
256
257 if package_graph_cache.is_none() {
258 package_graph_cache = Some(PackageGraph::load(&args.check_flags));
259 }
260 let Some(Some(graph)) = package_graph_cache.as_mut() else {
261 continue;
262 };
263
264 let mut independent = true;
265 for active in active_targets.keys() {
266 if !graph
267 .packages_are_independent(&active.package_id, &build_unit.package_id)
268 {
269 independent = false;
270 break;
271 }
272 }
273 if !independent {
274 continue;
275 }
276 }
277
278 let handles = file_map
279 .keys()
280 .map(same_file::Handle::from_path)
281 .collect::<Result<Vec<_>, _>>()
282 .ok();
283 let serialize_target = handles.is_none();
284 if serialize_target && !was_active && !active_targets.is_empty() {
285 continue;
286 }
287 if handles.as_ref().is_some_and(|handles| {
288 handles.iter().any(|handle| {
289 claimed_files
290 .get(handle)
291 .is_some_and(|owner| owner != &build_unit)
292 })
293 }) {
294 continue;
295 }
296
297 let target_files = active_targets.entry(build_unit.clone()).or_default();
298 let changed = fix_errors(target_files, file_map, build_unit_errors)?;
299 if !changed && !was_active {
300 active_targets.shift_remove(&build_unit);
301 }
302 if changed {
303 if let Some(handles) = handles {
304 for handle in handles {
305 claimed_files.entry(handle).or_insert(build_unit.clone());
306 }
307 }
308 made_changes = true;
309 if serialize_target {
310 break;
311 }
312 }
313 }
314 }
315
316 trace!("made_changes={made_changes:?}");
317 trace!("active_targets={active_targets:?}");
318
319 last_errors = errors;
320 iteration += 1;
321
322 if !made_changes {
323 if active_targets.is_empty() {
324 break;
325 }
326 let targets: Vec<_> = active_targets.keys().cloned().collect();
327 for target in targets {
328 finish_target(target, active_targets, &mut last_errors, &mut seen)?;
329 }
330 claimed_files.clear();
331 iteration = 0;
332 continue;
333 }
334 }
335
336 for files in active_targets.values() {
337 for (name, file) in files {
338 shell::fixed(name, file.fixes)?;
339 }
340 }
341
342 for e in last_errors.iter().flat_map(|(_, e)| e) {
343 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
344 }
345
346 active_targets.clear();
347 Ok(())
348}
349
350#[derive(Debug)]
352struct PackageGraph {
353 dependencies: HashMap<String, Vec<String>>,
354 reachable: HashMap<String, HashSet<String>>,
355}
356
357impl PackageGraph {
358 fn load(flags: &CheckFlags) -> Option<Self> {
360 let mut command = MetadataCommand::new();
361 command.no_deps();
362 command.other_options(flags.to_metadata_flags());
363
364 let metadata = match command.exec() {
365 Ok(metadata) => metadata,
366 Err(error) => {
367 warn!("failed to run `cargo metadata`: {error}");
368 return None;
369 }
370 };
371
372 if metadata
374 .packages
375 .iter()
376 .any(|package| package.manifest_path.file_name() != Some("Cargo.toml"))
377 {
378 return Self::load_resolved(flags);
379 }
380
381 let package_ids_by_path: HashMap<_, _> = metadata
382 .packages
383 .iter()
384 .filter_map(|package| {
385 package
386 .manifest_path
387 .parent()
388 .map(|path| (path, package.id.repr.as_str()))
389 })
390 .collect();
391 if package_ids_by_path.len() != metadata.packages.len() {
392 return Self::load_resolved(flags);
393 }
394
395 let package_names: HashSet<_> = metadata
396 .packages
397 .iter()
398 .map(|package| package.name.as_ref())
399 .collect();
400 let mut dependencies = HashMap::with_capacity(metadata.packages.len());
401 for package in &metadata.packages {
402 let mut package_dependencies = Vec::new();
403 for dependency in &package.dependencies {
404 if let Some(path) = &dependency.path {
405 let Some(dependency_id) = package_ids_by_path.get(path.as_path()) else {
406 return Self::load_resolved(flags);
407 };
408 package_dependencies.push((*dependency_id).to_owned());
409 } else if package_names.contains(dependency.name.as_str()) {
410 return Self::load_resolved(flags);
411 }
412 }
413 dependencies.insert(package.id.repr.clone(), package_dependencies);
414 }
415
416 Some(Self {
417 dependencies,
418 reachable: HashMap::new(),
419 })
420 }
421
422 fn load_resolved(flags: &CheckFlags) -> Option<Self> {
424 let mut command = MetadataCommand::new();
425 command.other_options(flags.to_metadata_flags());
426
427 let metadata = match command.exec() {
428 Ok(metadata) => metadata,
429 Err(error) => {
430 warn!("failed to run `cargo metadata`: {error}");
431 return None;
432 }
433 };
434 let Some(resolve) = metadata.resolve else {
435 warn!("`cargo metadata` did not return a dependency graph");
436 return None;
437 };
438 let dependencies = resolve
439 .nodes
440 .into_iter()
441 .map(|node| {
442 (
443 node.id.repr,
444 node.dependencies
445 .into_iter()
446 .map(|dependency| dependency.repr)
447 .collect(),
448 )
449 })
450 .collect();
451
452 Some(Self {
453 dependencies,
454 reachable: HashMap::new(),
455 })
456 }
457
458 fn packages_are_independent(&mut self, left: &str, right: &str) -> bool {
460 left != right && !self.depends_on(left, right) && !self.depends_on(right, left)
461 }
462
463 fn depends_on(&mut self, package: &str, target: &str) -> bool {
465 if !self.reachable.contains_key(package) {
466 let Some(reachable) = self.collect_reachable(package) else {
467 return true;
468 };
469 self.reachable.insert(package.to_owned(), reachable);
470 }
471
472 self.reachable
473 .get(package)
474 .is_none_or(|reachable| reachable.contains(target))
475 }
476
477 fn collect_reachable(&self, root: &str) -> Option<HashSet<String>> {
479 let mut reachable = HashSet::new();
480 let mut pending = vec![root];
481
482 while let Some(package) = pending.pop() {
483 if !reachable.insert(package.to_owned()) {
484 continue;
485 }
486 let dependencies = self.dependencies.get(package)?;
487 pending.extend(dependencies.iter().map(String::as_str));
488 }
489
490 reachable.remove(root);
491 Some(reachable)
492 }
493}
494
495fn finish_target(
497 target: BuildUnit,
498 active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
499 errors: &mut IndexMap<BuildUnit, IndexSet<String>>,
500 seen: &mut HashSet<BuildUnit>,
501) -> CargoResult<()> {
502 if seen
503 .iter()
504 .all(|build_unit| build_unit.package_id != target.package_id)
505 {
506 shell::status("Checking", format_package_id(&target.package_id)?)?;
507 }
508
509 if let Some(files) = active_targets.get(&target) {
510 for (name, file) in files {
511 shell::fixed(name, file.fixes)?;
512 }
513 }
514
515 for error in errors.get(&target).into_iter().flatten() {
516 shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
517 }
518
519 active_targets.shift_remove(&target);
520 errors.shift_remove(&target);
521 seen.insert(target);
522 Ok(())
523}
524
525fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
526 let cmd = if args.clippy { "clippy" } else { "check" };
527 let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
528 let mut command = Command::new(cargo);
529 command
530 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
531 .args(args.check_flags.to_flags())
532 .stderr(Stdio::piped())
533 .stdout(Stdio::piped());
534 if *lint_cap {
535 cap_lints(&mut command);
536 }
537 let output = command.output()?;
538 let mut output = to_check_output(output);
539
540 if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
541 *lint_cap = true;
542 cap_lints(&mut command);
543 output = to_check_output(command.output()?);
544 }
545
546 Ok(output)
547}
548
549fn cap_lints(command: &mut Command) {
551 if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
552 let separator = if flags.is_empty() { "" } else { "\u{1f}" };
553 command.env(
554 "CARGO_ENCODED_RUSTFLAGS",
555 format!("{flags}{separator}--cap-lints=warn"),
556 );
557 } else {
558 command.env(
559 "RUSTFLAGS",
560 format!(
561 "--cap-lints=warn {}",
562 env::var("RUSTFLAGS").unwrap_or("".to_owned())
563 ),
564 );
565 }
566}
567
568fn denied_lint(messages: &[CheckOutput]) -> bool {
569 messages.iter().any(|message| {
570 matches!(&message, CheckOutput::Message(message)
571 if message.message.level == DiagnosticLevel::Error
572 && message.message.diagnostic.code.is_some())
573 })
574}
575
576fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
577 let buf = BufReader::new(Cursor::new(output.stdout));
578 (
579 buf.lines()
580 .map_while(|l| l.ok())
581 .filter_map(|l| serde_json::from_str(&l).ok())
582 .collect(),
583 output.status.code(),
584 )
585}
586
587#[tracing::instrument(skip_all)]
588#[allow(clippy::type_complexity)]
589fn collect_errors(
590 messages: impl Iterator<Item = CheckOutput>,
591 seen: &HashSet<BuildUnit>,
592) -> (
593 IndexMap<BuildUnit, IndexSet<String>>,
594 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
595) {
596 let only = HashSet::new();
597 let mut build_unit_map = IndexMap::new();
598
599 let mut errors = IndexMap::new();
600
601 for message in messages {
602 let Message {
603 build_unit,
604 message: MessageDiagnostic { diagnostic, .. },
605 } = match message {
606 CheckOutput::Message(m) => m,
607 CheckOutput::Artifact(a) => {
608 if !seen.contains(&a.build_unit) && !a.fresh {
609 build_unit_map
610 .entry(a.build_unit.clone())
611 .or_insert(IndexMap::new());
612 }
613 continue;
614 }
615 };
616
617 let errors = errors
618 .entry(build_unit.clone())
619 .or_insert_with(IndexSet::new);
620
621 if seen.contains(&build_unit) {
622 trace!("rejecting build unit `{:?}` already seen", build_unit);
623 continue;
624 }
625
626 let file_map = build_unit_map
627 .entry(build_unit.clone())
628 .or_insert(IndexMap::new());
629
630 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
631 rustfix::Filter::Everything
632 } else {
633 rustfix::Filter::MachineApplicableOnly
634 };
635
636 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
637 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
638 if let Some(rendered) = diagnostic.rendered {
639 errors.insert(rendered);
640 }
641 continue;
642 };
643
644 let mut file_names = suggestion
645 .solutions
646 .iter()
647 .flat_map(|s| s.replacements.iter())
648 .map(|r| &r.snippet.file_name);
649
650 let Some(file_name) = file_names.next() else {
651 trace!("rejecting as it has no solutions {:?}", suggestion);
652 if let Some(rendered) = diagnostic.rendered {
653 errors.insert(rendered);
654 }
655 continue;
656 };
657
658 if !file_names.all(|f| f == file_name) {
659 trace!("rejecting as it changes multiple files: {:?}", suggestion);
660 if let Some(rendered) = diagnostic.rendered {
661 errors.insert(rendered);
662 }
663 continue;
664 }
665
666 let file_path = Path::new(&file_name);
667 if let Ok(home) = env::var("CARGO_HOME") {
669 if file_path.starts_with(home) {
670 if let Some(rendered) = diagnostic.rendered {
671 errors.insert(rendered);
672 }
673 continue;
674 }
675 }
676
677 if file_path.is_absolute() {
678 if let Some(sysroot) = get_sysroot() {
679 if file_path.starts_with(sysroot) {
680 if let Some(rendered) = diagnostic.rendered {
681 errors.insert(rendered);
682 }
683 continue;
684 }
685 }
686 }
687
688 file_map
689 .entry(file_name.to_owned())
690 .or_insert_with(IndexSet::new)
691 .insert((suggestion, diagnostic.rendered));
692 }
693
694 (errors, build_unit_map)
695}
696
697#[tracing::instrument(skip_all)]
698fn fix_errors(
699 files: &mut IndexMap<String, File>,
700 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
701 errors: &mut IndexSet<String>,
702) -> CargoResult<bool> {
703 let mut made_changes = false;
704 for (file, suggestions) in file_map {
705 let source = match paths::read(file.as_ref()) {
706 Ok(s) => s,
707 Err(e) => {
708 warn!("failed to read `{}`: {}", file, e);
709 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
710 continue;
711 }
712 };
713
714 let mut fixed = CodeFix::new(&source);
715 let mut num_fixes = 0;
716
717 for (suggestion, rendered) in suggestions.iter().rev() {
718 match fixed.apply(suggestion) {
719 Ok(()) => num_fixes += 1,
720 Err(rustfix::Error::AlreadyReplaced {
721 is_identical: true, ..
722 }) => {}
723 Err(e) => {
724 if let Some(rendered) = rendered {
725 errors.insert(rendered.to_owned());
726 }
727 warn!("{e:?}");
728 }
729 }
730 }
731 if fixed.modified() {
732 let new_source = fixed.finish()?;
733 let file_state = files.entry(file.clone()).or_insert(File {
734 fixes: 0,
735 original_source: source,
736 });
737 paths::write(&file, new_source)?;
738 made_changes = true;
739 file_state.fixes += num_fixes;
740 }
741 }
742
743 Ok(made_changes)
744}