1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::ffi::OsString;
3use std::fmt::Write as _;
4use std::io::{self, Write};
5use std::ops::{Deref, Index, IndexMut};
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8use std::sync::Arc;
9use std::sync::Mutex;
10use std::{env, fmt};
11
12use nu_ansi_term::Color;
13use cargo::core::compiler::{CompileMode, UserIntent, DefaultExecutor, Executor, RustcTargetData, Unit};
14use cargo::core::resolver::HasDevUnits;
15use cargo::core::resolver::features::{ForceAllTargets, CliFeatures};
16use cargo::core::manifest::Target;
17use cargo::core::package_id::PackageId;
18use cargo::core::shell::Shell;
19use cargo::core::{dependency, Package, Resolve, Workspace, Verbosity};
20use cargo::ops::Packages;
21use cargo::util::command_prelude::{ArgMatchesExt, ProfileChecking};
22use cargo::util::context::GlobalContext;
23use cargo::util::interning::InternedString;
24use cargo_util::ProcessBuilder;
25use cargo::{CargoResult, CliError, CliResult};
26use serde::{Deserialize, Serialize};
27use clap::{ArgAction, ArgMatches, CommandFactory, Parser};
28
29pub fn run<I: IntoIterator<Item = OsString>, W: Write>(args :I, config :&mut GlobalContext, stdout: W) -> CliResult {
30 let args = args.into_iter().collect::<Vec<_>>();
31 let Opt::Udeps(opt) = Opt::try_parse_from(&args)?;
32 let clap_matches = Opt::command().try_get_matches_from(args)?;
33 match opt.run(config, stdout, clap_matches.subcommand_matches("udeps").unwrap())? {
34 0 => Ok(()),
35 code => Err(CliError::code(code)),
36 }
37}
38
39#[derive(Parser, Debug)]
40#[command(
41 about,
42 name = "cargo",
43 bin_name = "cargo",
44)]
45enum Opt {
46 #[command(
47 about,
48 version,
49 name = "udeps",
50 after_help(
51 "\
52If the `--package` argument is given, then SPEC is a package ID specification
53which indicates which package should be built. If it is not given, then the
54current package is built. For more information on SPEC and its format, see the
55`cargo help pkgid` command.
56
57All packages in the workspace are checked if the `--workspace` flag is supplied. The
58`--workspace` flag is automatically assumed for a virtual manifest.
59Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.
60
61Compilation can be configured via the use of profiles which are configured in
62the manifest. The default profile for this command is `dev`, but passing
63the `--release` flag will use the `release` profile instead.
64
65The `--profile test` flag can be used to check unit tests with the
66`#[cfg(test)]` attribute."
67 )
68 )]
69 Udeps(OptUdeps),
70}
71
72#[derive(Parser, Debug)]
73#[allow(dead_code)]
74struct OptUdeps {
75 #[arg(short, long, help("[cargo] No output printed to stdout"), value_parser = clap::value_parser!(bool))]
76 quiet: bool,
77 #[arg(
78 short,
79 long,
80 value_name("SPEC"),
81 num_args(1..),
82 number_of_values(1),
83 help("[cargo] Package(s) to check")
84 )]
85 package: Vec<String>,
86 #[arg(long, help("[cargo] Alias for --workspace (deprecated)"), value_parser = clap::value_parser!(bool))]
87 all: bool,
88 #[arg(long, help("[cargo] Check all packages in the workspace"), value_parser = clap::value_parser!(bool))]
89 workspace: bool,
90 #[arg(
91 long,
92 value_name("SPEC"),
93 num_args(1..),
94 number_of_values(1),
95 help("[cargo] Exclude packages from the check")
96 )]
97 exclude: Vec<String>,
98 #[arg(
99 short,
100 long,
101 value_name("N"),
102 help("[cargo] Number of parallel jobs, defaults to # of CPUs")
103 )]
104 jobs: Option<String>,
105 #[arg(long, help("[cargo] Check only this package's library"), value_parser = clap::value_parser!(bool))]
106 lib: bool,
107 #[arg(
108 long,
109 value_name("NAME"),
110 num_args(0..),
111 number_of_values(1),
112 help("[cargo] Check only the specified binary")
113 )]
114 bin: Vec<String>,
115 #[arg(long, help("[cargo] Check all binaries"), value_parser = clap::value_parser!(bool))]
116 bins: bool,
117 #[arg(
118 long,
119 value_name("NAME"),
120 num_args(0..),
121 number_of_values(1),
122 help("[cargo] Check only the specified example")
123 )]
124 example: Vec<String>,
125 #[arg(long, help("[cargo] Check all examples"), value_parser = clap::value_parser!(bool))]
126 examples: bool,
127 #[arg(
128 long,
129 value_name("NAME"),
130 num_args(0..),
131 number_of_values(1),
132 help("[cargo] Check only the specified test target")
133 )]
134 test: Vec<String>,
135 #[arg(long, help("[cargo] Check all tests"), value_parser = clap::value_parser!(bool))]
136 tests: bool,
137 #[arg(
138 long,
139 value_name("NAME"),
140 num_args(0..),
141 number_of_values(1),
142 help("[cargo] Check only the specified bench target")
143 )]
144 bench: Vec<String>,
145 #[arg(long, help("[cargo] Check all benches"), value_parser = clap::value_parser!(bool))]
146 benches: bool,
147 #[arg(long, help("[cargo] Check all targets"), id = "all-targets", value_parser = clap::value_parser!(bool))]
148 all_targets: bool,
149 #[arg(long, help("[cargo] Check artifacts in release mode, with optimizations"), value_parser = clap::value_parser!(bool))]
150 release: bool,
151 #[arg(
152 long,
153 value_name("PROFILE-NAME"),
154 help("[cargo] Check artifacts with the specified profile")
155 )]
156 profile: Option<String>,
157 #[arg(
158 long,
159 value_name("FEATURES"),
160 num_args(0..),
161 help("[cargo] Space-separated list of features to activate")
162 )]
163 features: Vec<String>,
164 #[arg(long, help("[cargo] Activate all available features"), id = "all-features", value_parser = clap::value_parser!(bool))]
165 all_features: bool,
166 #[arg(long, help("[cargo] Do not activate the `default` feature"), id = "no-default-features", value_parser = clap::value_parser!(bool))]
167 no_default_features: bool,
168 #[arg(long, value_name("TRIPLE"), help("[cargo] Check for the target triple"))]
169 target: Option<String>,
170 #[arg(
171 long,
172 value_name("DIRECTORY"),
173 help("[cargo] Directory for all generated artifacts")
174 )]
175 target_dir: Option<PathBuf>,
176 #[arg(long, value_name("PATH"), id = "manifest-path", help("[cargo] Path to Cargo.toml"))]
177 manifest_path: Option<String>,
178 #[arg(
179 long,
180 value_name("FMT"),
181 id = "message-format",
182 ignore_case(true),
183 value_parser(["human", "json", "short"]),
184 default_value("human"),
185 help("[cargo] Error format")
186 )]
187 message_format: Vec<String>,
188 #[arg(
189 short,
190 long,
191 action = ArgAction::Count,
192 help("[cargo] Use verbose output (-vv very verbose/build.rs output)")
193 )]
194 verbose: u8,
195 #[arg(
196 long,
197 value_name("WHEN"),
198 ignore_case(false),
199 value_parser(["auto", "always", "never"]),
200 help("[cargo] Coloring")
201 )]
202 color: Option<String>,
203 #[arg(long, help("[cargo] Require Cargo.lock and cache are up to date"), value_parser = clap::value_parser!(bool))]
204 frozen: bool,
205 #[arg(long, help("[cargo] Require Cargo.lock is up to date"), value_parser = clap::value_parser!(bool))]
206 locked: bool,
207 #[arg(long, help("[cargo] Run without accessing the network"), value_parser = clap::value_parser!(bool))]
208 offline: bool,
209 #[arg(
210 long,
211 value_name("OUTPUT"),
212 default_value("human"),
213 value_enum,
214 help("Output format"))
215 ]
216 output: OutputKind,
217 #[arg(
218 long,
219 value_name("BACKEND"),
220 default_value("depinfo"),
221 value_enum,
222 help("Backend to use for determining unused deps"))
223 ]
224 backend :Backend,
225 #[arg(
226 long,
227 id = "keep-going",
228 help("Needed because the keep-going flag is asked about by cargo code"),
229 value_parser = clap::value_parser!(bool),
230 )]
231 keep_going :bool,
232}
233
234impl OptUdeps {
235 fn run<W: Write>(
236 &self,
237 config :&mut GlobalContext,
238 stdout :W,
239 clap_matches :&ArgMatches
240 ) -> CargoResult<i32> {
241 if self.verbose > 0 {
242 let mut shell = config.shell();
243 shell.warn(
244 "currently verbose command information (\"Running `..`\") are not correct.",
245 )?;
246 shell.warn("for example, `cargo-udeps` does these modifications:")?;
247 shell.warn("- changes `$CARGO` to the value given from `cargo`")?;
248 }
249
250 config.configure(
251 self.verbose.max(0).min(2) as u32,
252 self.quiet,
253 self.color.as_deref(),
254 self.frozen,
255 self.locked,
256 self.offline,
257 &self.target_dir,
258 &["binary-dep-depinfo".to_string()],
259 &[],
260 )?;
261 assert!(config.nightly_features_allowed);
262 let ws = clap_matches.workspace(config)?;
263 let test = match self.profile.as_deref() {
264 None => false,
265 Some("test") => true,
266 Some(profile) => return Err(anyhow::anyhow!(
267 "unknown profile: `{}`, only `test` is currently supported",
268 profile,
269 )),
270 };
271 let mode = UserIntent::Check { test };
272 let pc = ProfileChecking::LegacyTestOnly;
273 let compile_opts = clap_matches.compile_options(config, mode, Some(&ws), pc)?;
274 let requested_kinds = &compile_opts.build_config.requested_kinds;
275 let mut target_data = RustcTargetData::new(&ws, requested_kinds)?;
276
277 let cli_features = CliFeatures::from_command_line(
278 &self.features,
279 self.all_features,
280 !self.no_default_features,
281 )?;
282 let dry_run = false;
283 let ws_resolve = cargo::ops::resolve_ws_with_opts(
284 &ws,
285 &mut target_data,
286 requested_kinds,
287 &cli_features,
288 &Packages::All(Vec::new()).to_package_id_specs(&ws)?,
289 HasDevUnits::Yes,
290 ForceAllTargets::No,
291 dry_run,
292 )?;
293
294 let packages = ws_resolve.pkg_set
295 .get_many(ws_resolve.pkg_set.package_ids())?
296 .into_iter()
297 .map(|p| (p.package_id(), p))
298 .collect::<HashMap<_, _>>();
299
300 let dependency_names = ws
301 .members()
302 .map(|from| {
303 let val = DependencyNames::new(from, &packages, &ws_resolve.targeted_resolve, &mut config.shell())?;
304 let key = from.package_id();
305 Ok((key, val))
306 })
307 .collect::<CargoResult<HashMap<_, _>>>()?;
308
309 let data = Arc::new(Mutex::new(ExecData::new(&ws)?));
310 let exec :Arc<dyn Executor + 'static> = Arc::new(Exec { data : data.clone() });
311 cargo::ops::compile_with_exec(&ws, &compile_opts, &exec)?;
312 let data = data.lock().unwrap();
313
314 let mut used_normal_dev_dependencies = HashSet::new();
315 let mut used_build_dependencies = HashSet::new();
316 let mut normal_dependencies = dependency_names
317 .iter()
318 .flat_map(|(&m, d)| d[dependency::DepKind::Normal].non_lib.iter().map(move |&s| (m, s)))
319 .collect::<HashSet<_>>();
320 let mut dev_dependencies = dependency_names
321 .iter()
322 .flat_map(|(&m, d)| d[dependency::DepKind::Development].non_lib.iter().map(move |&s| (m, s)))
323 .collect::<HashSet<_>>();
324 let mut build_dependencies = dependency_names
325 .iter()
326 .flat_map(|(&m, d)| d[dependency::DepKind::Build].non_lib.iter().map(move |&s| (m, s)))
327 .collect::<HashSet<_>>();
328
329 let mut lib_stem_to_pkg_id = HashMap::new();
330 for cmd_info in data.all_cmd_infos.iter() {
331 let lib_stem = cmd_info.get_artifact_base_name();
332 lib_stem_to_pkg_id.insert(lib_stem, cmd_info.pkg);
334 }
335 enum BackendData {
336 Depinfo(DepInfo),
337 }
338 for cmd_info in data.relevant_cmd_infos.iter() {
339 let backend_data = match self.backend {
340 Backend::Depinfo => {
341 let depinfo = cmd_info.get_depinfo(&mut config.shell())?;
342 BackendData::Depinfo(depinfo)
343 },
344 };
345 if let Some(dependency_names) = dependency_names.get(&cmd_info.pkg) {
347 let collect_names = |
348 dnv :&DependencyNamesValue,
349 used_dependencies: &mut HashSet<(PackageId, InternedString)>,
350 dependencies: &mut HashSet<(PackageId, InternedString)>,
351 | {
352 match &backend_data {
353 BackendData::Depinfo(depinfo) => for dep in depinfo.deps_of_depfile() {
354 let fs = if let Some(fs) = dep.file_stem() {
355 fs
356 } else {
357 continue
358 };
359 let fs :String = match fs.to_str() {
360 Some(v) => v.to_string(),
361 _ => continue,
362 };
363 let lib_name = match fs.split_once('-') {
372 None => continue,
373 Some((lib_name, _)) => lib_name
374 };
375
376 if let Some(pkg_id) = lib_stem_to_pkg_id.get(&fs) {
387 if let Some(dependency_name) = dnv.by_package_id.get(pkg_id) {
388 used_dependencies.insert((cmd_info.pkg, *dependency_name));
389 }
390 } else {
391 let lib_name = lib_name.strip_prefix("lib").unwrap_or(lib_name);
395 if let Some(dependency_names) = dnv.by_lib_true_snakecased_name.get(lib_name) {
396 for dependency_name in dependency_names {
397 used_dependencies.insert((cmd_info.pkg, *dependency_name));
398 }
399 }
400 }
401 },
402 }
403
404 for extern_crate_name in &cmd_info.extern_crate_names {
405 if let Some(dependency_name) = dnv.by_extern_crate_name.get(&**extern_crate_name) {
409 dependencies.insert((cmd_info.pkg, *dependency_name));
410 }
411 }
412 };
413
414 collect_names(
415 &dependency_names.normal,
416 &mut used_normal_dev_dependencies,
417 &mut normal_dependencies,
418 );
419 collect_names(
420 &dependency_names.development,
421 &mut used_normal_dev_dependencies,
422 &mut dev_dependencies,
423 );
424 collect_names(
425 &dependency_names.build,
426 &mut used_build_dependencies,
427 &mut build_dependencies,
428 );
429 }
430 }
431
432 use anyhow::Context;
433 let workspace_ignore = ws
434 .custom_metadata()
435 .map::<CargoResult<_>, _>(|workspace_metadata| {
436 let PackageMetadata {
437 cargo_udeps: PackageMetadataCargoUdeps { ignore },
438 } = workspace_metadata
439 .clone()
440 .try_into()
441 .context("could not parse `workspace.metadata.cargo-udeps`")?;
442 Ok(ignore)
443 })
444 .transpose()?;
445
446 let mut outcome = Outcome::default();
447
448 let included_packages = compile_opts.spec.get_packages(&ws)?
449 .iter()
450 .map(|x|x.package_id())
451 .collect::<HashSet<_>>();
452 for (dependencies, used_dependencies, kind) in &[
453 (&normal_dependencies, &used_normal_dev_dependencies, dependency::DepKind::Normal),
454 (&dev_dependencies, &used_normal_dev_dependencies, dependency::DepKind::Development),
455 (&build_dependencies, &used_build_dependencies, dependency::DepKind::Build),
456 ] {
457 for &(id, dependency) in *dependencies {
458 if !included_packages.contains(&id) {
460 continue;
461 }
462
463 let ignore = ws_resolve
464 .pkg_set
465 .get_one(id)?
466 .manifest()
467 .custom_metadata()
468 .map::<CargoResult<_>, _>(|package_metadata| {
469 let PackageMetadata {
470 cargo_udeps: PackageMetadataCargoUdeps { ignore },
471 } = package_metadata
472 .clone()
473 .try_into()
474 .context("could not parse `package.metadata.cargo-udeps`")?;
475 Ok(ignore)
476 })
477 .transpose()?;
478
479 if !used_dependencies.contains(&(id, dependency)) {
480 if ignore.map_or(false, |ignore| ignore.contains(*kind, dependency)) ||
481 workspace_ignore.as_ref().map_or(false, |ignore| ignore.contains(*kind, dependency))
482 {
483 config.shell().info(format_args!("Ignoring `{}` ({:?})", dependency, kind))?;
484 } else {
485 outcome
486 .unused_deps
487 .entry(id)
488 .or_insert(OutcomeUnusedDeps::new(packages[&id].manifest_path())?)
489 .unused_deps_mut(*kind)
490 .insert(dependency);
491 }
492 }
493 }
494 }
495
496 outcome.success = outcome
497 .unused_deps
498 .values()
499 .all(|OutcomeUnusedDeps { normal, development, build, .. }| {
500 normal.is_empty() && development.is_empty() && build.is_empty()
501 });
502
503 if !outcome.success {
504 let mut note = "".to_owned();
505
506 if !self.all_targets {
507 note += "Note: These dependencies might be used by other targets.\n";
508
509 if !self.lib
510 && !self.bins
511 && !self.examples
512 && !self.tests
513 && !self.benches
514 && self.bin.is_empty()
515 && self.example.is_empty()
516 && self.test.is_empty()
517 && self.bench.is_empty()
518 {
519 note += " To find dependencies that are not used by any target, enable `--all-targets`.\n";
520 }
521 }
522
523 if dependency_names.values().any(DependencyNames::has_non_lib) {
524 note += "Note: Some dependencies are non-library packages.\n";
525 note += " `cargo-udeps` regards them as unused.\n";
526 }
527
528 note += "Note: They might be false-positive.\n";
529 note += " For example, `cargo-udeps` cannot detect usage of crates that are only used in doc-tests.\n";
530 note += " To ignore some dependencies, write `package.metadata.cargo-udeps.ignore` in Cargo.toml.\n";
531
532 outcome.note = Some(note);
533 }
534
535 outcome.print(self.output, stdout)?;
536 Ok(if outcome.success { 0 } else { 1 })
537 }
538}
539
540struct ExecData {
541 cargo_exe :OsString,
542 supports_color :bool,
543 workspace_members :Vec<PackageId>,
544 relevant_cmd_infos :Vec<CmdInfo>,
545 all_cmd_infos :Vec<CmdInfo>,
546}
547
548impl ExecData {
549 fn new(ws :&Workspace<'_>) -> CargoResult<Self> {
550 let cargo_exe = env::var_os(cargo::CARGO_ENV)
552 .map(Ok::<_, anyhow::Error>)
553 .unwrap_or_else(|| {
554 let cargo_exe = ws.gctx().cargo_exe()?;
556 ws.gctx().shell().warn(format!(
557 "Couldn't find $CARGO environment variable. Setting it to {}",
558 cargo_exe.display(),
559 ))?;
560 ws.gctx().shell().warn(
561 "`cargo-udeps` currently does not support basic Cargo commands such as `build`",
562 )?;
563 Ok(cargo_exe.into())
564 })?;
565 Ok(Self {
566 cargo_exe,
567 supports_color :ws.gctx().shell().err_supports_color(),
568 workspace_members :ws.members().map(Package::package_id).collect(),
569 relevant_cmd_infos : Vec::new(),
570 all_cmd_infos : Vec::new(),
571 })
572 }
573}
574
575struct Exec {
576 data :Arc<Mutex<ExecData>>,
577}
578
579impl Executor for Exec {
580 fn exec(&self, cmd :&ProcessBuilder, id :PackageId, target :&Target,
581 mode :CompileMode, on_stdout_line :&mut dyn FnMut(&str) -> CargoResult<()>,
582 on_stderr_line :&mut dyn FnMut(&str) -> CargoResult<()>) -> CargoResult<()> {
583
584 let cmd_info = cmd_info(id, target.is_custom_build(), cmd).unwrap_or_else(|e| {
585 panic!("Couldn't obtain crate info {:?}: {:?}", id, e);
586 });
587
588 let mut cmd = cmd.clone();
589
590 let is_path = id.source_id().is_path();
591 let is_workspace_member;
592
593 {
594 let mut bt = self.data.lock().unwrap();
596
597 is_workspace_member = bt.workspace_members.contains(&id);
598
599 bt.all_cmd_infos.push(cmd_info.clone());
600
601 if is_workspace_member {
604 bt.relevant_cmd_infos.push(cmd_info.clone());
605 }
606 assert!(
607 !(!is_path && is_workspace_member),
608 "`{}` is a workspace member but is not from a filesystem path",
609 id,
610 );
611 if (!cmd_info.cap_lints_allow) != is_path {
612 on_stderr_line(&format!(
613 "{} (!cap_lints_allow)={} differs from is_path={} for id={}",
614 if bt.supports_color {
615 Color::Yellow.bold().paint("warning:").to_string()
616 } else {
617 "warning:".to_owned()
618 },
619 !cmd_info.cap_lints_allow,
620 is_path,
621 id,
622 ))?;
623 }
624 cmd.env(cargo::CARGO_ENV, &bt.cargo_exe);
625 }
626 DefaultExecutor.exec(&cmd, id, target, mode, on_stdout_line, on_stderr_line)?;
627 Ok(())
628 }
629 fn force_rebuild(&self, unit :&Unit) -> bool {
630 let bt = self.data.lock().unwrap();
631 bt.workspace_members.contains(&unit.pkg.package_id())
632 }
633}
634
635#[derive(Clone, Debug)]
636struct CmdInfo {
637 pkg :PackageId,
638 #[allow(dead_code)]
639 custom_build :bool,
640 crate_name :String,
641 crate_type :String,
642 extra_filename :String,
643 cap_lints_allow :bool,
644 out_dir :String,
645 extern_crate_names :HashSet<String>,
646}
647
648impl CmdInfo {
649 fn get_artifact_base_name(&self) -> String {
650 let maybe_lib = if self.crate_type.ends_with("lib") ||
651 self.crate_type == "proc-macro" {
652 "lib"
653 } else {
654 ""
655 };
656 maybe_lib.to_owned() + &self.crate_name + &self.extra_filename
657 }
658 fn get_depinfo_filename(&self) -> String {
659 self.crate_name.clone() + &self.extra_filename + ".d"
660 }
661 fn get_depinfo_path(&self) -> PathBuf {
662 Path::new(&self.out_dir)
663 .join(self.get_depinfo_filename())
664 }
665 fn get_depinfo(&self, shell :&mut Shell) -> CargoResult<DepInfo> {
666 let p = self.get_depinfo_path();
667 shell.info(format_args!("Loading depinfo from {:?}", p))?;
668 let di = parse_rustc_dep_info(&p)?;
669 let di = di.iter()
670 .map(|(v, w)| {
671 let w = w.iter().map(PathBuf::from).collect::<Vec<_>>();
672 (PathBuf::from(v), w)
673 })
674 .collect::<Vec<_>>();
675 Ok(DepInfo { di, f_name : self.get_depinfo_filename() })
676 }
677}
678
679struct DepInfo {
680 di :Vec<(PathBuf, Vec<PathBuf>)>,
681 f_name :String,
682}
683
684impl DepInfo {
685 fn deps_of_depfile(&self) -> Vec<PathBuf> {
686 self.di.iter()
687 .find(|(v, _w)| {
688 v.file_name() == Some(&std::ffi::OsString::from(&self.f_name))
689 })
690 .map(|v| v.1.clone())
691 .unwrap_or_default()
692 }
693}
694
695fn parse_rustc_dep_info(rustc_dep_info :&Path) -> CargoResult<Vec<(String, Vec<String>)>> {
701 let contents = std::fs::read_to_string(rustc_dep_info)?;
702 contents
703 .lines()
704 .filter_map(|l| l.find(": ").map(|i| (l, i)))
705 .map(|(line, pos)| {
706 let target = &line[..pos];
707 let mut deps = line[pos + 2..].split_whitespace();
708
709 let mut ret = Vec::new();
710 while let Some(s) = deps.next() {
711 let mut file = s.to_string();
712 while file.ends_with('\\') {
713 file.pop();
714 file.push(' ');
715 file.push_str(deps.next().ok_or_else(|| {
716 anyhow::anyhow!("malformed dep-info format, trailing \\".to_string())
717 })?);
718 }
719 ret.push(file);
720 }
721 Ok((target.to_string(), ret))
722 })
723 .collect()
724}
725
726fn cmd_info(id :PackageId, custom_build :bool, cmd :&ProcessBuilder) -> CargoResult<CmdInfo> {
727 let mut args_iter = cmd.get_args();
728 let mut crate_name = None;
729 let mut crate_type = None;
730 let mut extra_filename = None;
731 let mut cap_lints_allow = false;
732 let mut out_dir = None;
733 let mut extern_crate_names = HashSet::new();
734 while let Some(v) = args_iter.next() {
735 if v == "--extern" {
736 if let Some(arg) = args_iter.next() {
737 let splitter = arg
738 .to_str()
739 .expect("non-utf8 paths not supported atm")
740 .split('=')
741 .collect::<Vec<_>>();
742 match *splitter {
743 [name] | [name, _] => extern_crate_names.insert(name.to_owned()),
744 _ => panic!("invalid format for extern arg: {:?}", arg),
745 };
746 }
747 } else if v == "--crate-name" {
748 if let Some(name) = args_iter.next() {
749 crate_name = Some(name.to_str()
750 .expect("non-utf8 crate names not supported")
751 .to_owned());
752 }
753 } else if v == "--crate-type" {
754 if let Some(ty) = args_iter.next() {
755 crate_type = Some(ty.to_str()
756 .expect("non-utf8 crate names not supported")
757 .to_owned());
758 }
759 } else if v == "--cap-lints" {
760 if let Some(c) = args_iter.next() {
761 if c == "allow" {
762 cap_lints_allow = true;
763 }
764 }
765 } else if v == "--out-dir" {
766 if let Some(d) = args_iter.next() {
767 out_dir = Some(d.to_str()
768 .expect("non-utf8 crate names not supported")
769 .to_owned());
770 }
771 } else if v == "-C" {
772 if let Some(arg) = args_iter.next() {
773 let arg = arg.to_str().expect("non-utf8 args not supported atm");
774 let mut splitter = arg.split('=');
775 if let (Some(n), Some(p)) = (splitter.next(), splitter.next()) {
776 if n == "extra-filename" {
777 extra_filename = Some(p.to_owned());
778 }
779 }
780 }
781 }
782 }
783 let pkg = id;
784 let crate_name = crate_name.ok_or_else(|| anyhow::anyhow!("crate name needed"))?;
785 let crate_type = crate_type.unwrap_or_else(|| "bin".to_owned());
786 let extra_filename = extra_filename.ok_or_else(|| anyhow::anyhow!("extra-filename needed"))?;
787 let out_dir = out_dir.ok_or_else(|| anyhow::anyhow!("outdir needed"))?;
788
789 Ok(CmdInfo {
790 pkg,
791 custom_build,
792 crate_name,
793 crate_type,
794 extra_filename,
795 cap_lints_allow,
796 out_dir,
797 extern_crate_names,
798 })
799}
800
801#[derive(Debug, Default)]
802struct DependencyNames {
803 normal: DependencyNamesValue,
804 development: DependencyNamesValue,
805 build: DependencyNamesValue,
806}
807
808impl DependencyNames {
809 fn new(
810 from :&Package,
811 packages :&HashMap<PackageId, &Package>,
812 resolve :&Resolve,
813 shell :&mut Shell,
814 ) -> CargoResult<Self> {
815 let mut this = Self::default();
816
817 let from = from.package_id();
818
819 for (to_pkg, deps) in resolve.deps(from) {
820 let to_pkg = packages.get(&to_pkg).unwrap_or_else(|| panic!("could not find `{}`", to_pkg));
821
822 if let Some(to_lib) = to_pkg
825 .targets()
826 .iter()
827 .find(|t| t.is_lib())
828 {
829 let extern_crate_name = resolve.extern_crate_name_and_dep_name(from, to_pkg.package_id(), to_lib)?.0.as_str();
830 let lib_true_snakecased_name = to_lib.crate_name();
831
832 for dep in deps {
833 assert_eq!(dep.package_name(), to_pkg.name());
834 let names = &mut this[dep.kind()];
835 names.by_extern_crate_name.insert(extern_crate_name, dep.name_in_toml());
836 if let Some(pkg) = names.by_package_id.insert(to_pkg.package_id(), dep.name_in_toml()) {
837 shell.warn(format!("duplicate package mentioned in toml {}. {pkg}", to_pkg.package_id()))?;
838 }
839
840 names
842 .by_lib_true_snakecased_name
843 .entry(lib_true_snakecased_name.clone())
844 .or_insert_with(HashSet::new)
845 .insert(dep.name_in_toml());
846 }
847 } else {
848 for dep in deps {
849 this[dep.kind()].non_lib.insert(dep.name_in_toml());
850 }
851 }
852 }
853
854 let ambiguous_names = |kinds: &[dependency::DepKind]| -> BTreeMap<_, _> {
855 kinds
856 .iter()
857 .flat_map(|&k| &this[k].by_lib_true_snakecased_name)
858 .filter(|(_, v)| v.len() > 1)
859 .flat_map(|(k, v)| v.iter().map(move |&v| (v, k.deref())))
860 .collect()
861 };
862
863 let ambiguous_normal_dev =
864 ambiguous_names(&[dependency::DepKind::Normal, dependency::DepKind::Development]);
865 let ambiguous_build = ambiguous_names(&[dependency::DepKind::Build]);
866
867 if !(ambiguous_normal_dev.is_empty() && ambiguous_build.is_empty()) {
868 let mut msg = format!(
869 "Currently `cargo-udeps` cannot distinguish multiple crates with the same `lib` name. This may cause false negative\n\
870 `{}`\n",
871 from,
872 );
873 let (edge, joint) = if ambiguous_build.is_empty() {
874 (' ', '└')
875 } else {
876 ('│', '├')
877 };
878 for (ambiguous, edge, joint, prefix) in &[
879 (ambiguous_normal_dev, edge, joint, "(dev-)"),
880 (ambiguous_build, ' ', '└', "build-"),
881 ] {
882 if !ambiguous.is_empty() {
883 writeln!(msg, "{}─── {}dependencies", joint, prefix).unwrap();
884 let mut ambiguous = ambiguous.iter().peekable();
885 while let Some((dep, lib)) = ambiguous.next() {
886 let joint = if ambiguous.peek().is_some() {
887 '├'
888 } else {
889 '└'
890 };
891 writeln!(msg, "{} {}─── {:?} → {:?}", edge, joint, dep, lib).unwrap();
892 }
893 }
894 }
895 shell.warn(msg.trim_end())?;
896 }
897
898 Ok(this)
899 }
900
901 fn has_non_lib(&self) -> bool {
902 [dependency::DepKind::Normal, dependency::DepKind::Development, dependency::DepKind::Build]
903 .iter()
904 .any(|&k| !self[k].non_lib.is_empty())
905 }
906}
907
908impl Index<dependency::DepKind> for DependencyNames {
909 type Output = DependencyNamesValue;
910
911 fn index(&self, index: dependency::DepKind) -> &DependencyNamesValue {
912 match index {
913 dependency::DepKind::Normal => &self.normal,
914 dependency::DepKind::Development => &self.development,
915 dependency::DepKind::Build => &self.build,
916 }
917 }
918}
919
920impl IndexMut<dependency::DepKind> for DependencyNames {
921 fn index_mut(&mut self, index: dependency::DepKind) -> &mut DependencyNamesValue {
922 match index {
923 dependency::DepKind::Normal => &mut self.normal,
924 dependency::DepKind::Development => &mut self.development,
925 dependency::DepKind::Build => &mut self.build,
926 }
927 }
928}
929
930#[derive(Debug, Default)]
931struct DependencyNamesValue {
932 by_extern_crate_name :HashMap<&'static str, InternedString>,
933 by_lib_true_snakecased_name :HashMap<String, HashSet<InternedString>>,
934 by_package_id :HashMap<PackageId, InternedString>,
935 non_lib :HashSet<InternedString>,
936}
937
938#[derive(Debug, Deserialize)]
939#[serde(rename_all = "kebab-case")]
940struct PackageMetadata {
941 #[serde(default)]
942 cargo_udeps: PackageMetadataCargoUdeps,
943}
944
945#[derive(Debug, Default, Deserialize)]
946struct PackageMetadataCargoUdeps {
947 #[serde(default)]
948 ignore: PackageMetadataCargoUdepsIgnore,
949}
950
951#[derive(Debug, Default, Deserialize)]
952struct PackageMetadataCargoUdepsIgnore {
953 #[serde(default)]
954 normal: HashSet<String>,
955 #[serde(default)]
956 development: HashSet<String>,
957 #[serde(default)]
958 build: HashSet<String>,
959}
960
961impl PackageMetadataCargoUdepsIgnore {
962 fn contains(&self, kind: dependency::DepKind, name_in_toml: InternedString) -> bool {
963 match kind {
964 dependency::DepKind::Normal => &self.normal,
965 dependency::DepKind::Development => &self.development,
966 dependency::DepKind::Build => &self.build,
967 }
968 .contains(&*name_in_toml)
969 }
970}
971
972#[derive(Default, Debug, Serialize)]
973struct Outcome {
974 success: bool,
975 unused_deps: BTreeMap<PackageId, OutcomeUnusedDeps>,
976 note: Option<String>,
977}
978
979impl Outcome {
980 fn print(&self, output: OutputKind, stdout: impl Write) -> io::Result<()> {
981 match output {
982 OutputKind::Human => self.print_human(stdout),
983 OutputKind::Json => self.print_json(stdout),
984 }
985 }
986
987 fn print_human(&self, mut stdout: impl Write) -> io::Result<()> {
988 if self.success {
989 writeln!(stdout, "All deps seem to have been used.")?;
990 } else {
991 writeln!(stdout, "unused dependencies:")?;
992
993 for (member, OutcomeUnusedDeps { normal, development, build, .. }) in &self.unused_deps {
994 fn edge_and_joint(p: bool) -> (char, char) {
995 if p {
996 (' ', '└')
997 } else {
998 ('│', '├')
999 }
1000 }
1001
1002 writeln!(stdout, "`{}`", member)?;
1003
1004 for (deps, (edge, joint), prefix) in &[
1005 (normal, edge_and_joint(development.is_empty() && build.is_empty()), ""),
1006 (development, edge_and_joint(build.is_empty()), "dev-"),
1007 (build, (' ', '└'), "build-"),
1008 ] {
1009 if !deps.is_empty() {
1010 writeln!(stdout, "{}─── {}dependencies", joint, prefix)?;
1011 let mut deps = deps.iter().peekable();
1012 while let Some(dep) = deps.next() {
1013 let joint = if deps.peek().is_some() {
1014 '├'
1015 } else {
1016 '└'
1017 };
1018 writeln!(stdout, "{} {}─── {:?}", edge, joint, dep)?;
1019 }
1020 }
1021 }
1022 }
1023
1024 if let Some(note) = &self.note {
1025 write!(stdout, "{}", note)?;
1026 }
1027 }
1028 stdout.flush()
1029 }
1030
1031 fn print_json(&self, mut stdout: impl Write) -> io::Result<()> {
1032 let json = serde_json::to_string(self).expect("should not fail");
1033 writeln!(stdout, "{}", json)?;
1034 stdout.flush()
1035 }
1036}
1037
1038#[derive(Debug, Serialize)]
1039struct OutcomeUnusedDeps {
1040 manifest_path: String,
1041 normal: BTreeSet<InternedString>,
1042 development: BTreeSet<InternedString>,
1043 build: BTreeSet<InternedString>,
1044}
1045
1046impl OutcomeUnusedDeps {
1047 fn new(manifest_path: &Path) -> CargoResult<Self> {
1048 let manifest_path = manifest_path
1049 .to_str()
1050 .ok_or_else(|| anyhow::anyhow!("{:?} is not valid utf-8", manifest_path))?
1051 .to_owned();
1052
1053 Ok(Self {
1054 manifest_path,
1055 normal: BTreeSet::new(),
1056 development: BTreeSet::new(),
1057 build: BTreeSet::new(),
1058 })
1059 }
1060
1061 fn unused_deps_mut(&mut self, kind: dependency::DepKind) -> &mut BTreeSet<InternedString> {
1062 match kind {
1063 dependency::DepKind::Normal => &mut self.normal,
1064 dependency::DepKind::Development => &mut self.development,
1065 dependency::DepKind::Build => &mut self.build,
1066 }
1067 }
1068}
1069
1070#[derive(clap::ValueEnum, Clone, Copy, Debug)]
1071enum OutputKind {
1072 Human,
1073 Json,
1074}
1075
1076impl FromStr for OutputKind {
1077 type Err = &'static str;
1078
1079 fn from_str(s: &str) -> std::result::Result<Self, &'static str> {
1080 match s {
1081 "human" => Ok(Self::Human),
1082 "json" => Ok(Self::Json),
1083 _ => Err(r#"expected "human" or "json" (you should not see this message)"#),
1084 }
1085 }
1086}
1087
1088#[derive(clap::ValueEnum, Clone, Copy, Debug)]
1089enum Backend {
1090 Depinfo,
1091}
1092
1093impl FromStr for Backend {
1094 type Err = &'static str;
1095
1096 fn from_str(s: &str) -> std::result::Result<Self, &'static str> {
1097 match s {
1098 "depinfo" => Ok(Self::Depinfo),
1099 _ => Err(r#"expected "depinfo" (you should not see this message)"#),
1100 }
1101 }
1102}
1103
1104trait ShellExt {
1105 fn info<T: fmt::Display>(&mut self, message: T) -> CargoResult<()>;
1106}
1107
1108impl ShellExt for Shell {
1109 fn info<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
1110 match self.verbosity() {
1111 Verbosity::Quiet => Ok(()),
1112 _ => self.print_ansi_stderr(
1113 format!(
1114 "{} {}\n",
1115 if self.err_supports_color() {
1116 Color::Cyan.bold().paint("info:").to_string()
1117 } else {
1118 "info:".to_owned()
1119 },
1120 message,
1121 )
1122 .as_ref(),
1123 )
1124 }
1125 }
1126}