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::DiagnosticStats;
27use crate::diagnostics::Lint;
28use crate::diagnostics::LintLevel;
29use crate::diagnostics::LintLevelProduct;
30use crate::diagnostics::get_key_value_span;
31use crate::diagnostics::rel_cwd_manifest_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 DiagnosticStats,
103 gctx: &GlobalContext,
104) -> CargoResult<()> {
105 let LintLevelProduct {
106 level: lint_level,
107 source,
108 } = level;
109
110 let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
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 if let Some(document) = document
152 && let Some(contents) = contents
153 && let Some(span) = get_key_value_span(document, &["build-dependencies", dep_name])
154 {
155 let span = span.key.start..span.value.end;
156 let mut help = Group::with_title(Level::HELP.secondary_title("remove the dependency"));
157 help = help.element(
158 Snippet::source(contents)
159 .path(&manifest_path)
160 .patch(Patch::new(span, "")),
161 );
162 report.push(help);
163 }
164
165 pkg_stats.record_lint(lint_level);
166 gctx.shell().print_report(&report, lint_level.force())?;
167 }
168
169 Ok(())
170}
171
172#[instrument(skip_all)]
173pub fn lint_build_results(
174 build_runner: &BuildRunner<'_, '_>,
175 global_stats: &mut DiagnosticStats,
176) -> CargoResult<()> {
177 for (pkg_id, states) in &build_runner.unused_dep_state.states {
178 let Some(pkg) = get_package(&build_runner.unused_dep_state, pkg_id) else {
179 continue;
180 };
181 let toml_lints = pkg
182 .manifest()
183 .normalized_toml()
184 .lints
185 .clone()
186 .map(|lints| lints.lints)
187 .unwrap_or(manifest::TomlLints::default());
188 let cargo_lints = toml_lints
189 .get("cargo")
190 .cloned()
191 .unwrap_or(manifest::TomlToolLints::default());
192 let level = LINT.level(
193 &cargo_lints,
194 pkg.rust_version(),
195 pkg.manifest().unstable_features(),
196 );
197 if level.level == LintLevel::Allow {
198 for (dep_kind, state) in states.iter() {
199 for ext in state.unused_externs.iter().flatten() {
200 debug!(
201 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, lint is allowed",
202 pkg_id.name(),
203 pkg_id.version(),
204 );
205 }
206 }
207 continue;
208 }
209
210 let mut pkg_stats = DiagnosticStats::new();
211 lint_package_build_results(build_runner, pkg, states, level, &mut pkg_stats)?;
212 if let Err(error) =
214 pkg_stats.report_summary("finalize", Some(&*pkg.name()), build_runner.bcx.gctx)
215 {
216 build_runner.bcx.gctx.shell().error(error)?;
217 }
218 *global_stats += pkg_stats;
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 DiagnosticStats,
229) -> CargoResult<()> {
230 let mut lint_count = 0;
231 let LintLevelProduct {
232 level: lint_level,
233 source,
234 } = level;
235 let manifest_path = rel_cwd_manifest_path(pkg.manifest_path(), build_runner.bcx.gctx);
236 let pkg_id = pkg.package_id();
237 for (dep_kind, state) in states.iter() {
238 for ext in state.unused_externs.iter().flatten() {
239 let mut used_in_dev = false;
240 match dep_kind {
241 DepKind::Normal => {
242 if let Some(state) = states.get(&DepKind::Development)
243 && state
244 .unused_externs
245 .as_ref()
246 .is_some_and(|ue| !ue.contains(ext))
247 {
248 used_in_dev = true;
249 }
250 }
251 DepKind::Development => {
252 if let Some(state) = states.get(&DepKind::Normal)
253 && state.externs.contains_key(ext)
254 {
255 trace!(
256 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, inherited from normal dependency",
257 pkg_id.name(),
258 pkg_id.version(),
259 );
260 continue;
261 }
262 }
263 DepKind::Build => {}
264 }
265 let Some(extern_state) = state.externs.get(ext) else {
266 debug!(
268 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, untracked dependent",
269 pkg_id.name(),
270 pkg_id.version(),
271 );
272 continue;
273 };
274 if state.seen_units.len() != state.needed_units {
275 debug_assert_ne!(state.externs.len(), 0, "assumes tracked is checked first");
276 debug!(
280 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, {} outstanding units",
281 pkg_id.name(),
282 pkg_id.version(),
283 state.needed_units - state.seen_units.len()
284 );
285 continue;
286 }
287 if is_transitive_dep(&extern_state.unit, &state.seen_units, build_runner.bcx) {
288 debug!(
289 "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, may be activating features",
290 pkg_id.name(),
291 pkg_id.version(),
292 );
293 continue;
294 }
295
296 let dependency = if let Some(dependency) = &extern_state.manifest_deps {
298 dependency
299 } else {
300 continue;
301 };
302 for dependency in dependency {
303 let manifest = pkg.manifest();
304 let document = manifest.document();
305 let contents = manifest.contents();
306 let level = lint_level.to_diagnostic_level();
307 let emitted_source = LINT.emitted_source(lint_level, source);
308 let toml_path = dependency.toml_path();
309
310 let mut primary = Group::with_title(level.primary_title(LINT.desc));
311 if let Some(document) = document
312 && let Some(contents) = contents
313 && let Some(span) = get_key_value_span(document, &toml_path)
314 {
315 let span = span.key.start..span.value.end;
316 primary = primary.element(
317 Snippet::source(contents)
318 .path(&manifest_path)
319 .annotation(AnnotationKind::Primary.span(span)),
320 );
321 } else {
322 primary = primary.element(Origin::path(&manifest_path));
323 }
324 if lint_count == 0 {
325 primary = primary.element(Level::NOTE.message(emitted_source));
326 }
327 lint_count += 1;
328 let mut report = vec![primary];
329 if let Some(document) = document
330 && let Some(contents) = contents
331 && let Some(span) = get_key_value_span(document, &toml_path)
332 {
333 let span = span.key.start..span.value.end;
334 let mut help =
335 Group::with_title(Level::HELP.secondary_title("remove the dependency"));
336 help = help.element(
337 Snippet::source(contents)
338 .path(&manifest_path)
339 .patch(Patch::new(span, "")),
340 );
341 report.push(help);
342 }
343 if used_in_dev {
344 let help = Group::with_title(Level::HELP.secondary_title(
345 "to still use for development builds, move to `dev-dependencies`",
346 ));
347 report.push(help);
348 }
349
350 pkg_stats.record_lint(lint_level);
351 build_runner
352 .bcx
353 .gctx
354 .shell()
355 .print_report(&report, lint_level.force())?;
356 }
357 }
358 }
359 Ok(())
360}
361
362fn get_package<'s>(
363 unused_dep_state: &'s UnusedDepState,
364 pkg_id: &PackageId,
365) -> Option<&'s Package> {
366 let state = unused_dep_state.states.get(pkg_id)?;
367 let mut iter = state.values();
368 let state = iter.next()?;
369 let mut iter = state.seen_units.iter();
370 let unit = iter.next()?;
371 Some(&unit.pkg)
372}
373
374#[instrument(skip_all)]
375fn is_transitive_dep(
376 direct_dep_unit: &Unit,
377 seen_units: &Vec<Unit>,
378 bcx: &BuildContext<'_, '_>,
379) -> bool {
380 let mut queue = std::collections::VecDeque::new();
381 for root_unit in seen_units {
382 for unit_dep in &bcx.unit_graph[root_unit] {
383 if root_unit.pkg.package_id() == unit_dep.unit.pkg.package_id() {
384 continue;
385 }
386 if unit_dep.unit == *direct_dep_unit {
387 continue;
388 }
389 queue.push_back(&unit_dep.unit);
390 }
391 }
392
393 while let Some(dep_unit) = queue.pop_front() {
394 for unit_dep in &bcx.unit_graph[dep_unit] {
395 if unit_dep.unit == *direct_dep_unit {
396 return true;
397 }
398 queue.push_back(&unit_dep.unit);
399 }
400 }
401
402 false
403}