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
30pub 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 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 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 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 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#[derive(Default)]
337struct DependenciesState {
338 externs: IndexMap<InternedString, ExternState>,
340 needed_units: usize,
345 seen_units: Vec<Unit>,
347 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 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}