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::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/// 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 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        // HACK: as other rules are added to this pass, this needs to move up into the pass
213        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                // not one we care to report
267                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                // Some compilations errored without printing the unused externs.
277                // Don't print the warning in order to reduce false positive
278                // spam during errors.
279                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            // Implicitly added dependencies (in the same crate) aren't interesting
297            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}