Skip to content

Commit 87915e7

Browse files
committed
feat: Avoid creating implicit features on edition 2024
1 parent e274101 commit 87915e7

File tree

9 files changed

+319
-69
lines changed

9 files changed

+319
-69
lines changed

src/cargo/util/lints.rs

Lines changed: 81 additions & 55 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;
@@ -254,71 +255,96 @@ pub fn unused_dependencies(
254255
) -> CargoResult<()> {
255256
let edition = pkg.manifest().edition();
256257
// Unused optional dependencies can only exist on edition 2024+
257-
if edition <= Edition::Edition2021 {
258+
if edition < Edition::Edition2024 {
258259
return Ok(());
259260
}
260261

261262
let lint_level = UNUSED_OPTIONAL_DEPENDENCY.level(lints, edition);
262263
if lint_level == LintLevel::Allow {
263264
return Ok(());
264265
}
265-
266+
let mut emitted_source = None;
266267
let manifest = pkg.manifest();
267-
let activated_opt_deps = manifest
268-
.resolved_toml()
269-
.features()
270-
.map(|map| {
271-
map.values()
272-
.flatten()
273-
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
274-
Dep { dep_name } => Some(dep_name.as_str()),
275-
_ => None,
276-
})
277-
.collect::<HashSet<_>>()
278-
})
279-
.unwrap_or_default();
268+
let original_toml = manifest.original_toml();
269+
// Unused dependencies were stripped from the manifest, leaving only the used ones
270+
let used_dependencies = manifest
271+
.dependencies()
272+
.into_iter()
273+
.map(|d| d.name_in_toml().to_string())
274+
.collect::<HashSet<String>>();
275+
let mut orig_deps = vec![
276+
(
277+
original_toml.dependencies.as_ref(),
278+
vec![DepKind::Normal.kind_table()],
279+
),
280+
(
281+
original_toml.dev_dependencies.as_ref(),
282+
vec![DepKind::Development.kind_table()],
283+
),
284+
(
285+
original_toml.build_dependencies.as_ref(),
286+
vec![DepKind::Build.kind_table()],
287+
),
288+
];
289+
for (name, platform) in original_toml.target.iter().flatten() {
290+
orig_deps.push((
291+
platform.dependencies.as_ref(),
292+
vec!["target", name, DepKind::Normal.kind_table()],
293+
));
294+
orig_deps.push((
295+
platform.dev_dependencies.as_ref(),
296+
vec!["target", name, DepKind::Development.kind_table()],
297+
));
298+
orig_deps.push((
299+
platform.build_dependencies.as_ref(),
300+
vec!["target", name, DepKind::Normal.kind_table()],
301+
));
302+
}
303+
for (deps, toml_path) in orig_deps {
304+
if let Some(deps) = deps {
305+
for name in deps.keys() {
306+
if !used_dependencies.contains(name.as_str()) {
307+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
308+
*error_count += 1;
309+
}
310+
let toml_path = toml_path
311+
.iter()
312+
.map(|s| *s)
313+
.chain(std::iter::once(name.as_str()))
314+
.collect::<Vec<_>>();
315+
let level = lint_level.to_diagnostic_level();
316+
let manifest_path = rel_cwd_manifest_path(path, gctx);
280317

281-
let mut emitted_source = None;
282-
for dep in manifest.dependencies() {
283-
let dep_name_in_toml = dep.name_in_toml();
284-
if !dep.is_optional() || activated_opt_deps.contains(dep_name_in_toml.as_str()) {
285-
continue;
286-
}
287-
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
288-
*error_count += 1;
289-
}
290-
let mut toml_path = vec![dep.kind().kind_table(), dep_name_in_toml.as_str()];
291-
let platform = dep.platform().map(|p| p.to_string());
292-
if let Some(platform) = platform.as_ref() {
293-
toml_path.insert(0, platform);
294-
toml_path.insert(0, "target");
295-
}
296-
let level = lint_level.to_diagnostic_level();
297-
let manifest_path = rel_cwd_manifest_path(path, gctx);
298-
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
299-
Snippet::source(manifest.contents())
300-
.origin(&manifest_path)
301-
.annotation(level.span(get_span(manifest.document(), &toml_path, false).unwrap()))
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()));
318+
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
319+
Snippet::source(manifest.contents())
320+
.origin(&manifest_path)
321+
.annotation(level.span(
322+
get_span(manifest.document(), toml_path.as_slice(), false).unwrap(),
323+
))
324+
.fold(true),
325+
);
326+
if emitted_source.is_none() {
327+
emitted_source = Some(format!(
328+
"`cargo::{}` is set to `{lint_level}`",
329+
UNUSED_OPTIONAL_DEPENDENCY.name
330+
));
331+
message =
332+
message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
333+
}
334+
let help = format!(
335+
"remove the dependency or activate it in a feature with `dep:{name}`"
336+
);
337+
message = message.footer(Level::Help.title(&help));
338+
let renderer = Renderer::styled().term_width(
339+
gctx.shell()
340+
.err_width()
341+
.diagnostic_terminal_width()
342+
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
343+
);
344+
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
345+
}
346+
}
310347
}
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))?;
322348
}
323349
Ok(())
324350
}

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

tests/testsuite/lints/unused_optional_dependencies/edition_2024/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use cargo_test_support::compare::assert_match_exact;
12
use cargo_test_support::prelude::*;
23
use cargo_test_support::registry::Package;
34
use cargo_test_support::str;
@@ -39,4 +40,16 @@ target-dep = { version = "0.1.0", optional = true }
3940
.success()
4041
.stdout_matches(str![""])
4142
.stderr_matches(file!["stderr.term.svg"]);
43+
44+
let expected_lockfile = r#"# This file is automatically @generated by Cargo.
45+
# It is not intended for manual editing.
46+
version = 3
47+
48+
[[package]]
49+
name = "foo"
50+
version = "0.1.0"
51+
"#;
52+
53+
let lock = p.read_lockfile();
54+
assert_match_exact(expected_lockfile, &lock);
4255
}

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

Lines changed: 4 additions & 8 deletions
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mod edition_2021;
22
mod edition_2024;
3+
mod renamed_deps;
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("bar", "0.2.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.2.0", package = "bar", 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)