Skip to main content

cargo/core/compiler/
unused_deps.rs

1use std::collections::BTreeSet;
2
3use cargo_util_schemas::manifest;
4use cargo_util_terminal::report::AnnotationKind;
5use cargo_util_terminal::report::Group;
6use cargo_util_terminal::report::Level;
7use cargo_util_terminal::report::Origin;
8use cargo_util_terminal::report::Patch;
9use cargo_util_terminal::report::Snippet;
10use indexmap::IndexMap;
11use indexmap::IndexSet;
12use tracing::{debug, instrument, trace};
13
14use super::BuildRunner;
15use super::unit::Unit;
16use crate::core::Dependency;
17use crate::core::Package;
18use crate::core::PackageId;
19use crate::core::compiler::build_config::CompileMode;
20use crate::core::dependency::DepKind;
21use crate::core::manifest::TargetKind;
22use crate::lints::LintLevel;
23use crate::lints::get_key_value_span;
24use crate::lints::rel_cwd_manifest_path;
25use crate::lints::rules::unused_dependencies::LINT;
26use crate::util::errors::CargoResult;
27use crate::util::interning::InternedString;
28
29/// Track and translate `unused_externs` to `unused_dependencies`
30pub struct UnusedDepState {
31    states: IndexMap<PackageId, IndexMap<DepKind, DependenciesState>>,
32}
33
34impl UnusedDepState {
35    #[instrument(name = "UnusedDepState::new", skip_all)]
36    pub fn new(build_runner: &mut BuildRunner<'_, '_>) -> Self {
37        // Find all units for a package that can report unused externs
38        let mut root_build_script_builds = IndexSet::new();
39        let roots = &build_runner.bcx.roots;
40        for root in roots.iter() {
41            for build_script_run in build_runner.unit_deps(root).iter() {
42                if !build_script_run.unit.target.is_custom_build()
43                    && build_script_run.unit.pkg.package_id() != root.pkg.package_id()
44                {
45                    continue;
46                }
47                for build_script_build in build_runner.unit_deps(&build_script_run.unit).iter() {
48                    if !build_script_build.unit.target.is_custom_build()
49                        && build_script_build.unit.pkg.package_id() != root.pkg.package_id()
50                    {
51                        continue;
52                    }
53                    if build_script_build.unit.mode != CompileMode::Build {
54                        continue;
55                    }
56                    root_build_script_builds.insert(build_script_build.unit.clone());
57                }
58            }
59        }
60
61        trace!(
62            "selected dep kinds: {:?}",
63            build_runner.bcx.selected_dep_kinds
64        );
65        let mut states = IndexMap::<_, IndexMap<_, DependenciesState>>::new();
66        for root in roots.iter().chain(root_build_script_builds.iter()) {
67            let pkg_id = root.pkg.package_id();
68            let dep_kind = dep_kind_of(root);
69            if !build_runner.bcx.selected_dep_kinds.contains(dep_kind) {
70                trace!(
71                    "pkg {} v{} ({dep_kind:?}): ignoring unused deps due to non-exhaustive units",
72                    pkg_id.name(),
73                    pkg_id.version(),
74                );
75                continue;
76            }
77            trace!(
78                "tracking root {} {} ({:?})",
79                root.pkg.name(),
80                unit_desc(root),
81                dep_kind
82            );
83
84            let state = states
85                .entry(pkg_id)
86                .or_default()
87                .entry(dep_kind)
88                .or_default();
89            state.needed_units += 1;
90            for dep in build_runner.unit_deps(root).iter() {
91                trace!(
92                    "    => {} (deps={})",
93                    dep.unit.pkg.name(),
94                    dep.manifest_deps.0.is_some()
95                );
96                let manifest_deps = if let Some(manifest_deps) = &dep.manifest_deps.0 {
97                    Some(manifest_deps.clone())
98                } else if dep.unit.pkg.package_id() == root.pkg.package_id() {
99                    None
100                } else {
101                    continue;
102                };
103                state.externs.insert(
104                    dep.extern_crate_name,
105                    ExternState {
106                        unit: dep.unit.clone(),
107                        manifest_deps,
108                    },
109                );
110            }
111        }
112
113        Self { states }
114    }
115
116    pub fn record_unused_externs_for_unit(
117        &mut self,
118        unit: &Unit,
119        unused_externs: BTreeSet<InternedString>,
120    ) {
121        let pkg_id = unit.pkg.package_id();
122        let dep_kind = dep_kind_of(unit);
123        trace!(
124            "pkg {} v{} ({dep_kind:?}): unused externs {unused_externs:?}",
125            pkg_id.name(),
126            pkg_id.version(),
127        );
128        let state = self
129            .states
130            .entry(pkg_id)
131            .or_default()
132            .entry(dep_kind)
133            .or_default();
134        state.seen_units.push(unit.clone());
135        if let Some(existing) = state.unused_externs.as_mut() {
136            existing.retain(|ext| unused_externs.contains(ext));
137        } else {
138            state.unused_externs = Some(unused_externs);
139        }
140    }
141
142    #[instrument(skip_all)]
143    pub fn emit_unused_warnings(
144        &self,
145        warn_count: &mut usize,
146        error_count: &mut usize,
147        build_runner: &mut BuildRunner<'_, '_>,
148    ) -> CargoResult<()> {
149        for (pkg_id, states) in &self.states {
150            let Some(pkg) = self.get_package(pkg_id) else {
151                continue;
152            };
153            let toml_lints = pkg
154                .manifest()
155                .normalized_toml()
156                .lints
157                .clone()
158                .map(|lints| lints.lints)
159                .unwrap_or(manifest::TomlLints::default());
160            let cargo_lints = toml_lints
161                .get("cargo")
162                .cloned()
163                .unwrap_or(manifest::TomlToolLints::default());
164            let (lint_level, reason) = LINT.level(
165                &cargo_lints,
166                pkg.rust_version(),
167                pkg.manifest().unstable_features(),
168            );
169
170            if lint_level == LintLevel::Allow {
171                for (dep_kind, state) in states.iter() {
172                    for ext in state.unused_externs.iter().flatten() {
173                        debug!(
174                            "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, lint is allowed",
175                            pkg_id.name(),
176                            pkg_id.version(),
177                        );
178                    }
179                }
180                continue;
181            }
182
183            let manifest_path = rel_cwd_manifest_path(pkg.manifest_path(), build_runner.bcx.gctx);
184            let mut lint_count = 0;
185            for (dep_kind, state) in states.iter() {
186                for ext in state.unused_externs.iter().flatten() {
187                    let mut used_in_dev = false;
188                    match dep_kind {
189                        DepKind::Normal => {
190                            if let Some(state) = states.get(&DepKind::Development)
191                                && state
192                                    .unused_externs
193                                    .as_ref()
194                                    .is_some_and(|ue| !ue.contains(ext))
195                            {
196                                used_in_dev = true;
197                            }
198                        }
199                        DepKind::Development => {
200                            if let Some(state) = states.get(&DepKind::Normal)
201                                && state.externs.contains_key(ext)
202                            {
203                                trace!(
204                                    "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, inherited from normal dependency",
205                                    pkg_id.name(),
206                                    pkg_id.version(),
207                                );
208                                continue;
209                            }
210                        }
211                        DepKind::Build => {}
212                    }
213                    let Some(extern_state) = state.externs.get(ext) else {
214                        // not one we care to report
215                        debug!(
216                            "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, untracked dependent",
217                            pkg_id.name(),
218                            pkg_id.version(),
219                        );
220                        continue;
221                    };
222                    if state.seen_units.len() != state.needed_units {
223                        debug_assert_ne!(
224                            state.externs.len(),
225                            0,
226                            "assumes tracked is checked first"
227                        );
228                        // Some compilations errored without printing the unused externs.
229                        // Don't print the warning in order to reduce false positive
230                        // spam during errors.
231                        debug!(
232                            "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, {} outstanding units",
233                            pkg_id.name(),
234                            pkg_id.version(),
235                            state.needed_units - state.seen_units.len()
236                        );
237                        continue;
238                    }
239                    if is_transitive_dep(&extern_state.unit, &state.seen_units, build_runner) {
240                        debug!(
241                            "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, may be activating features",
242                            pkg_id.name(),
243                            pkg_id.version(),
244                        );
245                        continue;
246                    }
247
248                    // Implicitly added dependencies (in the same crate) aren't interesting
249                    let dependency = if let Some(dependency) = &extern_state.manifest_deps {
250                        dependency
251                    } else {
252                        continue;
253                    };
254                    for dependency in dependency {
255                        let manifest = pkg.manifest();
256                        let document = manifest.document();
257                        let contents = manifest.contents();
258                        let level = lint_level.to_diagnostic_level();
259                        let emitted_source = LINT.emitted_source(lint_level, reason);
260                        let toml_path = dependency.toml_path();
261
262                        let mut primary = Group::with_title(level.primary_title(LINT.desc));
263                        if let Some(document) = document
264                            && let Some(contents) = contents
265                            && let Some(span) = get_key_value_span(document, &toml_path)
266                        {
267                            let span = span.key.start..span.value.end;
268                            primary = primary.element(
269                                Snippet::source(contents)
270                                    .path(&manifest_path)
271                                    .annotation(AnnotationKind::Primary.span(span)),
272                            );
273                        } else {
274                            primary = primary.element(Origin::path(&manifest_path));
275                        }
276                        if lint_count == 0 {
277                            primary = primary.element(Level::NOTE.message(emitted_source));
278                        }
279                        lint_count += 1;
280                        let mut report = vec![primary];
281                        if let Some(document) = document
282                            && let Some(contents) = contents
283                            && let Some(span) = get_key_value_span(document, &toml_path)
284                        {
285                            let span = span.key.start..span.value.end;
286                            let mut help = Group::with_title(
287                                Level::HELP.secondary_title("remove the dependency"),
288                            );
289                            help = help.element(
290                                Snippet::source(contents)
291                                    .path(&manifest_path)
292                                    .patch(Patch::new(span, "")),
293                            );
294                            report.push(help);
295                        }
296                        if used_in_dev {
297                            let help = Group::with_title(Level::HELP.secondary_title(
298                                "to still use for development builds, move to `dev-dependencies`",
299                            ));
300                            report.push(help);
301                        }
302
303                        if lint_level.is_warn() {
304                            *warn_count += 1;
305                        }
306                        if lint_level.is_error() {
307                            *error_count += 1;
308                        }
309                        build_runner
310                            .bcx
311                            .gctx
312                            .shell()
313                            .print_report(&report, lint_level.force())?;
314                    }
315                }
316            }
317        }
318        Ok(())
319    }
320
321    fn get_package(&self, pkg_id: &PackageId) -> Option<&Package> {
322        let state = self.states.get(pkg_id)?;
323        let mut iter = state.values();
324        let state = iter.next()?;
325        let mut iter = state.seen_units.iter();
326        let unit = iter.next()?;
327        Some(&unit.pkg)
328    }
329}
330
331/// Track a package's [`DepKind`]
332#[derive(Default)]
333struct DependenciesState {
334    /// All declared dependencies
335    externs: IndexMap<InternedString, ExternState>,
336    /// Expected [`Self::seen_units`] entries to know we've received them all
337    ///
338    /// To avoid warning in cases where we didn't,
339    /// e.g. if a [`Unit`] errored and didn't report unused externs.
340    needed_units: usize,
341    /// Units that have reported their unused externs
342    seen_units: Vec<Unit>,
343    /// Intersection of unused externs across all [`Self::seen_units`]
344    unused_externs: Option<BTreeSet<InternedString>>,
345}
346
347#[derive(Clone)]
348struct ExternState {
349    unit: Unit,
350    manifest_deps: Option<Vec<Dependency>>,
351}
352
353fn dep_kind_of(unit: &Unit) -> DepKind {
354    match unit.target.kind() {
355        TargetKind::Lib(_) => match unit.mode {
356            // To support lib.rs with #[cfg(test)] use foo_crate as _;
357            CompileMode::Test => DepKind::Development,
358            _ => DepKind::Normal,
359        },
360        TargetKind::Bin => DepKind::Normal,
361        TargetKind::Test => DepKind::Development,
362        TargetKind::Bench => DepKind::Development,
363        TargetKind::ExampleLib(_) => DepKind::Development,
364        TargetKind::ExampleBin => DepKind::Development,
365        TargetKind::CustomBuild => DepKind::Build,
366    }
367}
368
369fn unit_desc(unit: &Unit) -> String {
370    format!(
371        "{}/{}+{:?}",
372        unit.target.name(),
373        unit.target.kind().description(),
374        unit.mode,
375    )
376}
377
378#[instrument(skip_all)]
379fn is_transitive_dep(
380    direct_dep_unit: &Unit,
381    seen_units: &Vec<Unit>,
382    build_runner: &mut BuildRunner<'_, '_>,
383) -> bool {
384    let mut queue = std::collections::VecDeque::new();
385    for root_unit in seen_units {
386        for unit_dep in build_runner.unit_deps(root_unit) {
387            if root_unit.pkg.package_id() == unit_dep.unit.pkg.package_id() {
388                continue;
389            }
390            if unit_dep.unit == *direct_dep_unit {
391                continue;
392            }
393            queue.push_back(&unit_dep.unit);
394        }
395    }
396
397    while let Some(dep_unit) = queue.pop_front() {
398        for unit_dep in build_runner.unit_deps(dep_unit) {
399            if unit_dep.unit == *direct_dep_unit {
400                return true;
401            }
402            queue.push_back(&unit_dep.unit);
403        }
404    }
405
406    false
407}