1use std::path::Path;
2
3use crate::util::data_structures::IndexMap;
4use cargo_util_schemas::manifest;
5use cargo_util_schemas::manifest::TomlPackageBuild;
6use cargo_util_terminal::report::AnnotationKind;
7use cargo_util_terminal::report::Group;
8use cargo_util_terminal::report::Level;
9use cargo_util_terminal::report::Origin;
10use cargo_util_terminal::report::Snippet;
11use tracing::{debug, instrument, trace};
12
13use super::STYLE;
14use crate::CargoResult;
15use crate::GlobalContext;
16use crate::core::Package;
17use crate::core::PackageId;
18use crate::core::Workspace;
19use crate::core::compiler::BuildContext;
20use crate::core::compiler::BuildRunner;
21use crate::core::compiler::Unit;
22use crate::core::compiler::unused_deps::DependenciesState;
23use crate::core::compiler::unused_deps::UnusedDepState;
24use crate::core::dependency::DepKind;
25use crate::diagnostics::GlobalDiagnosticStats;
26use crate::diagnostics::Lint;
27use crate::diagnostics::LintLevel;
28use crate::diagnostics::LintLevelProduct;
29use crate::diagnostics::ScopedDiagnosticStats;
30use crate::diagnostics::get_key_value_span;
31use crate::diagnostics::workspace_rel_path;
32
33pub static LINT: &Lint = &Lint {
34 name: "unused_dependencies",
35 desc: "unused dependency",
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 it is 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 = Group::with_title(level.primary_title(LINT.desc));
134 if let Some(document) = document
135 && let Some(contents) = contents
136 && let Some(span) = get_key_value_span(document, &["build-dependencies", dep_name])
137 {
138 let span = span.key.start..span.value.end;
139 primary = primary.element(
140 Snippet::source(contents)
141 .path(&manifest_path)
142 .annotation(AnnotationKind::Primary.span(span)),
143 );
144 } else {
145 primary = primary.element(Origin::path(&manifest_path));
146 }
147 if i == 0 {
148 primary = primary.element(Level::NOTE.message(emitted_source));
149 }
150 let mut report = vec![primary];
151 let help = Group::with_title(
152 Level::HELP.secondary_title("consider removing the unused dependency"),
153 );
154 report.push(help);
155
156 pkg_stats.record_lint(lint_level);
157 gctx.shell().print_report(&report, lint_level.force())?;
158 }
159
160 Ok(())
161}
162
163#[instrument(skip_all)]
164pub fn lint_build_results(
165 build_runner: &BuildRunner<'_, '_>,
166 global_stats: &mut GlobalDiagnosticStats,
167) -> CargoResult<()> {
168 for (pkg_id, states) in &build_runner.unused_dep_state.states {
169 let Some(pkg) = get_package(&build_runner.unused_dep_state, pkg_id) else {
170 continue;
171 };
172 let toml_lints = pkg
173 .manifest()
174 .normalized_toml()
175 .lints
176 .clone()
177 .map(|lints| lints.lints)
178 .unwrap_or(manifest::TomlLints::default());
179 let cargo_lints = toml_lints
180 .get("cargo")
181 .cloned()
182 .unwrap_or(manifest::TomlToolLints::default());
183 let level = LINT.level(
184 &cargo_lints,
185 pkg.rust_version(),
186 pkg.manifest().unstable_features(),
187 build_runner.bcx.gctx,
188 );
189 if !pkg_id.source_id().is_path() {
190 for (dep_kind, state) in states.iter() {
191 for ext in state.unused_externs.iter().flatten() {
192 debug!(
193 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, package is capped",
194 pkg_id.name(),
195 pkg_id.version(),
196 );
197 }
198 }
199 continue;
200 }
201 if level.level == LintLevel::Allow {
202 for (dep_kind, state) in states.iter() {
203 for ext in state.unused_externs.iter().flatten() {
204 debug!(
205 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, lint is allowed",
206 pkg_id.name(),
207 pkg_id.version(),
208 );
209 }
210 }
211 continue;
212 }
213
214 let mut pkg_stats = global_stats.scope();
215 lint_package_build_results(build_runner, pkg, states, level, &mut pkg_stats)?;
216 pkg_stats.report_summary("finalize", Some(&*pkg.name()), build_runner.bcx.gctx)?;
217 }
218 Ok(())
219}
220
221fn lint_package_build_results(
222 build_runner: &BuildRunner<'_, '_>,
223 pkg: &Package,
224 states: &IndexMap<DepKind, DependenciesState>,
225 level: LintLevelProduct,
226 pkg_stats: &mut ScopedDiagnosticStats<'_>,
227) -> CargoResult<()> {
228 let mut lint_count = 0;
229 let LintLevelProduct {
230 level: lint_level,
231 source,
232 } = level;
233 let ws = build_runner.bcx.ws;
234 let manifest_path = workspace_rel_path(ws, pkg.manifest_path());
235 let pkg_id = pkg.package_id();
236 for (dep_kind, state) in states.iter() {
237 for ext in state.unused_externs.iter().flatten() {
238 let mut used_in_dev = false;
239 match dep_kind {
240 DepKind::Normal => {
241 if let Some(state) = states.get(&DepKind::Development)
242 && state
243 .unused_externs
244 .as_ref()
245 .is_some_and(|ue| !ue.contains(ext))
246 {
247 used_in_dev = true;
248 }
249 }
250 DepKind::Development => {
251 if let Some(state) = states.get(&DepKind::Normal)
252 && state.externs.contains_key(ext)
253 {
254 trace!(
255 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, inherited from normal dependency",
256 pkg_id.name(),
257 pkg_id.version(),
258 );
259 continue;
260 }
261 }
262 DepKind::Build => {}
263 }
264 let Some(extern_state) = state.externs.get(ext) else {
265 debug!(
267 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, untracked dependent",
268 pkg_id.name(),
269 pkg_id.version(),
270 );
271 continue;
272 };
273 if state.seen_units.len() != state.needed_units {
274 debug_assert_ne!(state.externs.len(), 0, "assumes tracked is checked first");
275 debug!(
279 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, {} outstanding units",
280 pkg_id.name(),
281 pkg_id.version(),
282 state.needed_units - state.seen_units.len()
283 );
284 continue;
285 }
286 if is_transitive_dep(&extern_state.unit, &state.seen_units, build_runner.bcx) {
287 debug!(
288 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, may be activating features",
289 pkg_id.name(),
290 pkg_id.version(),
291 );
292 continue;
293 }
294
295 let dependency = if let Some(dependency) = &extern_state.manifest_deps {
297 dependency
298 } else {
299 continue;
300 };
301 for dependency in dependency {
302 let manifest = pkg.manifest();
303 let document = manifest.document();
304 let contents = manifest.contents();
305 let level = lint_level.to_diagnostic_level();
306 let emitted_source = LINT.emitted_source(lint_level, source);
307 let toml_path = dependency.toml_path();
308
309 let mut primary = Group::with_title(level.primary_title(LINT.desc));
310 if let Some(document) = document
311 && let Some(contents) = contents
312 && let Some(span) = get_key_value_span(document, &toml_path)
313 {
314 let span = span.key.start..span.value.end;
315 primary = primary.element(
316 Snippet::source(contents)
317 .path(&manifest_path)
318 .annotation(AnnotationKind::Primary.span(span)),
319 );
320 } else {
321 primary = primary.element(Origin::path(&manifest_path));
322 }
323 if lint_count == 0 {
324 primary = primary.element(Level::NOTE.message(emitted_source));
325 }
326 lint_count += 1;
327 let mut report = vec![primary];
328 let help = Group::with_title(
329 Level::HELP.secondary_title("consider removing the unused dependency"),
330 );
331 report.push(help);
332 if used_in_dev {
333 let help = Group::with_title(Level::HELP.secondary_title(
334 "to still use for development builds, move to `dev-dependencies`",
335 ));
336 report.push(help);
337 }
338
339 pkg_stats.record_lint(lint_level);
340 build_runner
341 .bcx
342 .gctx
343 .shell()
344 .print_report(&report, lint_level.force())?;
345 }
346 }
347 }
348 Ok(())
349}
350
351fn get_package<'s>(
352 unused_dep_state: &'s UnusedDepState,
353 pkg_id: &PackageId,
354) -> Option<&'s Package> {
355 let state = unused_dep_state.states.get(pkg_id)?;
356 let mut iter = state.values();
357 let state = iter.next()?;
358 let mut iter = state.seen_units.iter();
359 let unit = iter.next()?;
360 Some(&unit.pkg)
361}
362
363#[instrument(skip_all)]
364fn is_transitive_dep(
365 direct_dep_unit: &Unit,
366 seen_units: &Vec<Unit>,
367 bcx: &BuildContext<'_, '_>,
368) -> bool {
369 let mut queue = std::collections::VecDeque::new();
370 for root_unit in seen_units {
371 for unit_dep in &bcx.unit_graph[root_unit] {
372 if root_unit.pkg.package_id() == unit_dep.unit.pkg.package_id() {
373 continue;
374 }
375 if unit_dep.unit == *direct_dep_unit {
376 continue;
377 }
378 queue.push_back(&unit_dep.unit);
379 }
380 }
381
382 while let Some(dep_unit) = queue.pop_front() {
383 for unit_dep in &bcx.unit_graph[dep_unit] {
384 if unit_dep.unit == *direct_dep_unit {
385 return true;
386 }
387 queue.push_back(&unit_dep.unit);
388 }
389 }
390
391 false
392}