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