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.other_options(flags.to_metadata_flags());
362
363 let metadata = match command.exec() {
364 Ok(metadata) => metadata,
365 Err(error) => {
366 warn!("failed to run `cargo metadata`: {error}");
367 return None;
368 }
369 };
370 let Some(resolve) = metadata.resolve else {
371 warn!("`cargo metadata` did not return a dependency graph");
372 return None;
373 };
374 let dependencies = resolve
375 .nodes
376 .into_iter()
377 .map(|node| {
378 (
379 node.id.repr,
380 node.dependencies
381 .into_iter()
382 .map(|dependency| dependency.repr)
383 .collect(),
384 )
385 })
386 .collect();
387
388 Some(Self {
389 dependencies,
390 reachable: HashMap::new(),
391 })
392 }
393
394 fn packages_are_independent(&mut self, left: &str, right: &str) -> bool {
396 left != right && !self.depends_on(left, right) && !self.depends_on(right, left)
397 }
398
399 fn depends_on(&mut self, package: &str, target: &str) -> bool {
401 if !self.reachable.contains_key(package) {
402 let Some(reachable) = self.collect_reachable(package) else {
403 return true;
404 };
405 self.reachable.insert(package.to_owned(), reachable);
406 }
407
408 self.reachable
409 .get(package)
410 .is_none_or(|reachable| reachable.contains(target))
411 }
412
413 fn collect_reachable(&self, root: &str) -> Option<HashSet<String>> {
415 let mut reachable = HashSet::new();
416 let mut pending = vec![root];
417
418 while let Some(package) = pending.pop() {
419 if !reachable.insert(package.to_owned()) {
420 continue;
421 }
422 let dependencies = self.dependencies.get(package)?;
423 pending.extend(dependencies.iter().map(String::as_str));
424 }
425
426 reachable.remove(root);
427 Some(reachable)
428 }
429}
430
431fn finish_target(
433 target: BuildUnit,
434 active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
435 errors: &mut IndexMap<BuildUnit, IndexSet<String>>,
436 seen: &mut HashSet<BuildUnit>,
437) -> CargoResult<()> {
438 if seen
439 .iter()
440 .all(|build_unit| build_unit.package_id != target.package_id)
441 {
442 shell::status("Checking", format_package_id(&target.package_id)?)?;
443 }
444
445 if let Some(files) = active_targets.get(&target) {
446 for (name, file) in files {
447 shell::fixed(name, file.fixes)?;
448 }
449 }
450
451 for error in errors.get(&target).into_iter().flatten() {
452 shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
453 }
454
455 active_targets.shift_remove(&target);
456 errors.shift_remove(&target);
457 seen.insert(target);
458 Ok(())
459}
460
461fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
462 let cmd = if args.clippy { "clippy" } else { "check" };
463 let mut command = Command::new(env!("CARGO"));
464 command
465 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
466 .args(args.check_flags.to_flags())
467 .stderr(Stdio::piped())
468 .stdout(Stdio::piped());
469 if *lint_cap {
470 cap_lints(&mut command);
471 }
472 let output = command.output()?;
473 let mut output = to_check_output(output);
474
475 if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
476 *lint_cap = true;
477 cap_lints(&mut command);
478 output = to_check_output(command.output()?);
479 }
480
481 Ok(output)
482}
483
484fn cap_lints(command: &mut Command) {
486 if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
487 let separator = if flags.is_empty() { "" } else { "\u{1f}" };
488 command.env(
489 "CARGO_ENCODED_RUSTFLAGS",
490 format!("{flags}{separator}--cap-lints=warn"),
491 );
492 } else {
493 command.env(
494 "RUSTFLAGS",
495 format!(
496 "--cap-lints=warn {}",
497 env::var("RUSTFLAGS").unwrap_or("".to_owned())
498 ),
499 );
500 }
501}
502
503fn denied_lint(messages: &[CheckOutput]) -> bool {
504 messages.iter().any(|message| {
505 matches!(&message, CheckOutput::Message(message)
506 if message.message.level == DiagnosticLevel::Error
507 && message.message.diagnostic.code.is_some())
508 })
509}
510
511fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
512 let buf = BufReader::new(Cursor::new(output.stdout));
513 (
514 buf.lines()
515 .map_while(|l| l.ok())
516 .filter_map(|l| serde_json::from_str(&l).ok())
517 .collect(),
518 output.status.code(),
519 )
520}
521
522#[tracing::instrument(skip_all)]
523#[allow(clippy::type_complexity)]
524fn collect_errors(
525 messages: impl Iterator<Item = CheckOutput>,
526 seen: &HashSet<BuildUnit>,
527) -> (
528 IndexMap<BuildUnit, IndexSet<String>>,
529 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
530) {
531 let only = HashSet::new();
532 let mut build_unit_map = IndexMap::new();
533
534 let mut errors = IndexMap::new();
535
536 for message in messages {
537 let Message {
538 build_unit,
539 message: MessageDiagnostic { diagnostic, .. },
540 } = match message {
541 CheckOutput::Message(m) => m,
542 CheckOutput::Artifact(a) => {
543 if !seen.contains(&a.build_unit) && !a.fresh {
544 build_unit_map
545 .entry(a.build_unit.clone())
546 .or_insert(IndexMap::new());
547 }
548 continue;
549 }
550 };
551
552 let errors = errors
553 .entry(build_unit.clone())
554 .or_insert_with(IndexSet::new);
555
556 if seen.contains(&build_unit) {
557 trace!("rejecting build unit `{:?}` already seen", build_unit);
558 continue;
559 }
560
561 let file_map = build_unit_map
562 .entry(build_unit.clone())
563 .or_insert(IndexMap::new());
564
565 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
566 rustfix::Filter::Everything
567 } else {
568 rustfix::Filter::MachineApplicableOnly
569 };
570
571 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
572 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
573 if let Some(rendered) = diagnostic.rendered {
574 errors.insert(rendered);
575 }
576 continue;
577 };
578
579 let mut file_names = suggestion
580 .solutions
581 .iter()
582 .flat_map(|s| s.replacements.iter())
583 .map(|r| &r.snippet.file_name);
584
585 let Some(file_name) = file_names.next() else {
586 trace!("rejecting as it has no solutions {:?}", suggestion);
587 if let Some(rendered) = diagnostic.rendered {
588 errors.insert(rendered);
589 }
590 continue;
591 };
592
593 if !file_names.all(|f| f == file_name) {
594 trace!("rejecting as it changes multiple files: {:?}", suggestion);
595 if let Some(rendered) = diagnostic.rendered {
596 errors.insert(rendered);
597 }
598 continue;
599 }
600
601 let file_path = Path::new(&file_name);
602 if let Ok(home) = env::var("CARGO_HOME") {
604 if file_path.starts_with(home) {
605 if let Some(rendered) = diagnostic.rendered {
606 errors.insert(rendered);
607 }
608 continue;
609 }
610 }
611
612 if file_path.is_absolute() {
613 if let Some(sysroot) = get_sysroot() {
614 if file_path.starts_with(sysroot) {
615 if let Some(rendered) = diagnostic.rendered {
616 errors.insert(rendered);
617 }
618 continue;
619 }
620 }
621 }
622
623 file_map
624 .entry(file_name.to_owned())
625 .or_insert_with(IndexSet::new)
626 .insert((suggestion, diagnostic.rendered));
627 }
628
629 (errors, build_unit_map)
630}
631
632#[tracing::instrument(skip_all)]
633fn fix_errors(
634 files: &mut IndexMap<String, File>,
635 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
636 errors: &mut IndexSet<String>,
637) -> CargoResult<bool> {
638 let mut made_changes = false;
639 for (file, suggestions) in file_map {
640 let source = match paths::read(file.as_ref()) {
641 Ok(s) => s,
642 Err(e) => {
643 warn!("failed to read `{}`: {}", file, e);
644 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
645 continue;
646 }
647 };
648
649 let mut fixed = CodeFix::new(&source);
650 let mut num_fixes = 0;
651
652 for (suggestion, rendered) in suggestions.iter().rev() {
653 match fixed.apply(suggestion) {
654 Ok(()) => num_fixes += 1,
655 Err(rustfix::Error::AlreadyReplaced {
656 is_identical: true, ..
657 }) => {}
658 Err(e) => {
659 if let Some(rendered) = rendered {
660 errors.insert(rendered.to_owned());
661 }
662 warn!("{e:?}");
663 }
664 }
665 }
666 if fixed.modified() {
667 let new_source = fixed.finish()?;
668 let file_state = files.entry(file.clone()).or_insert(File {
669 fixes: 0,
670 original_source: source,
671 });
672 paths::write(&file, new_source)?;
673 made_changes = true;
674 file_state.fixes += num_fixes;
675 }
676 }
677
678 Ok(made_changes)
679}