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
29pub 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 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 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 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 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#[derive(Default)]
333struct DependenciesState {
334 externs: IndexMap<InternedString, ExternState>,
336 needed_units: usize,
341 seen_units: Vec<Unit>,
343 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 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}