Skip to content

Commit d2f1fb5

Browse files
committed
feat: Remove unused deps in edition >= 2024
1 parent cb4fc60 commit d2f1fb5

File tree

9 files changed

+349
-70
lines changed

9 files changed

+349
-70
lines changed

src/cargo/util/lints.rs

Lines changed: 79 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::core::dependency::DepKind;
12
use crate::core::FeatureValue::Dep;
23
use crate::core::{Edition, FeatureValue, Package};
34
use crate::util::interning::InternedString;
@@ -253,72 +254,87 @@ pub fn unused_dependencies(
253254
if lint_level == LintLevel::Allow {
254255
return Ok(());
255256
}
256-
257-
let manifest = pkg.manifest();
258-
let user_defined_features = manifest.resolved_toml().features();
259-
let features = user_defined_features.map_or(HashSet::new(), |f| {
260-
f.keys().map(|k| InternedString::new(&k)).collect()
261-
});
262-
// Add implicit features for optional dependencies if they weren't
263-
// explicitly listed anywhere.
264-
let explicitly_listed = user_defined_features.map_or(HashSet::new(), |f| {
265-
f.values()
266-
.flatten()
267-
.filter_map(|v| match FeatureValue::new(v.into()) {
268-
Dep { dep_name } => Some(dep_name),
269-
_ => None,
270-
})
271-
.collect()
272-
});
273-
274257
let mut emitted_source = None;
275-
for dep in manifest.dependencies() {
276-
let dep_name_in_toml = dep.name_in_toml();
277-
if !dep.is_optional()
278-
|| features.contains(&dep_name_in_toml)
279-
|| explicitly_listed.contains(&dep_name_in_toml)
280-
{
281-
continue;
282-
}
283-
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
284-
*error_count += 1;
285-
}
286-
let level = lint_level.to_diagnostic_level();
287-
let manifest_path = rel_cwd_manifest_path(path, gctx);
258+
let manifest = pkg.manifest();
259+
let original_toml = manifest.original_toml();
260+
let all_dependencies = manifest
261+
.dependencies()
262+
.into_iter()
263+
.map(|d| d.name_in_toml().to_string())
264+
.collect::<HashSet<String>>();
265+
let mut orig_deps = vec![
266+
(
267+
original_toml.dependencies.as_ref(),
268+
vec![DepKind::Normal.kind_table()],
269+
),
270+
(
271+
original_toml.dev_dependencies.as_ref(),
272+
vec![DepKind::Development.kind_table()],
273+
),
274+
(
275+
original_toml.build_dependencies.as_ref(),
276+
vec![DepKind::Build.kind_table()],
277+
),
278+
];
279+
for (name, platform) in original_toml.target.iter().flatten() {
280+
orig_deps.push((
281+
platform.dependencies.as_ref(),
282+
vec!["target", name, DepKind::Normal.kind_table()],
283+
));
284+
orig_deps.push((
285+
platform.dev_dependencies.as_ref(),
286+
vec!["target", name, DepKind::Development.kind_table()],
287+
));
288+
orig_deps.push((
289+
platform.build_dependencies.as_ref(),
290+
vec!["target", name, DepKind::Normal.kind_table()],
291+
));
292+
}
293+
for (deps, toml_path) in orig_deps {
294+
if let Some(deps) = deps {
295+
for (name, _) in deps {
296+
if !all_dependencies.contains(name.as_str()) {
297+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
298+
*error_count += 1;
299+
}
300+
let toml_path = toml_path
301+
.iter()
302+
.map(|s| *s)
303+
.chain(std::iter::once(name.as_str()))
304+
.collect::<Vec<_>>();
305+
let level = lint_level.to_diagnostic_level();
306+
let manifest_path = rel_cwd_manifest_path(path, gctx);
288307

289-
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
290-
Snippet::source(manifest.contents())
291-
.origin(&manifest_path)
292-
.annotation(
293-
level.span(
294-
get_span(
295-
manifest.document(),
296-
&["dependencies", &dep_name_in_toml],
297-
false,
298-
)
299-
.unwrap(),
300-
),
301-
)
302-
.fold(true),
303-
);
304-
if emitted_source.is_none() {
305-
emitted_source = Some(format!(
306-
"`cargo::{}` is set to `{lint_level}`",
307-
UNUSED_OPTIONAL_DEPENDENCY.name
308-
));
309-
message = message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
308+
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
309+
Snippet::source(manifest.contents())
310+
.origin(&manifest_path)
311+
.annotation(level.span(
312+
get_span(manifest.document(), toml_path.as_slice(), false).unwrap(),
313+
))
314+
.fold(true),
315+
);
316+
if emitted_source.is_none() {
317+
emitted_source = Some(format!(
318+
"`cargo::{}` is set to `{lint_level}`",
319+
UNUSED_OPTIONAL_DEPENDENCY.name
320+
));
321+
message =
322+
message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
323+
}
324+
let help = format!(
325+
"remove the dependency or activate it in a feature with `dep:{name}`"
326+
);
327+
message = message.footer(Level::Help.title(&help));
328+
let renderer = Renderer::styled().term_width(
329+
gctx.shell()
330+
.err_width()
331+
.diagnostic_terminal_width()
332+
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
333+
);
334+
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
335+
}
336+
}
310337
}
311-
let help = format!(
312-
"remove the dependency or activate it in a feature with `dep:{dep_name_in_toml}`"
313-
);
314-
message = message.footer(Level::Help.title(&help));
315-
let renderer = Renderer::styled().term_width(
316-
gctx.shell()
317-
.err_width()
318-
.diagnostic_terminal_width()
319-
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
320-
);
321-
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
322338
}
323339
Ok(())
324340
}

