Skip to main content

cargo/diagnostics/rules/
unused_dependencies.rs

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/// Lint for `[build-dependencies]` without a `build.rs`
91///
92/// These are always unused.
93///
94/// This must be determined independent of the compiler since there are no build targets to pass to
95/// rustc to report on these.
96#[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                // not one we care to report
268                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                // Some compilations errored without printing the unused externs.
278                // Don't print the warning in order to reduce false positive
279                // spam during errors.
280                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            // Implicitly added dependencies (in the same crate) aren't interesting
298            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}