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 = if args.dangerous_parallel_fixes {
96 None
97 } else {
98 PackageGraph::load(&args.check_flags)
99 };
100 let mut seen = HashSet::new();
101
102 loop {
103 trace!("iteration={iteration}");
104 trace!("active_targets={active_targets:?}");
105 let (messages, exit_code) = check(args, &mut lint_cap)?;
106
107 if !args.broken_code && exit_code != Some(0) {
108 let mut out = String::new();
109
110 if !active_targets.is_empty() {
111 out.push_str(
112 "failed to automatically apply fixes suggested by rustc\n\n\
113 after fixes were automatically applied the \
114 compiler reported errors within these files:\n\n",
115 );
116
117 for (
118 file,
119 File {
120 fixes: _,
121 original_source,
122 },
123 ) in active_targets.values().flat_map(|files| files.iter())
124 {
125 out.push_str(&format!(" * {file}\n"));
126 shell::note(format!("reverting `{file}` to its original state"))?;
127 paths::write(file, original_source)?;
128 }
129 active_targets.clear();
130 out.push('\n');
131
132 out.push_str(&gen_please_report_this_bug_text(args.clippy));
133
134 let mut errors = messages
135 .into_iter()
136 .filter_map(|e| match e {
137 CheckOutput::Message(m) => m.message.diagnostic.rendered,
138 _ => None,
139 })
140 .peekable();
141 if errors.peek().is_some() {
142 out.push_str("The errors reported are:\n");
143 }
144
145 for e in errors {
146 out.push_str(&format!("{}\n\n", e.trim_end()));
147 }
148
149 let (messages, _) = check(args, &mut lint_cap)?;
150 let mut errors = messages
151 .into_iter()
152 .filter_map(|e| match e {
153 CheckOutput::Message(m) => m.message.diagnostic.rendered,
154 _ => None,
155 })
156 .peekable();
157
158 if errors.peek().is_some() {
159 out.push_str("The original errors are:\n");
160 }
161
162 for e in errors {
163 out.push_str(&format!("{}\n\n", e.trim_end()));
164 }
165
166 shell::warn(out)?;
167 } else {
168 for e in messages.into_iter().filter_map(|e| match e {
169 CheckOutput::Message(m) => m.message.diagnostic.rendered,
170 _ => None,
171 }) {
172 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
173 }
174 }
175
176 shell::note("try using `--broken-code` to fix errors")?;
177 anyhow::bail!("could not compile");
178 }
179
180 let (mut errors, mut build_unit_map) = collect_errors(messages.into_iter(), &seen);
181
182 if iteration >= max_iterations {
183 if active_targets.is_empty() {
184 break;
185 }
186 let targets: Vec<_> = active_targets.keys().cloned().collect();
187 for target in targets {
188 if let Some(file_map) = build_unit_map.get(&target) {
189 let target_errors = errors.entry(target.clone()).or_default();
190 target_errors.extend(
191 file_map
192 .values()
193 .flatten()
194 .filter_map(|(_, diagnostic)| diagnostic.clone()),
195 );
196 }
197 finish_target(target, active_targets, &mut errors, &mut seen)?;
198 }
199 claimed_files.clear();
200 iteration = 0;
201 }
202
203 let mut finalized_targets = false;
204 if !active_targets.is_empty()
205 && active_targets
206 .keys()
207 .all(|target| build_unit_map.get(target).is_none_or(IndexMap::is_empty))
208 {
209 let targets: Vec<_> = active_targets.keys().cloned().collect();
210 for target in targets {
211 build_unit_map.shift_remove(&target);
212 finish_target(target, active_targets, &mut errors, &mut seen)?;
213 }
214 debug_assert!(active_targets.is_empty());
215 claimed_files.clear();
216 iteration = 0;
217 finalized_targets = true;
218 }
219
220 let mut made_changes = false;
221 let continuing_batch = !active_targets.is_empty();
224
225 for (build_unit, file_map) in build_unit_map {
226 if seen.contains(&build_unit) {
227 continue;
228 }
229
230 let build_unit_errors = errors
231 .entry(build_unit.clone())
232 .or_insert_with(IndexSet::new);
233
234 if active_targets.is_empty() && file_map.is_empty() {
235 if finalized_targets && build_unit_errors.is_empty() {
236 continue;
237 }
238 if seen.iter().all(|b| b.package_id != build_unit.package_id) {
239 shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
240 }
241 for e in build_unit_errors.iter() {
242 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
243 }
244 errors.shift_remove(&build_unit);
245
246 seen.insert(build_unit);
247 } else if !file_map.is_empty() {
248 let was_active = active_targets.contains_key(&build_unit);
249 if continuing_batch && !was_active {
250 continue;
251 }
252
253 if !args.dangerous_parallel_fixes && !was_active && !active_targets.is_empty() {
254 let Some(graph) = package_graph.as_mut() else {
255 continue;
256 };
257
258 let mut independent = true;
259 for active in active_targets.keys() {
260 if !graph
261 .packages_are_independent(&active.package_id, &build_unit.package_id)
262 {
263 independent = false;
264 break;
265 }
266 }
267 if !independent {
268 continue;
269 }
270 }
271
272 let handles = file_map
273 .keys()
274 .map(same_file::Handle::from_path)
275 .collect::<Result<Vec<_>, _>>()
276 .ok();
277 let serialize_target = handles.is_none();
278 if serialize_target && !was_active && !active_targets.is_empty() {
279 continue;
280 }
281 if handles.as_ref().is_some_and(|handles| {
282 handles.iter().any(|handle| {
283 claimed_files
284 .get(handle)
285 .is_some_and(|owner| owner != &build_unit)
286 })
287 }) {
288 continue;
289 }
290
291 let target_files = active_targets.entry(build_unit.clone()).or_default();
292 let changed = fix_errors(target_files, file_map, build_unit_errors)?;
293 if !changed && !was_active {
294 active_targets.shift_remove(&build_unit);
295 }
296 if changed {
297 if let Some(handles) = handles {
298 for handle in handles {
299 claimed_files.entry(handle).or_insert(build_unit.clone());
300 }
301 }
302 made_changes = true;
303 if serialize_target {
304 break;
305 }
306 }
307 }
308 }
309
310 trace!("made_changes={made_changes:?}");
311 trace!("active_targets={active_targets:?}");
312
313 last_errors = errors;
314 iteration += 1;
315
316 if !made_changes {
317 if active_targets.is_empty() {
318 break;
319 }
320 let targets: Vec<_> = active_targets.keys().cloned().collect();
321 for target in targets {
322 finish_target(target, active_targets, &mut last_errors, &mut seen)?;
323 }
324 claimed_files.clear();
325 iteration = 0;
326 continue;
327 }
328 }
329
330 for files in active_targets.values() {
331 for (name, file) in files {
332 shell::fixed(name, file.fixes)?;
333 }
334 }
335
336 for e in last_errors.iter().flat_map(|(_, e)| e) {
337 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
338 }
339
340 active_targets.clear();
341 Ok(())
342}
343
344#[derive(Debug)]
346struct PackageGraph {
347 dependencies: HashMap<String, Vec<String>>,
348 reachable: HashMap<String, HashSet<String>>,
349}
350
351impl PackageGraph {
352 fn load(flags: &CheckFlags) -> Option<Self> {
354 let mut command = MetadataCommand::new();
355 command.other_options(flags.to_metadata_flags());
356
357 let metadata = match command.exec() {
358 Ok(metadata) => metadata,
359 Err(error) => {
360 warn!("failed to run `cargo metadata`: {error}");
361 return None;
362 }
363 };
364 let Some(resolve) = metadata.resolve else {
365 warn!("`cargo metadata` did not return a dependency graph");
366 return None;
367 };
368 let dependencies = resolve
369 .nodes
370 .into_iter()
371 .map(|node| {
372 (
373 node.id.repr,
374 node.dependencies
375 .into_iter()
376 .map(|dependency| dependency.repr)
377 .collect(),
378 )
379 })
380 .collect();
381
382 Some(Self {
383 dependencies,
384 reachable: HashMap::new(),
385 })
386 }
387
388 fn packages_are_independent(&mut self, left: &str, right: &str) -> bool {
390 left != right && !self.depends_on(left, right) && !self.depends_on(right, left)
391 }
392
393 fn depends_on(&mut self, package: &str, target: &str) -> bool {
395 if !self.reachable.contains_key(package) {
396 let Some(reachable) = self.collect_reachable(package) else {
397 return true;
398 };
399 self.reachable.insert(package.to_owned(), reachable);
400 }
401
402 self.reachable
403 .get(package)
404 .is_none_or(|reachable| reachable.contains(target))
405 }
406
407 fn collect_reachable(&self, root: &str) -> Option<HashSet<String>> {
409 let mut reachable = HashSet::new();
410 let mut pending = vec![root];
411
412 while let Some(package) = pending.pop() {
413 if !reachable.insert(package.to_owned()) {
414 continue;
415 }
416 let dependencies = self.dependencies.get(package)?;
417 pending.extend(dependencies.iter().map(String::as_str));
418 }
419
420 reachable.remove(root);
421 Some(reachable)
422 }
423}
424
425fn finish_target(
427 target: BuildUnit,
428 active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
429 errors: &mut IndexMap<BuildUnit, IndexSet<String>>,
430 seen: &mut HashSet<BuildUnit>,
431) -> CargoResult<()> {
432 if seen
433 .iter()
434 .all(|build_unit| build_unit.package_id != target.package_id)
435 {
436 shell::status("Checking", format_package_id(&target.package_id)?)?;
437 }
438
439 if let Some(files) = active_targets.get(&target) {
440 for (name, file) in files {
441 shell::fixed(name, file.fixes)?;
442 }
443 }
444
445 for error in errors.get(&target).into_iter().flatten() {
446 shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
447 }
448
449 active_targets.shift_remove(&target);
450 errors.shift_remove(&target);
451 seen.insert(target);
452 Ok(())
453}
454
455fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
456 let cmd = if args.clippy { "clippy" } else { "check" };
457 let mut command = Command::new(env!("CARGO"));
458 command
459 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
460 .args(args.check_flags.to_flags())
461 .stderr(Stdio::piped())
462 .stdout(Stdio::piped());
463 if *lint_cap {
464 cap_lints(&mut command);
465 }
466 let output = command.output()?;
467 let mut output = to_check_output(output);
468
469 if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
470 *lint_cap = true;
471 cap_lints(&mut command);
472 output = to_check_output(command.output()?);
473 }
474
475 Ok(output)
476}
477
478fn cap_lints(command: &mut Command) {
480 if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
481 let separator = if flags.is_empty() { "" } else { "\u{1f}" };
482 command.env(
483 "CARGO_ENCODED_RUSTFLAGS",
484 format!("{flags}{separator}--cap-lints=warn"),
485 );
486 } else {
487 command.env(
488 "RUSTFLAGS",
489 format!(
490 "--cap-lints=warn {}",
491 env::var("RUSTFLAGS").unwrap_or("".to_owned())
492 ),
493 );
494 }
495}
496
497fn denied_lint(messages: &[CheckOutput]) -> bool {
498 messages.iter().any(|message| {
499 matches!(&message, CheckOutput::Message(message)
500 if message.message.level == DiagnosticLevel::Error
501 && message.message.diagnostic.code.is_some())
502 })
503}
504
505fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
506 let buf = BufReader::new(Cursor::new(output.stdout));
507 (
508 buf.lines()
509 .map_while(|l| l.ok())
510 .filter_map(|l| serde_json::from_str(&l).ok())
511 .collect(),
512 output.status.code(),
513 )
514}
515
516#[tracing::instrument(skip_all)]
517#[allow(clippy::type_complexity)]
518fn collect_errors(
519 messages: impl Iterator<Item = CheckOutput>,
520 seen: &HashSet<BuildUnit>,
521) -> (
522 IndexMap<BuildUnit, IndexSet<String>>,
523 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
524) {
525 let only = HashSet::new();
526 let mut build_unit_map = IndexMap::new();
527
528 let mut errors = IndexMap::new();
529
530 for message in messages {
531 let Message {
532 build_unit,
533 message: MessageDiagnostic { diagnostic, .. },
534 } = match message {
535 CheckOutput::Message(m) => m,
536 CheckOutput::Artifact(a) => {
537 if !seen.contains(&a.build_unit) && !a.fresh {
538 build_unit_map
539 .entry(a.build_unit.clone())
540 .or_insert(IndexMap::new());
541 }
542 continue;
543 }
544 };
545
546 let errors = errors
547 .entry(build_unit.clone())
548 .or_insert_with(IndexSet::new);
549
550 if seen.contains(&build_unit) {
551 trace!("rejecting build unit `{:?}` already seen", build_unit);
552 continue;
553 }
554
555 let file_map = build_unit_map
556 .entry(build_unit.clone())
557 .or_insert(IndexMap::new());
558
559 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
560 rustfix::Filter::Everything
561 } else {
562 rustfix::Filter::MachineApplicableOnly
563 };
564
565 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
566 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
567 if let Some(rendered) = diagnostic.rendered {
568 errors.insert(rendered);
569 }
570 continue;
571 };
572
573 let mut file_names = suggestion
574 .solutions
575 .iter()
576 .flat_map(|s| s.replacements.iter())
577 .map(|r| &r.snippet.file_name);
578
579 let Some(file_name) = file_names.next() else {
580 trace!("rejecting as it has no solutions {:?}", suggestion);
581 if let Some(rendered) = diagnostic.rendered {
582 errors.insert(rendered);
583 }
584 continue;
585 };
586
587 if !file_names.all(|f| f == file_name) {
588 trace!("rejecting as it changes multiple files: {:?}", suggestion);
589 if let Some(rendered) = diagnostic.rendered {
590 errors.insert(rendered);
591 }
592 continue;
593 }
594
595 let file_path = Path::new(&file_name);
596 if let Ok(home) = env::var("CARGO_HOME") {
598 if file_path.starts_with(home) {
599 if let Some(rendered) = diagnostic.rendered {
600 errors.insert(rendered);
601 }
602 continue;
603 }
604 }
605
606 if file_path.is_absolute() {
607 if let Some(sysroot) = get_sysroot() {
608 if file_path.starts_with(sysroot) {
609 if let Some(rendered) = diagnostic.rendered {
610 errors.insert(rendered);
611 }
612 continue;
613 }
614 }
615 }
616
617 file_map
618 .entry(file_name.to_owned())
619 .or_insert_with(IndexSet::new)
620 .insert((suggestion, diagnostic.rendered));
621 }
622
623 (errors, build_unit_map)
624}
625
626#[tracing::instrument(skip_all)]
627fn fix_errors(
628 files: &mut IndexMap<String, File>,
629 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
630 errors: &mut IndexSet<String>,
631) -> CargoResult<bool> {
632 let mut made_changes = false;
633 for (file, suggestions) in file_map {
634 let source = match paths::read(file.as_ref()) {
635 Ok(s) => s,
636 Err(e) => {
637 warn!("failed to read `{}`: {}", file, e);
638 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
639 continue;
640 }
641 };
642
643 let mut fixed = CodeFix::new(&source);
644 let mut num_fixes = 0;
645
646 for (suggestion, rendered) in suggestions.iter().rev() {
647 match fixed.apply(suggestion) {
648 Ok(()) => num_fixes += 1,
649 Err(rustfix::Error::AlreadyReplaced {
650 is_identical: true, ..
651 }) => {}
652 Err(e) => {
653 if let Some(rendered) = rendered {
654 errors.insert(rendered.to_owned());
655 }
656 warn!("{e:?}");
657 }
658 }
659 }
660 if fixed.modified() {
661 let new_source = fixed.finish()?;
662 let file_state = files.entry(file.clone()).or_insert(File {
663 fixes: 0,
664 original_source: source,
665 });
666 paths::write(&file, new_source)?;
667 made_changes = true;
668 file_state.fixes += num_fixes;
669 }
670 }
671
672 Ok(made_changes)
673}