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 );
198 if level.level == LintLevel::Allow {
199 for (dep_kind, state) in states.iter() {
200 for ext in state.unused_externs.iter().flatten() {
201 debug!(
202 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, lint is allowed",
203 pkg_id.name(),
204 pkg_id.version(),
205 );
206 }
207 }
208 continue;
209 }
210
211 let mut pkg_stats = global_stats.scope();
212 lint_package_build_results(build_runner, pkg, states, level, &mut pkg_stats)?;
213 pkg_stats.report_summary("finalize", Some(&*pkg.name()), build_runner.bcx.gctx)?;
214 }
215 Ok(())
216}
217
218fn lint_package_build_results(
219 build_runner: &BuildRunner<'_, '_>,
220 pkg: &Package,
221 states: &IndexMap<DepKind, DependenciesState>,
222 level: LintLevelProduct,
223 pkg_stats: &mut ScopedDiagnosticStats<'_>,
224) -> CargoResult<()> {
225 let mut lint_count = 0;
226 let LintLevelProduct {
227 level: lint_level,
228 source,
229 } = level;
230 let ws = build_runner.bcx.ws;
231 let manifest_path = workspace_rel_path(ws, pkg.manifest_path());
232 let pkg_id = pkg.package_id();
233 for (dep_kind, state) in states.iter() {
234 for ext in state.unused_externs.iter().flatten() {
235 let mut used_in_dev = false;
236 match dep_kind {
237 DepKind::Normal => {
238 if let Some(state) = states.get(&DepKind::Development)
239 && state
240 .unused_externs
241 .as_ref()
242 .is_some_and(|ue| !ue.contains(ext))
243 {
244 used_in_dev = true;
245 }
246 }
247 DepKind::Development => {
248 if let Some(state) = states.get(&DepKind::Normal)
249 && state.externs.contains_key(ext)
250 {
251 trace!(
252 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, inherited from normal dependency",
253 pkg_id.name(),
254 pkg_id.version(),
255 );
256 continue;
257 }
258 }
259 DepKind::Build => {}
260 }
261 let Some(extern_state) = state.externs.get(ext) else {
262 debug!(
264 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, untracked dependent",
265 pkg_id.name(),
266 pkg_id.version(),
267 );
268 continue;
269 };
270 if state.seen_units.len() != state.needed_units {
271 debug_assert_ne!(state.externs.len(), 0, "assumes tracked is checked first");
272 debug!(
276 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, {} outstanding units",
277 pkg_id.name(),
278 pkg_id.version(),
279 state.needed_units - state.seen_units.len()
280 );
281 continue;
282 }
283 if is_transitive_dep(&extern_state.unit, &state.seen_units, build_runner.bcx) {
284 debug!(
285 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, may be activating features",
286 pkg_id.name(),
287 pkg_id.version(),
288 );
289 continue;
290 }
291
292 let dependency = if let Some(dependency) = &extern_state.manifest_deps {
294 dependency
295 } else {
296 continue;
297 };
298 for dependency in dependency {
299 let manifest = pkg.manifest();
300 let document = manifest.document();
301 let contents = manifest.contents();
302 let level = lint_level.to_diagnostic_level();
303 let emitted_source = LINT.emitted_source(lint_level, source);
304 let toml_path = dependency.toml_path();
305
306 let mut primary = Group::with_title(level.primary_title(LINT.desc));
307 if let Some(document) = document
308 && let Some(contents) = contents
309 && let Some(span) = get_key_value_span(document, &toml_path)
310 {
311 let span = span.key.start..span.value.end;
312 primary = primary.element(
313 Snippet::source(contents)
314 .path(&manifest_path)
315 .annotation(AnnotationKind::Primary.span(span)),
316 );
317 } else {
318 primary = primary.element(Origin::path(&manifest_path));
319 }
320 if lint_count == 0 {
321 primary = primary.element(Level::NOTE.message(emitted_source));
322 }
323 lint_count += 1;
324 let mut report = vec![primary];
325 if let Some(document) = document
326 && let Some(contents) = contents
327 && let Some(span) = get_key_value_span(document, &toml_path)
328 {
329 let span = span.key.start..span.value.end;
330 let mut help =
331 Group::with_title(Level::HELP.secondary_title("remove the dependency"));
332 help = help.element(
333 Snippet::source(contents)
334 .path(&manifest_path)
335 .patch(Patch::new(span, "")),
336 );
337 report.push(help);
338 }
339 if used_in_dev {
340 let help = Group::with_title(Level::HELP.secondary_title(
341 "to still use for development builds, move to `dev-dependencies`",
342 ));
343 report.push(help);
344 }
345
346 pkg_stats.record_lint(lint_level);
347 build_runner
348 .bcx
349 .gctx
350 .shell()
351 .print_report(&report, lint_level.force())?;
352 }
353 }
354 }
355 Ok(())
356}
357
358fn get_package<'s>(
359 unused_dep_state: &'s UnusedDepState,
360 pkg_id: &PackageId,
361) -> Option<&'s Package> {
362 let state = unused_dep_state.states.get(pkg_id)?;
363 let mut iter = state.values();
364 let state = iter.next()?;
365 let mut iter = state.seen_units.iter();
366 let unit = iter.next()?;
367 Some(&unit.pkg)
368}
369
370#[instrument(skip_all)]
371fn is_transitive_dep(
372 direct_dep_unit: &Unit,
373 seen_units: &Vec<Unit>,
374 bcx: &BuildContext<'_, '_>,
375) -> bool {
376 let mut queue = std::collections::VecDeque::new();
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 queue.push_back(&unit_dep.unit);
386 }
387 }
388
389 while let Some(dep_unit) = queue.pop_front() {
390 for unit_dep in &bcx.unit_graph[dep_unit] {
391 if unit_dep.unit == *direct_dep_unit {
392 return true;
393 }
394 queue.push_back(&unit_dep.unit);
395 }
396 }
397
398 false
399}