Skip to main content

cargo/core/compiler/
unused_deps.rs

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