1use std::collections::VecDeque;
2use std::path::Path;
3
4use crate::util::data_structures::{HashSet, IndexMap};
5use cargo_util_schemas::manifest;
6use cargo_util_schemas::manifest::TomlPackageBuild;
7use cargo_util_terminal::report::AnnotationKind;
8use cargo_util_terminal::report::Group;
9use cargo_util_terminal::report::Level;
10use cargo_util_terminal::report::Origin;
11use cargo_util_terminal::report::Snippet;
12use tracing::{debug, instrument, trace};
13
14use super::STYLE;
15use crate::CargoResult;
16use crate::GlobalContext;
17use crate::compiler::BuildContext;
18use crate::compiler::BuildRunner;
19use crate::compiler::Unit;
20use crate::compiler::unused_deps::DependenciesState;
21use crate::compiler::unused_deps::UnusedDepState;
22use crate::diagnostics::GlobalDiagnosticStats;
23use crate::diagnostics::Lint;
24use crate::diagnostics::LintLevel;
25use crate::diagnostics::LintLevelProduct;
26use crate::diagnostics::ScopedDiagnosticStats;
27use crate::diagnostics::get_key_value_span;
28use crate::diagnostics::workspace_rel_path;
29use crate::workspace::Package;
30use crate::workspace::PackageId;
31use crate::workspace::Workspace;
32use crate::workspace::dependency::DepKind;
33
34pub static LINT: &Lint = &Lint {
35 name: "unused_dependencies",
36 primary_group: &STYLE,
37 msrv: Some(super::CARGO_LINTS_MSRV),
38 feature_gate: None,
39 docs: Some(
40 r#"
41### What it does
42
43Checks for dependencies that are not used by any of the cargo targets.
44
45### Why is this bad?
46
47Slows down compilation time.
48
49### Drawbacks
50
51The lint is only emitted in specific circumstances as multiple cargo targets exist for the
52different dependencies tables and they must all be built to know if a dependency is unused.
53Currently, only the selected packages are checked and not all `path` dependencies like most lints.
54The cargo target selection flags,
55independent of which packages are selected, determine which dependencies tables are checked.
56As there is no way to select all cargo targets that use `[dev-dependencies]`,
57they are unchecked.
58
59Examples:
60- `cargo check` will lint `[build-dependencies]` and `[dependencies]`
61- `cargo check --all-targets` will still only lint `[build-dependencies]` and `[dependencies]` and not `[dev-dependencoes]`
62- `cargo check --bin foo` will not lint `[dependencies]` even if `foo` is the only bin though `[build-dependencies]` will be checked
63- `cargo check -p foo` will not lint any dependencies tables for the `path` dependency `bar` even if `bar` only has a `[lib]`
64
65There can be false positives when depending on a transitive dependency to activate a feature.
66
67For false positives from pinning the version of a transitive dependency in `Cargo.toml`,
68move the dependency to the `target."cfg(false)".dependencies` table.
69
70### Example
71
72```toml
73[package]
74name = "foo"
75
76[dependencies]
77unused = "1"
78```
79
80Should be written as:
81
82```toml
83[package]
84name = "foo"
85```
86"#,
87 ),
88};
89
90#[instrument(skip_all)]
97pub(crate) fn lint_package(
98 ws: &Workspace<'_>,
99 pkg: &Package,
100 manifest_path: &Path,
101 level: LintLevelProduct,
102 pkg_stats: &mut ScopedDiagnosticStats<'_>,
103 gctx: &GlobalContext,
104) -> CargoResult<()> {
105 let LintLevelProduct {
106 level: lint_level,
107 source,
108 } = level;
109
110 let manifest_path = workspace_rel_path(ws, manifest_path);
111
112 let manifest = pkg.manifest();
113 let Some(package) = &manifest.normalized_toml().package else {
114 return Ok(());
115 };
116 if package.build != Some(TomlPackageBuild::Auto(false)) {
117 return Ok(());
118 }
119
120 let document = manifest.document();
121 let contents = manifest.contents();
122
123 for (i, dep_name) in manifest
124 .normalized_toml()
125 .build_dependencies()
126 .iter()
127 .flat_map(|m| m.keys())
128 .enumerate()
129 {
130 let level = lint_level.to_diagnostic_level();
131 let emitted_source = LINT.emitted_source(lint_level, source);
132
133 let mut primary =
134 Group::with_title(level.primary_title(format!("unused build dependency `{dep_name}`")));
135 if let Some(document) = document
136 && let Some(contents) = contents
137 && let Some(span) = get_key_value_span(document, &["build-dependencies", dep_name])
138 {
139 let span = span.key.start..span.value.end;
140 primary = primary.element(
141 Snippet::source(contents)
142 .path(&manifest_path)
143 .annotation(AnnotationKind::Primary.span(span)),
144 );
145 } else {
146 primary = primary.element(Origin::path(&manifest_path));
147 }
148 if i == 0 {
149 primary = primary.element(Level::NOTE.message(emitted_source));
150 }
151 let mut report = vec![primary];
152 let help = Group::with_title(
153 Level::HELP
154 .secondary_title(format!("consider removing the dependency on `{dep_name}`")),
155 );
156 report.push(help);
157
158 pkg_stats.record_lint(lint_level);
159 gctx.shell().print_report(&report, lint_level.force())?;
160 }
161
162 Ok(())
163}
164
165#[instrument(skip_all)]
166pub fn lint_build_results(
167 build_runner: &BuildRunner<'_, '_>,
168 global_stats: &mut GlobalDiagnosticStats,
169) -> CargoResult<()> {
170 for (pkg_id, states) in &build_runner.unused_dep_state.states {
171 let Some(pkg) = get_package(&build_runner.unused_dep_state, pkg_id) else {
172 continue;
173 };
174 let toml_lints = pkg
175 .manifest()
176 .normalized_toml()
177 .lints
178 .clone()
179 .map(|lints| lints.lints)
180 .unwrap_or(manifest::TomlLints::default());
181 let cargo_lints = toml_lints
182 .get("cargo")
183 .cloned()
184 .unwrap_or(manifest::TomlToolLints::default());
185 let level = LINT.level(
186 &cargo_lints,
187 pkg.rust_version(),
188 pkg.manifest().unstable_features(),
189 build_runner.bcx.gctx,
190 );
191 if !pkg_id.source_id().is_path() {
192 for (dep_kind, state) in states.iter() {
193 for ext in state.unused_externs.iter().flatten() {
194 debug!(
195 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, package is capped",
196 pkg_id.name(),
197 pkg_id.version(),
198 );
199 }
200 }
201 continue;
202 }
203 if level.level == LintLevel::Allow {
204 for (dep_kind, state) in states.iter() {
205 for ext in state.unused_externs.iter().flatten() {
206 debug!(
207 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, lint is allowed",
208 pkg_id.name(),
209 pkg_id.version(),
210 );
211 }
212 }
213 continue;
214 }
215
216 let mut pkg_stats = global_stats.scope();
217 lint_package_build_results(build_runner, pkg, states, level, &mut pkg_stats)?;
218 pkg_stats.report_summary("finalize", Some(&*pkg.name()), build_runner.bcx.gctx)?;
219 }
220 Ok(())
221}
222
223fn lint_package_build_results(
224 build_runner: &BuildRunner<'_, '_>,
225 pkg: &Package,
226 states: &IndexMap<DepKind, DependenciesState>,
227 level: LintLevelProduct,
228 pkg_stats: &mut ScopedDiagnosticStats<'_>,
229) -> CargoResult<()> {
230 let mut lint_count = 0;
231 let LintLevelProduct {
232 level: lint_level,
233 source,
234 } = level;
235 let ws = build_runner.bcx.ws;
236 let manifest_path = workspace_rel_path(ws, pkg.manifest_path());
237 let pkg_id = pkg.package_id();
238 for (dep_kind, state) in states.iter() {
239 for ext in state.unused_externs.iter().flatten() {
240 let mut used_in_dev = false;
241 match dep_kind {
242 DepKind::Normal => {
243 if let Some(state) = states.get(&DepKind::Development)
244 && state
245 .unused_externs
246 .as_ref()
247 .is_some_and(|ue| !ue.contains(ext))
248 {
249 used_in_dev = true;
250 }
251 }
252 DepKind::Development => {
253 if let Some(state) = states.get(&DepKind::Normal)
254 && state.externs.contains_key(ext)
255 {
256 trace!(
257 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, inherited from normal dependency",
258 pkg_id.name(),
259 pkg_id.version(),
260 );
261 continue;
262 }
263 }
264 DepKind::Build => {}
265 }
266 let Some(extern_state) = state.externs.get(ext) else {
267 debug!(
269 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, untracked dependent",
270 pkg_id.name(),
271 pkg_id.version(),
272 );
273 continue;
274 };
275 if state.seen_units.len() != state.needed_units {
276 debug_assert_ne!(state.externs.len(), 0, "assumes tracked is checked first");
277 debug!(
281 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, {} outstanding units",
282 pkg_id.name(),
283 pkg_id.version(),
284 state.needed_units - state.seen_units.len()
285 );
286 continue;
287 }
288 if is_transitive_dep(&extern_state.unit, &state.seen_units, build_runner.bcx) {
289 debug!(
290 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, may be activating features",
291 pkg_id.name(),
292 pkg_id.version(),
293 );
294 continue;
295 }
296
297 let dependency = if let Some(dependency) = &extern_state.manifest_deps {
299 dependency
300 } else {
301 continue;
302 };
303 for dependency in dependency {
304 let manifest = pkg.manifest();
305 let document = manifest.document();
306 let contents = manifest.contents();
307 let level = lint_level.to_diagnostic_level();
308 let emitted_source = LINT.emitted_source(lint_level, source);
309 let toml_path = dependency.toml_path();
310 let dep_name = toml_path.last().unwrap();
311
312 let mut primary = Group::with_title(
313 level.primary_title(format!("unused dependency `{dep_name}`")),
314 );
315 if let Some(document) = document
316 && let Some(contents) = contents
317 && let Some(span) = get_key_value_span(document, &toml_path)
318 {
319 let span = span.key.start..span.value.end;
320 primary = primary.element(
321 Snippet::source(contents)
322 .path(&manifest_path)
323 .annotation(AnnotationKind::Primary.span(span)),
324 );
325 } else {
326 primary = primary.element(Origin::path(&manifest_path));
327 }
328 if lint_count == 0 {
329 primary = primary.element(Level::NOTE.message(emitted_source));
330 }
331 lint_count += 1;
332 let mut report = vec![primary];
333 let help =
334 Group::with_title(Level::HELP.secondary_title(format!(
335 "consider removing the dependency on `{dep_name}`"
336 )));
337 report.push(help);
338 if used_in_dev {
339 let help = Group::with_title(Level::HELP.secondary_title(
340 "to still use for development builds, move to `dev-dependencies`",
341 ));
342 report.push(help);
343 }
344
345 pkg_stats.record_lint(lint_level);
346 build_runner
347 .bcx
348 .gctx
349 .shell()
350 .print_report(&report, lint_level.force())?;
351 }
352 }
353 }
354 Ok(())
355}
356
357fn get_package<'s>(
358 unused_dep_state: &'s UnusedDepState,
359 pkg_id: &PackageId,
360) -> Option<&'s Package> {
361 let state = unused_dep_state.states.get(pkg_id)?;
362 let mut iter = state.values();
363 let state = iter.next()?;
364 let mut iter = state.seen_units.iter();
365 let unit = iter.next()?;
366 Some(&unit.pkg)
367}
368
369#[instrument(skip_all)]
370fn is_transitive_dep(
371 direct_dep_unit: &Unit,
372 seen_units: &Vec<Unit>,
373 bcx: &BuildContext<'_, '_>,
374) -> bool {
375 let mut queue = VecDeque::new();
376 let mut visited: HashSet<&Unit> = HashSet::default();
377 for root_unit in seen_units {
378 for unit_dep in &bcx.unit_graph[root_unit] {
379 if root_unit.pkg.package_id() == unit_dep.unit.pkg.package_id() {
380 continue;
381 }
382 if unit_dep.unit == *direct_dep_unit {
383 continue;
384 }
385 if visited.insert(&unit_dep.unit) {
386 queue.push_back(&unit_dep.unit);
387 }
388 }
389 }
390
391 while let Some(dep_unit) = queue.pop_front() {
392 for unit_dep in &bcx.unit_graph[dep_unit] {
393 if unit_dep.unit == *direct_dep_unit {
394 return true;
395 }
396 if visited.insert(&unit_dep.unit) {
397 queue.push_back(&unit_dep.unit);
398 }
399 }
400 }
401
402 false
403}