Skip to main content

cargo/diagnostics/rules/
unused_dependencies.rs

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/// 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 = 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                // not one we care to report
266                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                // Some compilations errored without printing the unused externs.
276                // Don't print the warning in order to reduce false positive
277                // spam during errors.
278                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            // Implicitly added dependencies (in the same crate) aren't interesting
296            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}