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