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