Skip to main content

cargo/diagnostics/rules/
unused_dependencies.rs

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