Skip to main content

cargo_fixit/ops/
fixit.rs

1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::env;
6use std::io::BufRead;
7use std::io::BufReader;
8use std::io::Read;
9use std::path::Path;
10use std::process::Child;
11use std::process::Command;
12use std::process::Stdio;
13use std::thread::JoinHandle;
14
15use anyhow::Context;
16use cargo_metadata::Metadata;
17use cargo_metadata::MetadataCommand;
18use cargo_util::paths;
19use cargo_util_schemas::core::PackageIdSpec;
20use clap::ArgAction;
21use clap::Parser;
22use indexmap::{IndexMap, IndexSet};
23use rustfix::{collect_suggestions, CodeFix, Suggestion};
24use tracing::{trace, warn};
25
26use crate::util::cli::PackageSelection;
27use crate::{
28    core::{shell, sysroot::get_sysroot},
29    ops::check::{
30        BuildUnit, CheckOutput, CrateType, DiagnosticLevel, Message, MessageDiagnostic, TargetKind,
31    },
32    util::{
33        cli::CheckFlags, messages::gen_please_report_this_bug_text, package::format_package_id,
34        vcs::VcsOpts,
35    },
36    CargoResult,
37};
38
39#[derive(Debug, Parser)]
40pub struct FixitArgs {
41    /// Run `clippy` instead of `check`
42    #[arg(long)]
43    clippy: bool,
44
45    /// Fix code even if it already has compiler errors
46    #[arg(long)]
47    broken_code: bool,
48
49    /// Fix all targets together, risking stale suggestions
50    #[arg(long = "Zdangerous-parallel-fixes")]
51    dangerous_parallel_fixes: bool,
52
53    #[command(flatten)]
54    color: colorchoice_clap::Color,
55
56    #[command(flatten)]
57    vcs_opts: VcsOpts,
58
59    #[command(flatten)]
60    check_flags: CheckFlags,
61
62    #[arg(long, action = ArgAction::Count)]
63    verbose: u8,
64}
65
66impl FixitArgs {
67    pub fn exec(self) -> CargoResult<()> {
68        exec(self)
69    }
70
71    fn to_command(&self) -> Command {
72        let cmd = if self.clippy { "clippy" } else { "check" };
73        let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
74        let mut command = Command::new(cargo);
75        command.arg(cmd).args(self.check_flags.to_flags());
76        command
77    }
78}
79
80#[derive(Debug, Default)]
81struct ActiveState {
82    snapshots: IndexMap<String, File>,
83    iterations: usize,
84}
85
86#[derive(Debug, Default)]
87struct File {
88    fixes: u32,
89    original_source: String,
90}
91
92type BuildUnitErrors = IndexMap<UnitId, IndexSet<String>>;
93type BuildUnitSuggestions =
94    IndexMap<UnitId, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>;
95
96#[tracing::instrument(skip_all)]
97fn exec(args: FixitArgs) -> CargoResult<()> {
98    args.color.write_global();
99
100    args.vcs_opts.valid_vcs()?;
101
102    let mut active_units = IndexMap::new();
103    match fix(&args, &mut active_units) {
104        Ok(()) => Ok(()),
105        Err(error) => {
106            for (file, original) in active_units
107                .values()
108                .flat_map(|state| state.snapshots.iter())
109            {
110                paths::write(file, &original.original_source)?;
111            }
112            Err(error)
113        }
114    }
115}
116
117fn fix(args: &FixitArgs, active_units: &mut IndexMap<UnitId, ActiveState>) -> CargoResult<()> {
118    let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
119        .ok()
120        .and_then(|i| i.parse().ok())
121        .unwrap_or(4);
122    let package_metadata = package_metadata(&args.check_flags)?;
123    let primary_packages = PrimaryPackages::from_metadata(&package_metadata, &args.check_flags)?;
124    let mut plan = if args.dangerous_parallel_fixes {
125        UnitGraph::flat(&package_metadata)
126    } else {
127        UnitGraph::new(&package_metadata)
128    };
129    trace!("plan `{plan:#?}`");
130
131    let mut lint_cap = false;
132    let mut seen = BTreeSet::new();
133    let mut first = true;
134    let mut claimed_files: HashMap<same_file::Handle, UnitId> = HashMap::new();
135    loop {
136        trace!("check ({active_units:?})");
137        let mut check = Check::run(args, lint_cap)?;
138        let mut messages = Vec::new();
139        {
140            let mut errors = IndexMap::new();
141            for message in check.output() {
142                if first {
143                    match &message {
144                        CheckOutput::Message(Message {
145                            build_unit,
146                            message: MessageDiagnostic { diagnostic, .. },
147                        }) => {
148                            let package_id = &build_unit.package_id;
149                            let unit_id = UnitId::from_message(build_unit);
150                            if !is_local(package_id) || !plan.dependencies.contains_key(&unit_id) {
151                                if let Some(rendered) = diagnostic.rendered.clone() {
152                                    let errors =
153                                        errors.entry(unit_id).or_insert_with(IndexSet::new);
154                                    errors.insert(rendered);
155                                }
156                            }
157                        }
158                        CheckOutput::Artifact(a) => {
159                            let package_id = &a.build_unit.package_id;
160                            let unit_id = UnitId::from_message(&a.build_unit);
161                            if !is_local(package_id) || !plan.dependencies.contains_key(&unit_id) {
162                                for error in errors.get(&unit_id).into_iter().flatten() {
163                                    shell::print_ansi_stderr(
164                                        format!("{}\n\n", error.trim_end()).as_bytes(),
165                                    )?;
166                                }
167                                if !a.fresh && seen.insert(package_id.to_owned()) {
168                                    shell::status("Checking", format_package_id(package_id)?)?;
169                                }
170                            }
171                        }
172                    }
173                }
174                print_built(args, &message)?;
175                messages.push(message);
176            }
177            first = false;
178        }
179        let (mut diagnostics, mut exit_code) = check.wait()?;
180        if apply_lint_cap(&messages, exit_code, &mut lint_cap) {
181            let mut check = Check::run(args, lint_cap)?;
182            messages.clear();
183            for message in check.output() {
184                print_built(args, &message)?;
185                messages.push(message);
186            }
187            (diagnostics, exit_code) = check.wait()?;
188        }
189        messages.sort_unstable_by_key(|m| m.build_unit().cloned());
190
191        if messages.is_empty() && exit_code != Some(0) {
192            shell::print_ansi_stderr(&diagnostics)?;
193            anyhow::bail!("could not compile");
194        } else if !args.broken_code && exit_code != Some(0) {
195            let mut out = String::new();
196
197            if !active_units.is_empty() {
198                out.push_str(
199                    "failed to automatically apply fixes suggested by rustc\n\n\
200                    after fixes were automatically applied the \
201                    compiler reported errors within these files:\n\n",
202                );
203
204                for (
205                    file,
206                    File {
207                        fixes: _,
208                        original_source,
209                    },
210                ) in active_units
211                    .values()
212                    .flat_map(|state| state.snapshots.iter())
213                {
214                    out.push_str(&format!("  * {file}\n"));
215                    shell::note(format!("reverting `{file}` to its original state"))?;
216                    paths::write(file, original_source)?;
217                }
218                active_units.clear();
219                out.push('\n');
220
221                out.push_str(&gen_please_report_this_bug_text(args.clippy));
222
223                let mut errors = messages
224                    .into_iter()
225                    .filter_map(|e| match e {
226                        CheckOutput::Message(m) => m.message.diagnostic.rendered,
227                        _ => None,
228                    })
229                    .peekable();
230                if errors.peek().is_some() {
231                    out.push_str("The errors reported are:\n");
232                }
233
234                for e in errors {
235                    out.push_str(&format!("{}\n\n", e.trim_end()));
236                }
237
238                let mut check = Check::run(args, lint_cap)?;
239                let mut messages = Vec::new();
240                for message in check.output() {
241                    print_built(args, &message)?;
242                    messages.push(message);
243                }
244                let _ = check.wait()?;
245                let mut errors = messages
246                    .into_iter()
247                    .filter_map(|e| match e {
248                        CheckOutput::Message(m) => m.message.diagnostic.rendered,
249                        _ => None,
250                    })
251                    .peekable();
252
253                if errors.peek().is_some() {
254                    out.push_str("The original errors are:\n");
255                }
256                for e in errors {
257                    out.push_str(&format!("{}\n\n", e.trim_end()));
258                }
259                shell::warn(out)?;
260            } else {
261                for e in messages.into_iter().filter_map(|e| match e {
262                    CheckOutput::Message(m) => m.message.diagnostic.rendered,
263                    _ => None,
264                }) {
265                    shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
266                }
267            }
268
269            shell::note("try using `--broken-code` to fix errors")?;
270            anyhow::bail!("could not compile");
271        }
272
273        let observed_packages: HashSet<String> = messages
274            .iter()
275            .filter_map(CheckOutput::build_unit)
276            .map(|unit| unit.package_id.clone())
277            .collect();
278        let (mut errors, suggestions) = collect_diagnostics(
279            messages.into_iter(),
280            &plan.finished,
281            &primary_packages,
282            active_units,
283            max_iterations,
284        );
285
286        let mut finishing = true;
287        while finishing {
288            let mut finished = BTreeSet::new();
289            for unit_id in active_units.keys() {
290                if suggestions.contains_key(unit_id) {
291                    continue;
292                }
293                let errors = errors.shift_remove(unit_id);
294                finish_unit(unit_id, active_units, errors.as_ref())?;
295                finished.insert(unit_id.clone());
296            }
297            active_units.retain(|k, _v| !finished.contains(k));
298            claimed_files.retain(|_k, v| !finished.contains(v));
299            plan.mark_finished(finished);
300            finishing = false;
301            for unit_id in plan.take_ready() {
302                finishing = true;
303                trace!("scheduling `{unit_id:?}`");
304                let package_id = unit_id.package_id();
305                if observed_packages.contains(package_id) && seen.insert(package_id.to_owned()) {
306                    shell::status("Checking", format_package_id(package_id)?)?;
307                }
308                active_units.insert(unit_id, Default::default());
309            }
310        }
311        if active_units.is_empty() {
312            assert!(plan.is_empty(), "{plan:#?}");
313            break;
314        }
315
316        'units: for (unit_id, state) in active_units.iter_mut() {
317            let unit_suggestions = suggestions
318                .get(unit_id)
319                .expect("finished all active_units without suggestions");
320            for path in state.snapshots.keys().chain(unit_suggestions.keys()) {
321                let Ok(handle) = same_file::Handle::from_path(path) else {
322                    continue;
323                };
324                match claimed_files.entry(handle) {
325                    std::collections::hash_map::Entry::Occupied(entry)
326                        if entry.get() != unit_id =>
327                    {
328                        trace!("deferring `{unit_id:?}` due to contention over {path}");
329                        claimed_files.retain(|_k, v| v != unit_id);
330                        continue 'units;
331                    }
332                    std::collections::hash_map::Entry::Occupied(_) => {}
333                    std::collections::hash_map::Entry::Vacant(entry) => {
334                        entry.insert(unit_id.clone());
335                    }
336                }
337            }
338            trace!("fixing `{unit_id:?}` {state:?}");
339            state.iterations += 1;
340            let _made_changes = fix_suggestions(unit_suggestions, state)?;
341        }
342    }
343    Ok(())
344}
345
346/// Packages that Cargo treats as primary for the current invocation.
347#[derive(Debug)]
348struct PrimaryPackages {
349    package_ids: HashSet<String>,
350}
351
352impl PrimaryPackages {
353    /// Reconstructs Cargo's primary package set from its package and target selectors.
354    fn from_metadata(metadata: &Metadata, flags: &CheckFlags) -> CargoResult<Self> {
355        let mut package_ids = match flags.package_selection() {
356            PackageSelection::Default => metadata
357                .workspace_default_members
358                .iter()
359                .map(|package_id| package_id.repr.clone())
360                .collect(),
361            PackageSelection::Workspace { exclude } => {
362                let matcher = PackageSpecMatcher::new(exclude)?;
363                let mut package_ids = HashSet::new();
364                for package in metadata.workspace_packages() {
365                    if !matcher.matches(package)? {
366                        package_ids.insert(package.id.repr.clone());
367                    }
368                }
369                package_ids
370            }
371            PackageSelection::Packages(packages) => {
372                let matcher = PackageSpecMatcher::new(packages)?;
373                let mut package_ids = HashSet::new();
374                for package in metadata.workspace_packages() {
375                    if matcher.matches(package)? {
376                        package_ids.insert(package.id.repr.clone());
377                    }
378                }
379                package_ids
380            }
381        };
382
383        for package in metadata.workspace_packages() {
384            if package_ids.contains(&package.id.repr) && !flags.selects_package_targets(package)? {
385                package_ids.remove(&package.id.repr);
386            }
387        }
388
389        Ok(Self { package_ids })
390    }
391
392    fn contains(&self, package_id: &str) -> bool {
393        self.package_ids.contains(package_id)
394    }
395}
396
397/// Matches Cargo package specifications and package-name glob patterns.
398#[derive(Debug)]
399struct PackageSpecMatcher {
400    specs: Vec<PackageIdSpec>,
401    patterns: Vec<glob::Pattern>,
402}
403
404impl PackageSpecMatcher {
405    fn new(raw_specs: &[String]) -> CargoResult<Self> {
406        let mut specs = Vec::new();
407        let mut patterns = Vec::new();
408
409        for raw_spec in raw_specs {
410            match PackageIdSpec::parse(raw_spec) {
411                Ok(spec) => specs.push(spec),
412                Err(_) if raw_spec.contains(&['*', '?', '[', ']'][..]) => {
413                    let pattern = glob::Pattern::new(raw_spec)
414                        .with_context(|| format!("failed to parse package pattern `{raw_spec}`"))?;
415                    patterns.push(pattern);
416                }
417                Err(error) => {
418                    return Err(error).with_context(|| {
419                        format!("failed to parse package specification `{raw_spec}`")
420                    });
421                }
422            }
423        }
424
425        Ok(Self { specs, patterns })
426    }
427
428    fn matches(&self, package: &cargo_metadata::Package) -> CargoResult<bool> {
429        if self
430            .patterns
431            .iter()
432            .any(|pattern| pattern.matches(package.name.as_ref()))
433        {
434            return Ok(true);
435        }
436
437        let package_id = PackageIdSpec::parse(&package.id.repr)
438            .with_context(|| format!("failed to parse package ID `{}`", package.id))?;
439        Ok(self
440            .specs
441            .iter()
442            .any(|spec| package_id_matches(spec, &package_id)))
443    }
444}
445
446/// Mirrors Cargo's internal package-ID matching rules.
447fn package_id_matches(spec: &PackageIdSpec, package_id: &PackageIdSpec) -> bool {
448    spec.name() == package_id.name()
449        && spec.partial_version().is_none_or(|version| {
450            package_id
451                .version()
452                .is_some_and(|package_version| version.matches(&package_version))
453        })
454        && spec.url().is_none_or(|url| package_id.url() == Some(url))
455        && spec
456            .kind()
457            .is_none_or(|kind| package_id.kind() == Some(kind))
458}
459
460/// Loads unresolved package metadata once and reuses it for selection and ordering.
461fn package_metadata(flags: &CheckFlags) -> CargoResult<Metadata> {
462    let mut command = MetadataCommand::new();
463    command.no_deps();
464    command.other_options(flags.to_metadata_flags());
465    let metadata = command.exec().context("failed to run `cargo metadata`")?;
466    Ok(metadata)
467}
468
469fn finish_unit(
470    unit_id: &UnitId,
471    active_units: &IndexMap<UnitId, ActiveState>,
472    errors: Option<&IndexSet<String>>,
473) -> CargoResult<()> {
474    trace!("finishing build unit `{unit_id:?}`");
475    if let Some(state) = active_units.get(unit_id) {
476        for (name, file) in &state.snapshots {
477            shell::fixed(name, file.fixes)?;
478        }
479    }
480
481    for error in errors.into_iter().flatten() {
482        shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
483    }
484
485    Ok(())
486}
487
488struct Check {
489    child: Child,
490    diagnostics: JoinHandle<std::io::Result<Vec<u8>>>,
491}
492
493impl Check {
494    fn run(args: &FixitArgs, lint_cap: bool) -> CargoResult<Self> {
495        let mut command = args.to_command();
496        command.args(["--message-format", "json-diagnostic-rendered-ansi"]);
497        if lint_cap {
498            cap_lints(&mut command);
499        }
500        let mut child = command
501            .stderr(Stdio::piped())
502            .stdout(Stdio::piped())
503            .spawn()?;
504        let mut stderr = child.stderr.take().expect("stderr is piped");
505        // Drain stderr while callers read stdout so neither pipe can block Cargo.
506        let diagnostics = std::thread::spawn(move || {
507            let mut diagnostics = Vec::new();
508            stderr.read_to_end(&mut diagnostics)?;
509            Ok(diagnostics)
510        });
511        Ok(Self { child, diagnostics })
512    }
513
514    fn output(&mut self) -> impl Iterator<Item = CheckOutput> {
515        let stdout = self.child.stdout.take().expect("stdout is piped");
516        BufReader::new(stdout)
517            .lines()
518            .map_while(|line| line.ok())
519            .filter_map(|line| serde_json::from_str(&line).ok())
520    }
521
522    fn wait(self) -> CargoResult<(Vec<u8>, Option<i32>)> {
523        let output = self.child.wait_with_output()?;
524        let diagnostics = self
525            .diagnostics
526            .join()
527            .map_err(|_| anyhow::anyhow!("failed to read cargo diagnostics: thread panicked"))??;
528        Ok((diagnostics, output.status.code()))
529    }
530}
531
532fn apply_lint_cap(output: &[CheckOutput], status: Option<i32>, lint_cap: &mut bool) -> bool {
533    if !*lint_cap && status != Some(0) && !*lint_cap && denied_lint(output) {
534        *lint_cap = true;
535    }
536
537    *lint_cap
538}
539
540fn print_built(args: &FixitArgs, message: &CheckOutput) -> CargoResult<()> {
541    if args.verbose == 0 {
542        return Ok(());
543    }
544
545    match message {
546        CheckOutput::Message(_) => {}
547        CheckOutput::Artifact(a) => {
548            if !a.fresh {
549                let pkg_id = format_package_id(&a.build_unit.package_id)?;
550                let name = &a.build_unit.target.name;
551                let kind = &a.build_unit.target.kind;
552                let kind = if 1 < kind.len() {
553                    "lib" // HACK: if its multiple, it is only a lib
554                } else {
555                    match &kind[0] {
556                        TargetKind::Bin => "bin",
557                        TargetKind::Test => "test",
558                        TargetKind::Bench => "bench",
559                        TargetKind::Example => "example",
560                        TargetKind::CustomBuild => "custom-build",
561                        TargetKind::Lib(_) => "lib",
562                    }
563                };
564                shell::status("Checked", format!("{pkg_id} - {name} ({kind})"))?;
565            }
566        }
567    }
568
569    Ok(())
570}
571
572/// Applies the original lint cap while preserving existing compiler flags.
573fn cap_lints(command: &mut Command) {
574    if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
575        let separator = if flags.is_empty() { "" } else { "\u{1f}" };
576        command.env(
577            "CARGO_ENCODED_RUSTFLAGS",
578            format!("{flags}{separator}--cap-lints=warn"),
579        );
580    } else {
581        command.env(
582            "RUSTFLAGS",
583            format!(
584                "--cap-lints=warn {}",
585                env::var("RUSTFLAGS").unwrap_or("".to_owned())
586            ),
587        );
588    }
589}
590
591fn denied_lint(messages: &[CheckOutput]) -> bool {
592    messages.iter().any(|message| {
593        matches!(&message, CheckOutput::Message(message)
594                if message.message.level == DiagnosticLevel::Error
595                    && message.message.diagnostic.code.is_some())
596    })
597}
598
599#[tracing::instrument(skip_all)]
600fn collect_diagnostics(
601    messages: impl Iterator<Item = CheckOutput>,
602    finished: &BTreeSet<UnitId>,
603    primary_packages: &PrimaryPackages,
604    active_units: &mut IndexMap<UnitId, ActiveState>,
605    max_iterations: usize,
606) -> (BuildUnitErrors, BuildUnitSuggestions) {
607    let only = HashSet::new();
608
609    let mut suggestions = IndexMap::new();
610    let mut errors = IndexMap::new();
611
612    for message in messages {
613        let Message {
614            build_unit,
615            message: MessageDiagnostic { diagnostic, .. },
616        } = match message {
617            CheckOutput::Message(m) => m,
618            CheckOutput::Artifact(a) => {
619                let unit_id = UnitId::from_message(&a.build_unit);
620                errors.entry(unit_id).or_insert_with(IndexSet::new);
621                continue;
622            }
623        };
624
625        let unit_id = UnitId::from_message(&build_unit);
626        if finished.contains(&unit_id) {
627            trace!("rejecting build unit `{:?}` already finished", build_unit);
628            continue;
629        }
630
631        if let Some(state) = active_units.get_mut(&unit_id) {
632            if state.iterations >= max_iterations {
633                trace!(
634                    "rejecting build unit `{:?}` exceeded max iteration count",
635                    build_unit
636                );
637                let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
638                if let Some(rendered) = diagnostic.rendered {
639                    errors.insert(rendered);
640                }
641                continue;
642            }
643        }
644
645        if !primary_packages.contains(&build_unit.package_id) {
646            trace!(
647                "rejecting build unit `{:?}` not selected by the user",
648                build_unit
649            );
650            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
651            if let Some(rendered) = diagnostic.rendered {
652                errors.insert(rendered);
653            }
654            continue;
655        }
656
657        let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
658            rustfix::Filter::Everything
659        } else {
660            rustfix::Filter::MachineApplicableOnly
661        };
662        let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
663            trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
664            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
665            if let Some(rendered) = diagnostic.rendered {
666                errors.insert(rendered);
667            }
668            continue;
669        };
670
671        let mut file_names = suggestion
672            .solutions
673            .iter()
674            .flat_map(|s| s.replacements.iter())
675            .map(|r| &r.snippet.file_name);
676
677        let Some(file_name) = file_names.next() else {
678            trace!("rejecting as it has no solutions {:?}", suggestion);
679            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
680            if let Some(rendered) = diagnostic.rendered {
681                errors.insert(rendered);
682            }
683            continue;
684        };
685
686        if !file_names.all(|f| f == file_name) {
687            trace!("rejecting as it changes multiple files: {:?}", suggestion);
688            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
689            if let Some(rendered) = diagnostic.rendered {
690                errors.insert(rendered);
691            }
692            continue;
693        }
694
695        let file_path = Path::new(&file_name);
696        // Do not write into registry cache. See rust-lang/cargo#9857.
697        if let Ok(home) = env::var("CARGO_HOME") {
698            if file_path.starts_with(home) {
699                let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
700                if let Some(rendered) = diagnostic.rendered {
701                    errors.insert(rendered);
702                }
703                continue;
704            }
705        }
706
707        if file_path.is_absolute() {
708            if let Some(sysroot) = get_sysroot() {
709                if file_path.starts_with(sysroot) {
710                    let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
711                    if let Some(rendered) = diagnostic.rendered {
712                        errors.insert(rendered);
713                    }
714                    continue;
715                }
716            }
717        }
718
719        let unit_suggestions = suggestions
720            .entry(unit_id.clone())
721            .or_insert(IndexMap::new());
722        unit_suggestions
723            .entry(file_name.to_owned())
724            .or_insert_with(IndexSet::new)
725            .insert((suggestion, diagnostic.rendered));
726    }
727
728    (errors, suggestions)
729}
730
731#[tracing::instrument(skip_all)]
732fn fix_suggestions(
733    unit_suggestions: &IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
734    state: &mut ActiveState,
735) -> CargoResult<bool> {
736    let mut made_changes = false;
737    for (file, suggestions) in unit_suggestions {
738        let source = match paths::read(file.as_ref()) {
739            Ok(s) => s,
740            Err(e) => {
741                warn!("failed to read `{}`: {}", file, e);
742                continue;
743            }
744        };
745
746        let mut fixed = CodeFix::new(&source);
747        let mut num_fixes = 0;
748
749        for (suggestion, _rendered) in suggestions.iter().rev() {
750            match fixed.apply(suggestion) {
751                Ok(()) => num_fixes += 1,
752                Err(rustfix::Error::AlreadyReplaced {
753                    is_identical: true, ..
754                }) => {}
755                Err(e) => {
756                    warn!("{e:?}");
757                }
758            }
759        }
760        if fixed.modified() {
761            let new_source = fixed.finish()?;
762            let file_state = state.snapshots.entry(file.clone()).or_insert(File {
763                fixes: 0,
764                original_source: source,
765            });
766            paths::write(file, new_source)?;
767            made_changes = true;
768            file_state.fixes += num_fixes;
769        }
770    }
771
772    Ok(made_changes)
773}
774
775#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
776struct UnitId {
777    inner: std::sync::Arc<UnitIdInner>,
778}
779
780#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
781struct UnitIdInner {
782    // HACK: this should also track
783    // - test-mode or not
784    // - platform
785    // - host vs target mode
786    package_id: String,
787    target_kind: TargetKind,
788}
789
790impl UnitId {
791    fn from_message(build_unit: &BuildUnit) -> Self {
792        // HACK: just collapse all libs to one kind since we can't distinguish them
793        let target_kind = build_unit
794            .target
795            .kind
796            .first()
797            .expect("build unit targets have at least one kind");
798        let target_kind = match target_kind {
799            TargetKind::Lib(_) => TargetKind::Lib(CrateType::Lib),
800            target_kind => target_kind.clone(),
801        };
802
803        Self {
804            inner: std::sync::Arc::new(UnitIdInner {
805                package_id: build_unit.package_id.clone(),
806                target_kind,
807            }),
808        }
809    }
810
811    fn from_metadata(
812        package: &cargo_metadata::Package,
813        target_kind: &cargo_metadata::TargetKind,
814    ) -> Self {
815        let target_kind = match target_kind {
816            cargo_metadata::TargetKind::Bin => TargetKind::Bin,
817            cargo_metadata::TargetKind::Test => TargetKind::Test,
818            cargo_metadata::TargetKind::Bench => TargetKind::Bench,
819            cargo_metadata::TargetKind::Example => TargetKind::Example,
820            cargo_metadata::TargetKind::CustomBuild => TargetKind::CustomBuild,
821            // HACK: just collapse all libs to one kind since we can't distinguish them
822            cargo_metadata::TargetKind::Lib
823            | cargo_metadata::TargetKind::RLib
824            | cargo_metadata::TargetKind::DyLib
825            | cargo_metadata::TargetKind::CDyLib
826            | cargo_metadata::TargetKind::StaticLib
827            | cargo_metadata::TargetKind::ProcMacro => TargetKind::Lib(CrateType::Lib),
828            target_kind => TargetKind::Lib(CrateType::Other(target_kind.to_string())),
829        };
830
831        Self {
832            inner: std::sync::Arc::new(UnitIdInner {
833                package_id: package.id.repr.clone(),
834                target_kind,
835            }),
836        }
837    }
838
839    fn package_id(&self) -> &str {
840        &self.inner.package_id
841    }
842
843    fn target_kind(&self) -> &TargetKind {
844        &self.inner.target_kind
845    }
846}
847
848#[derive(Debug)]
849struct UnitGraph {
850    dependencies: BTreeMap<UnitId, BTreeSet<UnitId>>,
851    finished: BTreeSet<UnitId>,
852}
853
854impl UnitGraph {
855    fn flat(metadata: &Metadata) -> Self {
856        let mut dependencies = BTreeMap::default();
857        for package in &metadata.packages {
858            for target in &package.targets {
859                for kind in &target.kind {
860                    let unit_id = UnitId::from_metadata(package, kind);
861                    dependencies.insert(unit_id, Default::default());
862                }
863            }
864        }
865
866        Self {
867            dependencies,
868            finished: Default::default(),
869        }
870    }
871
872    fn new(metadata: &Metadata) -> Self {
873        let mut dependencies = BTreeMap::default();
874        let mut path_to_lib_unit_ids = BTreeMap::default();
875        for package in &metadata.packages {
876            let mut build_script_unit_id = None;
877            let mut lib_unit_ids = BTreeSet::new();
878            let mut other_unit_ids = BTreeSet::new();
879            for target in &package.targets {
880                for kind in &target.kind {
881                    let unit_id = UnitId::from_metadata(package, kind);
882                    if matches!(unit_id.target_kind(), TargetKind::CustomBuild) {
883                        build_script_unit_id = Some(unit_id);
884                    } else if matches!(unit_id.target_kind(), TargetKind::Lib(_)) {
885                        lib_unit_ids.insert(unit_id);
886                    } else {
887                        other_unit_ids.insert(unit_id);
888                    }
889                }
890            }
891
892            for unit_id in other_unit_ids {
893                let deps = if !lib_unit_ids.is_empty() {
894                    lib_unit_ids.clone()
895                } else {
896                    build_script_unit_id.clone().into_iter().collect()
897                };
898                dependencies.insert(unit_id, deps);
899            }
900            if !lib_unit_ids.is_empty() {
901                let path_source = manifest_path_to_dep_path(&package.manifest_path);
902                path_to_lib_unit_ids.insert(path_source.to_owned(), lib_unit_ids.clone());
903                for unit_id in lib_unit_ids {
904                    let deps = build_script_unit_id.clone().into_iter().collect();
905                    dependencies.insert(unit_id, deps);
906                }
907            }
908            if let Some(unit_id) = build_script_unit_id {
909                dependencies.insert(unit_id, Default::default());
910            }
911        }
912
913        for package in &metadata.packages {
914            for dependency in &package.dependencies {
915                let Some(dep_path) = &dependency.path else {
916                    continue;
917                };
918                let Some(dep_unit_ids) = path_to_lib_unit_ids.get(dep_path) else {
919                    continue;
920                };
921                for target in &package.targets {
922                    for kind in &target.kind {
923                        let unit_id = UnitId::from_metadata(package, kind);
924                        let applies = match (&unit_id.target_kind(), &dependency.kind) {
925                            (TargetKind::CustomBuild, cargo_metadata::DependencyKind::Build) => {
926                                true
927                            }
928                            (TargetKind::Lib(_), cargo_metadata::DependencyKind::Normal) => true,
929                            (TargetKind::Lib(_), cargo_metadata::DependencyKind::Development) => {
930                                // HACK: should include for unit test variant except that would
931                                // cause cycles
932                                false
933                            }
934                            (TargetKind::Bin, cargo_metadata::DependencyKind::Normal) => true,
935                            (TargetKind::Bin, cargo_metadata::DependencyKind::Development) => {
936                                // HACK: for the unit test variant
937                                true
938                            }
939                            (TargetKind::Test, cargo_metadata::DependencyKind::Normal) => true,
940                            (TargetKind::Test, cargo_metadata::DependencyKind::Development) => true,
941                            (TargetKind::Bench, cargo_metadata::DependencyKind::Normal) => true,
942                            (TargetKind::Bench, cargo_metadata::DependencyKind::Development) => {
943                                true
944                            }
945                            (TargetKind::Example, cargo_metadata::DependencyKind::Normal) => true,
946                            (TargetKind::Example, cargo_metadata::DependencyKind::Development) => {
947                                true
948                            }
949                            _ => false,
950                        };
951                        if applies {
952                            dependencies
953                                .entry(unit_id)
954                                .or_default()
955                                .extend(dep_unit_ids.clone());
956                        }
957                    }
958                }
959            }
960        }
961
962        Self {
963            dependencies,
964            finished: Default::default(),
965        }
966    }
967
968    fn is_empty(&self) -> bool {
969        self.dependencies.is_empty()
970    }
971
972    fn take_ready(&mut self) -> BTreeSet<UnitId> {
973        self.dependencies
974            .extract_if(.., |_k, v| v.is_empty())
975            .map(|(k, _v)| k)
976            .collect()
977    }
978
979    fn mark_finished(&mut self, finished: BTreeSet<UnitId>) {
980        for dependencies in self.dependencies.values_mut() {
981            dependencies.retain(|id| !finished.contains(id));
982        }
983        self.finished.extend(finished);
984    }
985}
986
987fn manifest_path_to_dep_path(manifest_path: &camino::Utf8Path) -> &camino::Utf8Path {
988    if manifest_path.ends_with("Cargo.toml") {
989        manifest_path.parent().unwrap()
990    } else {
991        manifest_path
992    }
993}
994
995fn is_local(package_id: &str) -> bool {
996    package_id.starts_with("path+")
997}