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 #[arg(long)]
43 clippy: bool,
44
45 #[arg(long)]
47 broken_code: bool,
48
49 #[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#[derive(Debug)]
348struct PrimaryPackages {
349 package_ids: HashSet<String>,
350}
351
352impl PrimaryPackages {
353 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#[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
446fn 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
460fn 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 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" } 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
572fn 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 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 package_id: String,
787 target_kind: TargetKind,
788}
789
790impl UnitId {
791 fn from_message(build_unit: &BuildUnit) -> Self {
792 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 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 false
933 }
934 (TargetKind::Bin, cargo_metadata::DependencyKind::Normal) => true,
935 (TargetKind::Bin, cargo_metadata::DependencyKind::Development) => {
936 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}