Skip to main content

cargo_fixit/ops/
fixit.rs

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    /// Run `clippy` instead of `check`
31    #[arg(long)]
32    clippy: bool,
33
34    /// Fix code even if it already has compiler errors
35    #[arg(long)]
36    broken_code: bool,
37
38    /// Fix all targets together, risking stale suggestions
39    #[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        // Admit build units from one compiler snapshot only when their packages are independent.
222        // Once a batch is active, recheck and finish it before considering additional units.
223        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/// Resolved package dependencies used to batch only transitively unrelated packages.
345#[derive(Debug)]
346struct PackageGraph {
347    dependencies: HashMap<String, Vec<String>>,
348    reachable: HashMap<String, HashSet<String>>,
349}
350
351impl PackageGraph {
352    /// Loads the package graph, returning `None` when batching must remain serial.
353    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    /// Returns whether both packages are known and transitively unrelated.
389    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    /// Returns whether `package` transitively depends on `target`.
394    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    /// Collects the packages transitively reachable from `root`.
408    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
425/// Marks a target complete after reporting its fixes and remaining diagnostics.
426fn 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
478/// Applies the original lint cap while preserving existing compiler flags.
479fn 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        // Do not write into registry cache. See rust-lang/cargo#9857.
597        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}