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