cargo_udeps/
lib.rs

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			//println!("lib stem {} -> {}", lib_stem, cmd_info.pkg);
333			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			// may not be workspace member
346			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							// The file names are like cratename-hash.rmeta or .rlib,
364							// where "hash" is a hash string that cargo calls "metadata"
365							// internally and computes in its "compute_metadata" function,
366							// and cratename is the snakecased crate name.
367
368							// First, we continue if there is no - in the filename.
369							// it's likely a source file or some other artifact we aren't
370							// interested in. This is obviously only a stupid heuristic.
371							let lib_name = match fs.split_once('-') {
372								None => continue,
373								Some((lib_name, _)) => lib_name
374							};
375
376							// The metadata hash is not available through cargo's api
377							// outside of the Executor trait impl. We do our best to obtain
378							// the hashes from that impl, but the executor is not called
379							// for anything but crates that have to be recompiled.
380							// Thus, any crates that weren't recompiled we don't know the
381							// metadata hash of. So we perform a check: if we know the metadata
382							// hash, we use it, otherwise we don't.
383							// This gives a bit surprising behaviour when re-running
384							// cargo-udeps but at least sometimes the results are more accurate.
385
386							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								// TODO this is a hack as we unconditionally strip the prefix.
392								// It won't work for proc macro crates that start with "lib".
393								// See maybe_lib in the code above.
394								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						// We ignore:
406						// 1. the `lib` that `bin`s, `example`s, and `test`s in the same `Package` depend on
407						// 2. crates bundled with `rustc` such as `proc-macro`
408						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				// This package may have been explicitly excluded via flags.
459				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		// `$CARGO` should be present when `cargo-udeps` is executed as `cargo udeps ..` or `cargo run -- udeps ..`.
551		let cargo_exe = env::var_os(cargo::CARGO_ENV)
552			.map(Ok::<_, anyhow::Error>)
553			.unwrap_or_else(|| {
554				// Unless otherwise specified, `$CARGO` is set to `config.cargo_exe()` for compilation commands which points at `cargo-udeps`.
555				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			// TODO unwrap used
595			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 the crate is not a in the workspace,
602			// we are not interested in its information.
603			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
695// Bases on function with same name from cargo source src/cargo/core/compiler/fingerprint.rs
696/// Parse the `.d` dep-info file generated by rustc.
697///
698/// Result is a Vec of `(target, prerequisites)` tuples where `target` is the
699/// rule name, and `prerequisites` is a list of files that it depends on.
700fn 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			// Not all dependencies contain `lib` targets as it is OK to append non-library packages to `Cargo.toml`.
823			// Their `bin` targets can be built with `cargo build --bins -p <SPEC>` and are available in build scripts.
824			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					// Two `Dependenc`ies with the same name point at the same `Package`.
841					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}