src/cargo/util/toml/mod.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use annotate_snippets::{Level, Renderer, Snippet};
2-
use std::collections::{BTreeMap, BTreeSet, HashMap};
2+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
33
use std::ffi::OsStr;
44
use std::path::{Path, PathBuf};
55
use std::rc::Rc;
@@ -20,6 +20,7 @@ use crate::core::compiler::{CompileKind, CompileTarget};
2020
use crate::core::dependency::{Artifact, ArtifactTarget, DepKind};
2121
use crate::core::manifest::{ManifestMetadata, TargetSourcePath};
2222
use crate::core::resolver::ResolveBehavior;
23+
use crate::core::FeatureValue::Dep;
2324
use crate::core::{find_workspace_root, resolve_relative_path, CliUnstable, FeatureValue};
2425
use crate::core::{Dependency, Manifest, Package, PackageId, Summary, Target};
2526
use crate::core::{Edition, EitherManifest, Feature, Features, VirtualManifest, Workspace};
@@ -300,30 +301,56 @@ fn resolve_toml(
300301

301302
if let Some(original_package) = original_toml.package() {
302303
let resolved_package = resolve_package_toml(original_package, package_root, &inherit)?;
304+
let edition = resolved_package
305+
.resolved_edition()
306+
.expect("previously resolved")
307+
.map_or(Edition::default(), |e| {
308+
Edition::from_str(&e).unwrap_or_default()
309+
});
303310
resolved_toml.package = Some(resolved_package);
304311

312+
let activated_opt_deps = resolved_toml
313+
.features
314+
.as_ref()
315+
.map(|map| {
316+
map.values()
317+
.flatten()
318+
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
319+
Dep { dep_name } => Some(dep_name.as_str()),
320+
_ => None,
321+
})
322+
.collect::<HashSet<_>>()
323+
})
324+
.unwrap_or_default();
325+
305326
resolved_toml.dependencies = resolve_dependencies(
306327
gctx,
328+
edition,
307329
&features,
308330
original_toml.dependencies.as_ref(),
331+
&activated_opt_deps,
309332
None,
310333
&inherit,
311334
package_root,
312335
warnings,
313336
)?;
314337
resolved_toml.dev_dependencies = resolve_dependencies(
315338
gctx,
339+
edition,
316340
&features,
317341
original_toml.dev_dependencies(),
342+
&activated_opt_deps,
318343
Some(DepKind::Development),
319344
&inherit,
320345
package_root,
321346
warnings,
322347
)?;
323348
resolved_toml.build_dependencies = resolve_dependencies(
324349
gctx,
350+
edition,
325351
&features,
326352
original_toml.build_dependencies(),
353+
&activated_opt_deps,
327354
Some(DepKind::Build),
328355
&inherit,
329356
package_root,
@@ -333,26 +360,32 @@ fn resolve_toml(
333360
for (name, platform) in original_toml.target.iter().flatten() {
334361
let resolved_dependencies = resolve_dependencies(
335362
gctx,
363+
edition,
336364
&features,
337365
platform.dependencies.as_ref(),
366+
&activated_opt_deps,
338367
None,
339368
&inherit,
340369
package_root,
341370
warnings,
342371
)?;
343372
let resolved_dev_dependencies = resolve_dependencies(
344373
gctx,
374+
edition,
345375
&features,
346376
platform.dev_dependencies(),
377+
&activated_opt_deps,
347378
Some(DepKind::Development),
348379
&inherit,
349380
package_root,
350381
warnings,
351382
)?;
352383
let resolved_build_dependencies = resolve_dependencies(
353384
gctx,
385+
edition,
354386
&features,
355387
platform.build_dependencies(),
388+
&activated_opt_deps,
356389
Some(DepKind::Build),
357390
&inherit,
358391
package_root,
@@ -561,8 +594,10 @@ fn default_readme_from_package_root(package_root: &Path) -> Option<String> {
561594
#[tracing::instrument(skip_all)]
562595
fn resolve_dependencies<'a>(
563596
gctx: &GlobalContext,
597+
edition: Edition,
564598
features: &Features,
565599
orig_deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>,
600+
activated_opt_deps: &HashSet<&str>,
566601
kind: Option<DepKind>,
567602
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
568603
package_root: &Path,
@@ -610,10 +645,18 @@ fn resolve_dependencies<'a>(
610645
}
611646
}
612647

613-
deps.insert(
614-
name_in_toml.clone(),
615-
manifest::InheritableDependency::Value(resolved.clone()),
616-
);
648+
// if the dependency is not optional, it is always used
649+
// if the dependency is optional and activated, it is used
650+
// if the dependency is optional and not activated, it is not used
651+
let is_dep_activated =
652+
!resolved.is_optional() || activated_opt_deps.contains(name_in_toml.as_str());
653+
// If the edition is less than 2024, we don't need to check for unused optional dependencies
654+
if edition < Edition::Edition2024 || is_dep_activated {
655+
deps.insert(
656+
name_in_toml.clone(),
657+
manifest::InheritableDependency::Value(resolved.clone()),
658+
);
659+
}
617660
}
618661
Ok(Some(deps))
619662
}

tests/testsuite/lints/implicit_features/edition_2024/stderr.term.svg

Lines changed: 1 addition & 1 deletion
Loading
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use cargo_test_support::prelude::*;
2+
use cargo_test_support::registry::Package;
3+
use cargo_test_support::str;
4+
use cargo_test_support::{file, project};
5+
6+
#[cargo_test(nightly, reason = "edition2024 is not stable")]
7+
fn case() {
8+
Package::new("bar", "0.1.0").publish();
9+
Package::new("baz", "0.1.0").publish();
10+
Package::new("target-dep", "0.1.0").publish();
11+
let p = project()
12+
.file(
13+
"Cargo.toml",
14+
r#"
15+
cargo-features = ["edition2024"]
16+
[package]
17+
name = "foo"
18+
version = "0.1.0"
19+
edition = "2024"
20+
21+
[dependencies]
22+
bar = { version = "0.1.0", optional = true }
23+
24+
[build-dependencies]
25+
baz = { version = "0.1.0", optional = true }
26+
27+
[target.'cfg(target_os = "linux")'.dependencies]
28+
target-dep = { version = "0.1.0", optional = true }
29+
"#,
30+
)
31+
.file("src/lib.rs", "")
32+
.build();
33+
34+
snapbox::cmd::Command::cargo_ui()
35+
.masquerade_as_nightly_cargo(&["edition2024"])
36+
.current_dir(p.root())
37+
.arg("check")
38+
.assert()
39+
.success()
40+
.stdout_matches(str![""])
41+
.stderr_matches(file!["stderr.term.svg"]);
42+
}

0 commit comments

Comments
 (0)