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::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    pub fn new(build_runner: &mut BuildRunner<'_, '_>) -> Self {
34        let mut states = IndexMap::<_, IndexMap<_, DependenciesState>>::new();
35
36        let roots = &build_runner.bcx.roots;
37
38        // Find all units for a package that can report unused externs
39        let mut root_build_script_builds = IndexSet::new();
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        for root in roots.iter().chain(root_build_script_builds.iter()) {
66            let pkg_id = root.pkg.package_id();
67            let dep_kind = dep_kind_of(root);
68            if !build_runner.bcx.selected_dep_kinds.contains(dep_kind) {
69                trace!(
70                    "pkg {} v{} ({dep_kind:?}): ignoring unused deps due to non-exhaustive units",
71                    pkg_id.name(),
72                    pkg_id.version(),
73                );
74                continue;
75            }
76            trace!(
77                "tracking root {} {} ({:?})",
78                root.pkg.name(),
79                unit_desc(root),
80                dep_kind
81            );
82
83            let state = states
84                .entry(pkg_id)
85                .or_default()
86                .entry(dep_kind)
87                .or_default();
88            state.needed_units += 1;
89            for dep in build_runner.unit_deps(root).iter() {
90                trace!(
91                    "    => {} (deps={})",
92                    dep.unit.pkg.name(),
93                    dep.manifest_deps.0.is_some()
94                );
95                let manifest_deps = if let Some(manifest_deps) = &dep.manifest_deps.0 {
96                    Some(manifest_deps.clone())
97                } else if dep.unit.pkg.package_id() == root.pkg.package_id() {
98                    None
99                } else {
100                    continue;
101                };
102                state.externs.insert(dep.extern_crate_name, manifest_deps);
103            }
104        }
105
106        Self { states }
107    }
108
109    pub fn record_unused_externs_for_unit(&mut self, unit: &Unit, unused_externs: Vec<String>) {
110        let pkg_id = unit.pkg.package_id();
111        let kind = dep_kind_of(unit);
112        if let Some(state) = self.states.get_mut(&pkg_id).and_then(|s| s.get_mut(&kind)) {
113            state
114                .unused_externs
115                .entry(unit.clone())
116                .or_default()
117                .extend(unused_externs.into_iter().map(|s| InternedString::new(&s)));
118        }
119    }
120
121    pub fn emit_unused_warnings(
122        &self,
123        warn_count: &mut usize,
124        error_count: &mut usize,
125        build_runner: &mut BuildRunner<'_, '_>,
126    ) -> CargoResult<()> {
127        for (pkg_id, states) in &self.states {
128            let Some(pkg) = self.get_package(pkg_id) else {
129                continue;
130            };
131            let toml_lints = pkg
132                .manifest()
133                .normalized_toml()
134                .lints
135                .clone()
136                .map(|lints| lints.lints)
137                .unwrap_or(manifest::TomlLints::default());
138            let cargo_lints = toml_lints
139                .get("cargo")
140                .cloned()
141                .unwrap_or(manifest::TomlToolLints::default());
142            let (lint_level, reason) = LINT.level(
143                &cargo_lints,
144                pkg.rust_version(),
145                pkg.manifest().unstable_features(),
146            );
147
148            if lint_level == LintLevel::Allow {
149                continue;
150            }
151
152            let mut ignore = Vec::new();
153            if let Some(unused_dependencies) = cargo_lints.get("unused_dependencies") {
154                if let Some(config) = unused_dependencies.config() {
155                    if let Some(config_ignore) = config.get("ignore") {
156                        if let Ok(config_ignore) =
157                            toml::Value::try_into::<Vec<InternedString>>(config_ignore.clone())
158                        {
159                            ignore = config_ignore
160                        } else {
161                            anyhow::bail!(
162                                "`lints.cargo.unused_dependencies.ignore` must be a list of string"
163                            );
164                        }
165                    }
166                }
167            }
168
169            let manifest_path = rel_cwd_manifest_path(pkg.manifest_path(), build_runner.bcx.gctx);
170            let mut lint_count = 0;
171            for (dep_kind, state) in states.iter() {
172                if state.unused_externs.len() != state.needed_units {
173                    // Some compilations errored without printing the unused externs.
174                    // Don't print the warning in order to reduce false positive
175                    // spam during errors.
176                    trace!(
177                        "pkg {} v{} ({dep_kind:?}): ignoring unused deps due to {} outstanding units",
178                        pkg_id.name(),
179                        pkg_id.version(),
180                        state.needed_units
181                    );
182                    continue;
183                }
184
185                for (ext, dependency) in &state.externs {
186                    if state
187                        .unused_externs
188                        .values()
189                        .any(|unused| !unused.contains(ext))
190                    {
191                        trace!(
192                            "pkg {} v{} ({dep_kind:?}): extern {} is used",
193                            pkg_id.name(),
194                            pkg_id.version(),
195                            ext
196                        );
197                        continue;
198                    }
199
200                    // Implicitly added dependencies (in the same crate) aren't interesting
201                    let dependency = if let Some(dependency) = dependency {
202                        dependency
203                    } else {
204                        continue;
205                    };
206                    for dependency in dependency {
207                        if ignore.contains(&dependency.name_in_toml()) {
208                            trace!(
209                                "pkg {} v{} ({dep_kind:?}): extern {} is ignored",
210                                pkg_id.name(),
211                                pkg_id.version(),
212                                ext
213                            );
214                            continue;
215                        }
216
217                        let manifest = pkg.manifest();
218                        let document = manifest.document();
219                        let contents = manifest.contents();
220                        let level = lint_level.to_diagnostic_level();
221                        let emitted_source = LINT.emitted_source(lint_level, reason);
222                        let toml_path = dependency.toml_path();
223
224                        let mut primary = Group::with_title(level.primary_title(LINT.desc));
225                        if let Some(document) = document
226                            && let Some(contents) = contents
227                            && let Some(span) = get_key_value_span(document, &toml_path)
228                        {
229                            let span = span.key.start..span.value.end;
230                            primary = primary.element(
231                                Snippet::source(contents)
232                                    .path(&manifest_path)
233                                    .annotation(AnnotationKind::Primary.span(span)),
234                            );
235                        } else {
236                            primary = primary.element(Origin::path(&manifest_path));
237                        }
238                        if lint_count == 0 {
239                            primary = primary.element(Level::NOTE.message(emitted_source));
240                        }
241                        lint_count += 1;
242                        let mut report = vec![primary];
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                            let mut help = Group::with_title(
249                                Level::HELP.secondary_title("remove the dependency"),
250                            );
251                            help = help.element(
252                                Snippet::source(contents)
253                                    .path(&manifest_path)
254                                    .patch(Patch::new(span, "")),
255                            );
256                            report.push(help);
257                        }
258
259                        if lint_level.is_warn() {
260                            *warn_count += 1;
261                        }
262                        if lint_level.is_error() {
263                            *error_count += 1;
264                        }
265                        build_runner
266                            .bcx
267                            .gctx
268                            .shell()
269                            .print_report(&report, lint_level.force())?;
270                    }
271                }
272            }
273        }
274        Ok(())
275    }
276
277    fn get_package(&self, pkg_id: &PackageId) -> Option<&Package> {
278        let state = self.states.get(pkg_id)?;
279        let mut iter = state.values();
280        let state = iter.next()?;
281        let mut iter = state.unused_externs.keys();
282        let unit = iter.next()?;
283        Some(&unit.pkg)
284    }
285}
286
287/// Track a package's [`DepKind`]
288#[derive(Default)]
289struct DependenciesState {
290    /// All declared dependencies
291    externs: IndexMap<InternedString, Option<Vec<Dependency>>>,
292    /// Expected [`Self::unused_externs`] entries to know we've received them all
293    ///
294    /// To avoid warning in cases where we didn't,
295    /// e.g. if a [`Unit`] errored and didn't report unused externs.
296    needed_units: usize,
297    /// As reported by rustc
298    unused_externs: IndexMap<Unit, Vec<InternedString>>,
299}
300
301fn dep_kind_of(unit: &Unit) -> DepKind {
302    match unit.target.kind() {
303        TargetKind::Lib(_) => match unit.mode {
304            // To support lib.rs with #[cfg(test)] use foo_crate as _;
305            CompileMode::Test => DepKind::Development,
306            _ => DepKind::Normal,
307        },
308        TargetKind::Bin => DepKind::Normal,
309        TargetKind::Test => DepKind::Development,
310        TargetKind::Bench => DepKind::Development,
311        TargetKind::ExampleLib(_) => DepKind::Development,
312        TargetKind::ExampleBin => DepKind::Development,
313        TargetKind::CustomBuild => DepKind::Build,
314    }
315}
316
317fn unit_desc(unit: &Unit) -> String {
318    format!(
319        "{}/{}+{:?}",
320        unit.target.name(),
321        unit.target.kind().description(),
322        unit.mode,
323    )
324}