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