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 anyhow::Context;
12use cargo_metadata::Metadata;
13use cargo_metadata::MetadataCommand;
14use cargo_util::paths;
15use cargo_util_schemas::core::PackageIdSpec;
16use clap::Parser;
17use indexmap::{IndexMap, IndexSet};
18use rustfix::{collect_suggestions, CodeFix, Suggestion};
19use tracing::{trace, warn};
20
21use crate::util::cli::PackageSelection;
22use crate::{
23    core::{shell, sysroot::get_sysroot},
24    ops::check::{BuildUnit, CheckOutput, DiagnosticLevel, Message, MessageDiagnostic},
25    util::{
26        cli::CheckFlags, messages::gen_please_report_this_bug_text, package::format_package_id,
27        vcs::VcsOpts,
28    },
29    CargoResult,
30};
31
32#[derive(Debug, Parser)]
33pub struct FixitArgs {
34    /// Run `clippy` instead of `check`
35    #[arg(long)]
36    clippy: bool,
37
38    /// Fix code even if it already has compiler errors
39    #[arg(long)]
40    broken_code: bool,
41
42    /// Fix all targets together, risking stale suggestions
43    #[arg(long = "Zdangerous-parallel-fixes")]
44    dangerous_parallel_fixes: bool,
45
46    #[command(flatten)]
47    color: colorchoice_clap::Color,
48
49    #[command(flatten)]
50    vcs_opts: VcsOpts,
51
52    #[command(flatten)]
53    check_flags: CheckFlags,
54}
55
56impl FixitArgs {
57    pub fn exec(self) -> CargoResult<()> {
58        exec(self)
59    }
60
61    fn to_command(&self) -> Command {
62        let cmd = if self.clippy { "clippy" } else { "check" };
63        let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
64        let mut command = Command::new(cargo);
65        command.arg(cmd).args(self.check_flags.to_flags());
66        command
67    }
68}
69
70#[derive(Debug, Default)]
71struct File {
72    fixes: u32,
73    original_source: String,
74}
75
76type BuildUnitErrors = IndexMap<BuildUnit, IndexSet<String>>;
77type BuildUnitSuggestions =
78    IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>;
79
80#[tracing::instrument(skip_all)]
81fn exec(args: FixitArgs) -> CargoResult<()> {
82    args.color.write_global();
83
84    args.vcs_opts.valid_vcs()?;
85
86    let mut active_targets = IndexMap::new();
87    match fix(&args, &mut active_targets) {
88        Ok(()) => Ok(()),
89        Err(error) => {
90            for (file, original) in active_targets.values().flat_map(|files| files.iter()) {
91                paths::write(file, &original.original_source)?;
92            }
93            Err(error)
94        }
95    }
96}
97
98fn fix(
99    args: &FixitArgs,
100    active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
101) -> CargoResult<()> {
102    let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
103        .ok()
104        .and_then(|i| i.parse().ok())
105        .unwrap_or(4);
106    let mut iteration = 0;
107    let mut lint_cap = false;
108
109    let mut last_errors = IndexMap::new();
110    let mut claimed_files: HashMap<same_file::Handle, BuildUnit> = HashMap::new();
111    let mut package_metadata_cache = None;
112    let mut primary_packages_cache = None;
113    let mut package_graph_cache: Option<Option<PackageGraph>> = None;
114    let mut seen = HashSet::new();
115
116    loop {
117        trace!("iteration={iteration}");
118        trace!("active_targets={active_targets:?}");
119        let (messages, exit_code) = check(args, &mut lint_cap)?;
120
121        if messages.is_empty() && exit_code != Some(0) {
122            let mut command = args.to_command();
123            command.status()?;
124            anyhow::bail!("could not compile");
125        } else if !args.broken_code && exit_code != Some(0) {
126            let mut out = String::new();
127
128            if !active_targets.is_empty() {
129                out.push_str(
130                    "failed to automatically apply fixes suggested by rustc\n\n\
131                    after fixes were automatically applied the \
132                    compiler reported errors within these files:\n\n",
133                );
134
135                for (
136                    file,
137                    File {
138                        fixes: _,
139                        original_source,
140                    },
141                ) in active_targets.values().flat_map(|files| files.iter())
142                {
143                    out.push_str(&format!("  * {file}\n"));
144                    shell::note(format!("reverting `{file}` to its original state"))?;
145                    paths::write(file, original_source)?;
146                }
147                active_targets.clear();
148                out.push('\n');
149
150                out.push_str(&gen_please_report_this_bug_text(args.clippy));
151
152                let mut errors = messages
153                    .into_iter()
154                    .filter_map(|e| match e {
155                        CheckOutput::Message(m) => m.message.diagnostic.rendered,
156                        _ => None,
157                    })
158                    .peekable();
159                if errors.peek().is_some() {
160                    out.push_str("The errors reported are:\n");
161                }
162
163                for e in errors {
164                    out.push_str(&format!("{}\n\n", e.trim_end()));
165                }
166
167                let (messages, _) = check(args, &mut lint_cap)?;
168                let mut errors = messages
169                    .into_iter()
170                    .filter_map(|e| match e {
171                        CheckOutput::Message(m) => m.message.diagnostic.rendered,
172                        _ => None,
173                    })
174                    .peekable();
175
176                if errors.peek().is_some() {
177                    out.push_str("The original errors are:\n");
178                }
179
180                for e in errors {
181                    out.push_str(&format!("{}\n\n", e.trim_end()));
182                }
183
184                shell::warn(out)?;
185            } else {
186                for e in messages.into_iter().filter_map(|e| match e {
187                    CheckOutput::Message(m) => m.message.diagnostic.rendered,
188                    _ => None,
189                }) {
190                    shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
191                }
192            }
193
194            shell::note("try using `--broken-code` to fix errors")?;
195            anyhow::bail!("could not compile");
196        }
197
198        let (mut errors, mut build_unit_map) = collect_errors(messages.into_iter(), &seen);
199        if build_unit_map.values().any(|file_map| !file_map.is_empty()) {
200            let primary_packages = if let Some(primary_packages) = &primary_packages_cache {
201                primary_packages
202            } else {
203                let metadata = package_metadata(&mut package_metadata_cache, &args.check_flags)?;
204                primary_packages_cache
205                    .insert(PrimaryPackages::from_metadata(metadata, &args.check_flags)?)
206            };
207            retain_primary_fixes(primary_packages, &mut errors, &mut build_unit_map);
208        }
209
210        if iteration >= max_iterations {
211            if active_targets.is_empty() {
212                break;
213            }
214            let targets: Vec<_> = active_targets.keys().cloned().collect();
215            for target in targets {
216                if let Some(file_map) = build_unit_map.get(&target) {
217                    let target_errors = errors.entry(target.clone()).or_default();
218                    target_errors.extend(
219                        file_map
220                            .values()
221                            .flatten()
222                            .filter_map(|(_, diagnostic)| diagnostic.clone()),
223                    );
224                }
225                finish_target(target, active_targets, &mut errors, &mut seen)?;
226            }
227            claimed_files.clear();
228            iteration = 0;
229        }
230
231        let mut finalized_targets = false;
232        if !active_targets.is_empty()
233            && active_targets
234                .keys()
235                .all(|target| build_unit_map.get(target).is_none_or(IndexMap::is_empty))
236        {
237            let targets: Vec<_> = active_targets.keys().cloned().collect();
238            for target in targets {
239                build_unit_map.shift_remove(&target);
240                finish_target(target, active_targets, &mut errors, &mut seen)?;
241            }
242            debug_assert!(active_targets.is_empty());
243            claimed_files.clear();
244            iteration = 0;
245            finalized_targets = true;
246        }
247
248        let mut made_changes = false;
249        // Admit build units from one compiler snapshot only when their packages are independent.
250        // Once a batch is active, recheck and finish it before considering additional units.
251        let continuing_batch = !active_targets.is_empty();
252
253        for (build_unit, file_map) in build_unit_map {
254            if seen.contains(&build_unit) {
255                continue;
256            }
257
258            let build_unit_errors = errors
259                .entry(build_unit.clone())
260                .or_insert_with(IndexSet::new);
261
262            if active_targets.is_empty() && file_map.is_empty() {
263                if finalized_targets && build_unit_errors.is_empty() {
264                    continue;
265                }
266                if seen.iter().all(|b| b.package_id != build_unit.package_id) {
267                    shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
268                }
269                for e in build_unit_errors.iter() {
270                    shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
271                }
272                errors.shift_remove(&build_unit);
273
274                seen.insert(build_unit);
275            } else if !file_map.is_empty() {
276                let was_active = active_targets.contains_key(&build_unit);
277                if continuing_batch && !was_active {
278                    continue;
279                }
280
281                if !args.dangerous_parallel_fixes && !was_active && !active_targets.is_empty() {
282                    if active_targets
283                        .keys()
284                        .any(|active| active.package_id == build_unit.package_id)
285                    {
286                        continue;
287                    }
288
289                    if package_graph_cache.is_none() {
290                        let metadata =
291                            package_metadata(&mut package_metadata_cache, &args.check_flags)?;
292                        package_graph_cache = Some(PackageGraph::load(metadata, &args.check_flags));
293                    }
294                    let Some(Some(graph)) = package_graph_cache.as_mut() else {
295                        continue;
296                    };
297
298                    let mut independent = true;
299                    for active in active_targets.keys() {
300                        if !graph
301                            .packages_are_independent(&active.package_id, &build_unit.package_id)
302                        {
303                            independent = false;
304                            break;
305                        }
306                    }
307                    if !independent {
308                        continue;
309                    }
310                }
311
312                let handles = file_map
313                    .keys()
314                    .map(same_file::Handle::from_path)
315                    .collect::<Result<Vec<_>, _>>()
316                    .ok();
317                let serialize_target = handles.is_none();
318                if serialize_target && !was_active && !active_targets.is_empty() {
319                    continue;
320                }
321                if handles.as_ref().is_some_and(|handles| {
322                    handles.iter().any(|handle| {
323                        claimed_files
324                            .get(handle)
325                            .is_some_and(|owner| owner != &build_unit)
326                    })
327                }) {
328                    continue;
329                }
330
331                let target_files = active_targets.entry(build_unit.clone()).or_default();
332                let changed = fix_errors(target_files, file_map, build_unit_errors)?;
333                if !changed && !was_active {
334                    active_targets.shift_remove(&build_unit);
335                }
336                if changed {
337                    if let Some(handles) = handles {
338                        for handle in handles {
339                            claimed_files.entry(handle).or_insert(build_unit.clone());
340                        }
341                    }
342                    made_changes = true;
343                    if serialize_target {
344                        break;
345                    }
346                }
347            }
348        }
349
350        trace!("made_changes={made_changes:?}");
351        trace!("active_targets={active_targets:?}");
352
353        last_errors = errors;
354        iteration += 1;
355
356        if !made_changes {
357            if active_targets.is_empty() {
358                break;
359            }
360            let targets: Vec<_> = active_targets.keys().cloned().collect();
361            for target in targets {
362                finish_target(target, active_targets, &mut last_errors, &mut seen)?;
363            }
364            claimed_files.clear();
365            iteration = 0;
366            continue;
367        }
368    }
369
370    for files in active_targets.values() {
371        for (name, file) in files {
372            shell::fixed(name, file.fixes)?;
373        }
374    }
375
376    for e in last_errors.iter().flat_map(|(_, e)| e) {
377        shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
378    }
379
380    active_targets.clear();
381    Ok(())
382}
383
384/// Packages that Cargo treats as primary for the current invocation.
385#[derive(Debug)]
386struct PrimaryPackages {
387    package_ids: HashSet<String>,
388}
389
390impl PrimaryPackages {
391    /// Reconstructs Cargo's primary package set from its package-selection flags.
392    fn from_metadata(metadata: &Metadata, flags: &CheckFlags) -> CargoResult<Self> {
393        let package_ids = match flags.package_selection() {
394            PackageSelection::Default => metadata
395                .workspace_default_members
396                .iter()
397                .map(|package_id| package_id.repr.clone())
398                .collect(),
399            PackageSelection::Workspace { exclude } => {
400                let matcher = PackageSpecMatcher::new(exclude)?;
401                let mut package_ids = HashSet::new();
402                for package in metadata.workspace_packages() {
403                    if !matcher.matches(package)? {
404                        package_ids.insert(package.id.repr.clone());
405                    }
406                }
407                package_ids
408            }
409            PackageSelection::Packages(packages) => {
410                let matcher = PackageSpecMatcher::new(packages)?;
411                let mut package_ids = HashSet::new();
412                for package in metadata.workspace_packages() {
413                    if matcher.matches(package)? {
414                        package_ids.insert(package.id.repr.clone());
415                    }
416                }
417                package_ids
418            }
419        };
420        Ok(Self { package_ids })
421    }
422
423    fn contains(&self, package_id: &str) -> bool {
424        self.package_ids.contains(package_id)
425    }
426}
427
428/// Matches Cargo package specifications and package-name glob patterns.
429#[derive(Debug)]
430struct PackageSpecMatcher {
431    specs: Vec<PackageIdSpec>,
432    patterns: Vec<glob::Pattern>,
433}
434
435impl PackageSpecMatcher {
436    fn new(raw_specs: &[String]) -> CargoResult<Self> {
437        let mut specs = Vec::new();
438        let mut patterns = Vec::new();
439
440        for raw_spec in raw_specs {
441            match PackageIdSpec::parse(raw_spec) {
442                Ok(spec) => specs.push(spec),
443                Err(_) if raw_spec.contains(&['*', '?', '[', ']'][..]) => {
444                    let pattern = glob::Pattern::new(raw_spec)
445                        .with_context(|| format!("failed to parse package pattern `{raw_spec}`"))?;
446                    patterns.push(pattern);
447                }
448                Err(error) => {
449                    return Err(error).with_context(|| {
450                        format!("failed to parse package specification `{raw_spec}`")
451                    });
452                }
453            }
454        }
455
456        Ok(Self { specs, patterns })
457    }
458
459    fn matches(&self, package: &cargo_metadata::Package) -> CargoResult<bool> {
460        if self
461            .patterns
462            .iter()
463            .any(|pattern| pattern.matches(package.name.as_ref()))
464        {
465            return Ok(true);
466        }
467
468        let package_id = PackageIdSpec::parse(&package.id.repr)
469            .with_context(|| format!("failed to parse package ID `{}`", package.id))?;
470        Ok(self
471            .specs
472            .iter()
473            .any(|spec| package_id_matches(spec, &package_id)))
474    }
475}
476
477/// Mirrors Cargo's internal package-ID matching rules.
478fn package_id_matches(spec: &PackageIdSpec, package_id: &PackageIdSpec) -> bool {
479    spec.name() == package_id.name()
480        && spec.partial_version().is_none_or(|version| {
481            package_id
482                .version()
483                .is_some_and(|package_version| version.matches(&package_version))
484        })
485        && spec.url().is_none_or(|url| package_id.url() == Some(url))
486        && spec
487            .kind()
488            .is_none_or(|kind| package_id.kind() == Some(kind))
489}
490
491/// Loads unresolved package metadata once and reuses it for selection and ordering.
492fn package_metadata<'a>(
493    cache: &'a mut Option<Metadata>,
494    flags: &CheckFlags,
495) -> CargoResult<&'a Metadata> {
496    match cache {
497        Some(metadata) => Ok(metadata),
498        cache @ None => {
499            let mut command = MetadataCommand::new();
500            command.no_deps();
501            command.other_options(flags.to_metadata_flags());
502            let metadata = command.exec().context("failed to run `cargo metadata`")?;
503            Ok(cache.insert(metadata))
504        }
505    }
506}
507
508/// Discards dependency suggestions while preserving their diagnostics for display.
509fn retain_primary_fixes(
510    primary_packages: &PrimaryPackages,
511    errors: &mut BuildUnitErrors,
512    build_unit_map: &mut BuildUnitSuggestions,
513) {
514    for (build_unit, file_map) in build_unit_map {
515        if file_map.is_empty() || primary_packages.contains(&build_unit.package_id) {
516            continue;
517        }
518
519        let build_unit_errors = errors.entry(build_unit.clone()).or_default();
520        build_unit_errors.extend(
521            file_map
522                .values()
523                .flatten()
524                .filter_map(|(_, diagnostic)| diagnostic.clone()),
525        );
526        file_map.clear();
527    }
528}
529
530/// Package dependencies used to batch only transitively unrelated packages.
531#[derive(Debug)]
532struct PackageGraph {
533    dependencies: HashMap<String, Vec<String>>,
534    reachable: HashMap<String, HashSet<String>>,
535}
536
537impl PackageGraph {
538    /// Loads the package graph, returning `None` when batching must remain serial.
539    fn load(metadata: &Metadata, flags: &CheckFlags) -> Option<Self> {
540        // A script is identified by its manifest path, not its containing directory.
541        if metadata
542            .packages
543            .iter()
544            .any(|package| package.manifest_path.file_name() != Some("Cargo.toml"))
545        {
546            return Self::load_resolved(flags);
547        }
548
549        let package_ids_by_path: HashMap<_, _> = metadata
550            .packages
551            .iter()
552            .filter_map(|package| {
553                package
554                    .manifest_path
555                    .parent()
556                    .map(|path| (path, package.id.repr.as_str()))
557            })
558            .collect();
559        if package_ids_by_path.len() != metadata.packages.len() {
560            return Self::load_resolved(flags);
561        }
562
563        let package_names: HashSet<_> = metadata
564            .packages
565            .iter()
566            .map(|package| package.name.as_ref())
567            .collect();
568        let mut dependencies = HashMap::with_capacity(metadata.packages.len());
569        for package in &metadata.packages {
570            let mut package_dependencies = Vec::new();
571            for dependency in &package.dependencies {
572                if let Some(path) = &dependency.path {
573                    let Some(dependency_id) = package_ids_by_path.get(path.as_path()) else {
574                        return Self::load_resolved(flags);
575                    };
576                    package_dependencies.push((*dependency_id).to_owned());
577                } else if package_names.contains(dependency.name.as_str()) {
578                    return Self::load_resolved(flags);
579                }
580            }
581            dependencies.insert(package.id.repr.clone(), package_dependencies);
582        }
583
584        Some(Self {
585            dependencies,
586            reachable: HashMap::new(),
587        })
588    }
589
590    /// Resolves external packages when workspace metadata cannot prove independence.
591    fn load_resolved(flags: &CheckFlags) -> Option<Self> {
592        let mut command = MetadataCommand::new();
593        command.other_options(flags.to_metadata_flags());
594
595        let metadata = match command.exec() {
596            Ok(metadata) => metadata,
597            Err(error) => {
598                warn!("failed to run `cargo metadata`: {error}");
599                return None;
600            }
601        };
602        let Some(resolve) = metadata.resolve else {
603            warn!("`cargo metadata` did not return a dependency graph");
604            return None;
605        };
606        let dependencies = resolve
607            .nodes
608            .into_iter()
609            .map(|node| {
610                (
611                    node.id.repr,
612                    node.dependencies
613                        .into_iter()
614                        .map(|dependency| dependency.repr)
615                        .collect(),
616                )
617            })
618            .collect();
619
620        Some(Self {
621            dependencies,
622            reachable: HashMap::new(),
623        })
624    }
625
626    /// Returns whether both packages are known and transitively unrelated.
627    fn packages_are_independent(&mut self, left: &str, right: &str) -> bool {
628        left != right && !self.depends_on(left, right) && !self.depends_on(right, left)
629    }
630
631    /// Returns whether `package` transitively depends on `target`.
632    fn depends_on(&mut self, package: &str, target: &str) -> bool {
633        if !self.reachable.contains_key(package) {
634            let Some(reachable) = self.collect_reachable(package) else {
635                return true;
636            };
637            self.reachable.insert(package.to_owned(), reachable);
638        }
639
640        self.reachable
641            .get(package)
642            .is_none_or(|reachable| reachable.contains(target))
643    }
644
645    /// Collects the packages transitively reachable from `root`.
646    fn collect_reachable(&self, root: &str) -> Option<HashSet<String>> {
647        let mut reachable = HashSet::new();
648        let mut pending = vec![root];
649
650        while let Some(package) = pending.pop() {
651            if !reachable.insert(package.to_owned()) {
652                continue;
653            }
654            let dependencies = self.dependencies.get(package)?;
655            pending.extend(dependencies.iter().map(String::as_str));
656        }
657
658        reachable.remove(root);
659        Some(reachable)
660    }
661}
662
663/// Marks a target complete after reporting its fixes and remaining diagnostics.
664fn finish_target(
665    target: BuildUnit,
666    active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
667    errors: &mut BuildUnitErrors,
668    seen: &mut HashSet<BuildUnit>,
669) -> CargoResult<()> {
670    if seen
671        .iter()
672        .all(|build_unit| build_unit.package_id != target.package_id)
673    {
674        shell::status("Checking", format_package_id(&target.package_id)?)?;
675    }
676
677    if let Some(files) = active_targets.get(&target) {
678        for (name, file) in files {
679            shell::fixed(name, file.fixes)?;
680        }
681    }
682
683    for error in errors.get(&target).into_iter().flatten() {
684        shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
685    }
686
687    active_targets.shift_remove(&target);
688    errors.shift_remove(&target);
689    seen.insert(target);
690    Ok(())
691}
692
693fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
694    let mut command = args.to_command();
695    command
696        .args(["--message-format", "json-diagnostic-rendered-ansi"])
697        .stderr(Stdio::piped())
698        .stdout(Stdio::piped());
699    if *lint_cap {
700        cap_lints(&mut command);
701    }
702    let output = command.output()?;
703    let mut output = to_check_output(output);
704
705    if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
706        *lint_cap = true;
707        cap_lints(&mut command);
708        output = to_check_output(command.output()?);
709    }
710
711    Ok(output)
712}
713
714/// Applies the original lint cap while preserving existing compiler flags.
715fn cap_lints(command: &mut Command) {
716    if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
717        let separator = if flags.is_empty() { "" } else { "\u{1f}" };
718        command.env(
719            "CARGO_ENCODED_RUSTFLAGS",
720            format!("{flags}{separator}--cap-lints=warn"),
721        );
722    } else {
723        command.env(
724            "RUSTFLAGS",
725            format!(
726                "--cap-lints=warn {}",
727                env::var("RUSTFLAGS").unwrap_or("".to_owned())
728            ),
729        );
730    }
731}
732
733fn denied_lint(messages: &[CheckOutput]) -> bool {
734    messages.iter().any(|message| {
735        matches!(&message, CheckOutput::Message(message)
736                if message.message.level == DiagnosticLevel::Error
737                    && message.message.diagnostic.code.is_some())
738    })
739}
740
741fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
742    let buf = BufReader::new(Cursor::new(output.stdout));
743    (
744        buf.lines()
745            .map_while(|l| l.ok())
746            .filter_map(|l| serde_json::from_str(&l).ok())
747            .collect(),
748        output.status.code(),
749    )
750}
751
752#[tracing::instrument(skip_all)]
753fn collect_errors(
754    messages: impl Iterator<Item = CheckOutput>,
755    seen: &HashSet<BuildUnit>,
756) -> (BuildUnitErrors, BuildUnitSuggestions) {
757    let only = HashSet::new();
758    let mut build_unit_map = IndexMap::new();
759
760    let mut errors = IndexMap::new();
761
762    for message in messages {
763        let Message {
764            build_unit,
765            message: MessageDiagnostic { diagnostic, .. },
766        } = match message {
767            CheckOutput::Message(m) => m,
768            CheckOutput::Artifact(a) => {
769                if !seen.contains(&a.build_unit) && !a.fresh {
770                    build_unit_map
771                        .entry(a.build_unit.clone())
772                        .or_insert(IndexMap::new());
773                }
774                continue;
775            }
776        };
777
778        let errors = errors
779            .entry(build_unit.clone())
780            .or_insert_with(IndexSet::new);
781
782        if seen.contains(&build_unit) {
783            trace!("rejecting build unit `{:?}` already seen", build_unit);
784            continue;
785        }
786
787        let file_map = build_unit_map
788            .entry(build_unit.clone())
789            .or_insert(IndexMap::new());
790
791        let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
792            rustfix::Filter::Everything
793        } else {
794            rustfix::Filter::MachineApplicableOnly
795        };
796
797        let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
798            trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
799            if let Some(rendered) = diagnostic.rendered {
800                errors.insert(rendered);
801            }
802            continue;
803        };
804
805        let mut file_names = suggestion
806            .solutions
807            .iter()
808            .flat_map(|s| s.replacements.iter())
809            .map(|r| &r.snippet.file_name);
810
811        let Some(file_name) = file_names.next() else {
812            trace!("rejecting as it has no solutions {:?}", suggestion);
813            if let Some(rendered) = diagnostic.rendered {
814                errors.insert(rendered);
815            }
816            continue;
817        };
818
819        if !file_names.all(|f| f == file_name) {
820            trace!("rejecting as it changes multiple files: {:?}", suggestion);
821            if let Some(rendered) = diagnostic.rendered {
822                errors.insert(rendered);
823            }
824            continue;
825        }
826
827        let file_path = Path::new(&file_name);
828        // Do not write into registry cache. See rust-lang/cargo#9857.
829        if let Ok(home) = env::var("CARGO_HOME") {
830            if file_path.starts_with(home) {
831                if let Some(rendered) = diagnostic.rendered {
832                    errors.insert(rendered);
833                }
834                continue;
835            }
836        }
837
838        if file_path.is_absolute() {
839            if let Some(sysroot) = get_sysroot() {
840                if file_path.starts_with(sysroot) {
841                    if let Some(rendered) = diagnostic.rendered {
842                        errors.insert(rendered);
843                    }
844                    continue;
845                }
846            }
847        }
848
849        file_map
850            .entry(file_name.to_owned())
851            .or_insert_with(IndexSet::new)
852            .insert((suggestion, diagnostic.rendered));
853    }
854
855    (errors, build_unit_map)
856}
857
858#[tracing::instrument(skip_all)]
859fn fix_errors(
860    files: &mut IndexMap<String, File>,
861    file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
862    errors: &mut IndexSet<String>,
863) -> CargoResult<bool> {
864    let mut made_changes = false;
865    for (file, suggestions) in file_map {
866        let source = match paths::read(file.as_ref()) {
867            Ok(s) => s,
868            Err(e) => {
869                warn!("failed to read `{}`: {}", file, e);
870                errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
871                continue;
872            }
873        };
874
875        let mut fixed = CodeFix::new(&source);
876        let mut num_fixes = 0;
877
878        for (suggestion, rendered) in suggestions.iter().rev() {
879            match fixed.apply(suggestion) {
880                Ok(()) => num_fixes += 1,
881                Err(rustfix::Error::AlreadyReplaced {
882                    is_identical: true, ..
883                }) => {}
884                Err(e) => {
885                    if let Some(rendered) = rendered {
886                        errors.insert(rendered.to_owned());
887                    }
888                    warn!("{e:?}");
889                }
890            }
891        }
892        if fixed.modified() {
893            let new_source = fixed.finish()?;
894            let file_state = files.entry(file.clone()).or_insert(File {
895                fixes: 0,
896                original_source: source,
897            });
898            paths::write(&file, new_source)?;
899            made_changes = true;
900            file_state.fixes += num_fixes;
901        }
902    }
903
904    Ok(made_changes)
905}