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
27pub 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 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 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 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 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#[derive(Default)]
307struct DependenciesState {
308 externs: IndexMap<InternedString, ExternState>,
310 needed_units: Option<usize>,
315 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 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}