feat: Avoid creating implicit features on edition 2024

This commit is contained in:
Scott Schafer 2024-04-18 12:23:08 -06:00
parent e2741019c2
commit 87915e71fe
No known key found for this signature in database
9 changed files with 320 additions and 70 deletions

View File

@ -1,3 +1,4 @@
use crate::core::dependency::DepKind;
use crate::core::FeatureValue::Dep;
use crate::core::{Edition, FeatureValue, Package};
use crate::util::interning::InternedString;
@ -254,7 +255,7 @@ pub fn unused_dependencies(
) -> CargoResult<()> {
let edition = pkg.manifest().edition();
// Unused optional dependencies can only exist on edition 2024+
if edition <= Edition::Edition2021 {
if edition < Edition::Edition2024 {
return Ok(());
}
@ -262,63 +263,88 @@ pub fn unused_dependencies(
if lint_level == LintLevel::Allow {
return Ok(());
}
let manifest = pkg.manifest();
let activated_opt_deps = manifest
.resolved_toml()
.features()
.map(|map| {
map.values()
.flatten()
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
Dep { dep_name } => Some(dep_name.as_str()),
_ => None,
})
.collect::<HashSet<_>>()
})
.unwrap_or_default();
let mut emitted_source = None;
for dep in manifest.dependencies() {
let dep_name_in_toml = dep.name_in_toml();
if !dep.is_optional() || activated_opt_deps.contains(dep_name_in_toml.as_str()) {
continue;
let manifest = pkg.manifest();
let original_toml = manifest.original_toml();
// Unused dependencies were stripped from the manifest, leaving only the used ones
let used_dependencies = manifest
.dependencies()
.into_iter()
.map(|d| d.name_in_toml().to_string())
.collect::<HashSet<String>>();
let mut orig_deps = vec![
(
original_toml.dependencies.as_ref(),
vec![DepKind::Normal.kind_table()],
),
(
original_toml.dev_dependencies.as_ref(),
vec![DepKind::Development.kind_table()],
),
(
original_toml.build_dependencies.as_ref(),
vec![DepKind::Build.kind_table()],
),
];
for (name, platform) in original_toml.target.iter().flatten() {
orig_deps.push((
platform.dependencies.as_ref(),
vec!["target", name, DepKind::Normal.kind_table()],
));
orig_deps.push((
platform.dev_dependencies.as_ref(),
vec!["target", name, DepKind::Development.kind_table()],
));
orig_deps.push((
platform.build_dependencies.as_ref(),
vec!["target", name, DepKind::Normal.kind_table()],
));
}
for (deps, toml_path) in orig_deps {
if let Some(deps) = deps {
for name in deps.keys() {
if !used_dependencies.contains(name.as_str()) {
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
*error_count += 1;
}
let toml_path = toml_path
.iter()
.map(|s| *s)
.chain(std::iter::once(name.as_str()))
.collect::<Vec<_>>();
let level = lint_level.to_diagnostic_level();
let manifest_path = rel_cwd_manifest_path(path, gctx);
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
Snippet::source(manifest.contents())
.origin(&manifest_path)
.annotation(level.span(
get_span(manifest.document(), toml_path.as_slice(), false).unwrap(),
))
.fold(true),
);
if emitted_source.is_none() {
emitted_source = Some(format!(
"`cargo::{}` is set to `{lint_level}`",
UNUSED_OPTIONAL_DEPENDENCY.name
));
message =
message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
}
let help = format!(
"remove the dependency or activate it in a feature with `dep:{name}`"
);
message = message.footer(Level::Help.title(&help));
let renderer = Renderer::styled().term_width(
gctx.shell()
.err_width()
.diagnostic_terminal_width()
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
);
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
}
}
}
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
*error_count += 1;
}
let mut toml_path = vec![dep.kind().kind_table(), dep_name_in_toml.as_str()];
let platform = dep.platform().map(|p| p.to_string());
if let Some(platform) = platform.as_ref() {
toml_path.insert(0, platform);
toml_path.insert(0, "target");
}
let level = lint_level.to_diagnostic_level();
let manifest_path = rel_cwd_manifest_path(path, gctx);
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
Snippet::source(manifest.contents())
.origin(&manifest_path)
.annotation(level.span(get_span(manifest.document(), &toml_path, false).unwrap()))
.fold(true),
);
if emitted_source.is_none() {
emitted_source = Some(format!(
"`cargo::{}` is set to `{lint_level}`",
UNUSED_OPTIONAL_DEPENDENCY.name
));
message = message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
}
let help = format!(
"remove the dependency or activate it in a feature with `dep:{dep_name_in_toml}`"
);
message = message.footer(Level::Help.title(&help));
let renderer = Renderer::styled().term_width(
gctx.shell()
.err_width()
.diagnostic_terminal_width()
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
);
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
}
Ok(())
}

View File

@ -1,5 +1,5 @@
use annotate_snippets::{Level, Renderer, Snippet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::rc::Rc;
@ -20,6 +20,7 @@ use crate::core::compiler::{CompileKind, CompileTarget};
use crate::core::dependency::{Artifact, ArtifactTarget, DepKind};
use crate::core::manifest::{ManifestMetadata, TargetSourcePath};
use crate::core::resolver::ResolveBehavior;
use crate::core::FeatureValue::Dep;
use crate::core::{find_workspace_root, resolve_relative_path, CliUnstable, FeatureValue};
use crate::core::{Dependency, Manifest, Package, PackageId, Summary, Target};
use crate::core::{Edition, EitherManifest, Feature, Features, VirtualManifest, Workspace};
@ -300,12 +301,34 @@ fn resolve_toml(
if let Some(original_package) = original_toml.package() {
let resolved_package = resolve_package_toml(original_package, package_root, &inherit)?;
let edition = resolved_package
.resolved_edition()
.expect("previously resolved")
.map_or(Edition::default(), |e| {
Edition::from_str(&e).unwrap_or_default()
});
resolved_toml.package = Some(resolved_package);
let activated_opt_deps = resolved_toml
.features
.as_ref()
.map(|map| {
map.values()
.flatten()
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
Dep { dep_name } => Some(dep_name.as_str()),
_ => None,
})
.collect::<HashSet<_>>()
})
.unwrap_or_default();
resolved_toml.dependencies = resolve_dependencies(
gctx,
edition,
&features,
original_toml.dependencies.as_ref(),
&activated_opt_deps,
None,
&inherit,
package_root,
@ -313,8 +336,10 @@ fn resolve_toml(
)?;
resolved_toml.dev_dependencies = resolve_dependencies(
gctx,
edition,
&features,
original_toml.dev_dependencies(),
&activated_opt_deps,
Some(DepKind::Development),
&inherit,
package_root,
@ -322,8 +347,10 @@ fn resolve_toml(
)?;
resolved_toml.build_dependencies = resolve_dependencies(
gctx,
edition,
&features,
original_toml.build_dependencies(),
&activated_opt_deps,
Some(DepKind::Build),
&inherit,
package_root,
@ -333,8 +360,10 @@ fn resolve_toml(
for (name, platform) in original_toml.target.iter().flatten() {
let resolved_dependencies = resolve_dependencies(
gctx,
edition,
&features,
platform.dependencies.as_ref(),
&activated_opt_deps,
None,
&inherit,
package_root,
@ -342,8 +371,10 @@ fn resolve_toml(
)?;
let resolved_dev_dependencies = resolve_dependencies(
gctx,
edition,
&features,
platform.dev_dependencies(),
&activated_opt_deps,
Some(DepKind::Development),
&inherit,
package_root,
@ -351,8 +382,10 @@ fn resolve_toml(
)?;
let resolved_build_dependencies = resolve_dependencies(
gctx,
edition,
&features,
platform.build_dependencies(),
&activated_opt_deps,
Some(DepKind::Build),
&inherit,
package_root,
@ -561,8 +594,10 @@ fn default_readme_from_package_root(package_root: &Path) -> Option<String> {
#[tracing::instrument(skip_all)]
fn resolve_dependencies<'a>(
gctx: &GlobalContext,
edition: Edition,
features: &Features,
orig_deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>,
activated_opt_deps: &HashSet<&str>,
kind: Option<DepKind>,
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
package_root: &Path,
@ -610,10 +645,18 @@ fn resolve_dependencies<'a>(
}
}
deps.insert(
name_in_toml.clone(),
manifest::InheritableDependency::Value(resolved.clone()),
);
// if the dependency is not optional, it is always used
// if the dependency is optional and activated, it is used
// if the dependency is optional and not activated, it is not used
let is_dep_activated =
!resolved.is_optional() || activated_opt_deps.contains(name_in_toml.as_str());
// If the edition is less than 2024, we don't need to check for unused optional dependencies
if edition < Edition::Edition2024 || is_dep_activated {
deps.insert(
name_in_toml.clone(),
manifest::InheritableDependency::Value(resolved.clone()),
);
}
}
Ok(Some(deps))
}

View File

@ -20,7 +20,7 @@
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan class="fg-green bold"> Updating</tspan><tspan> `dummy-registry` index</tspan>
</tspan>
<tspan x="10px" y="46px"><tspan class="fg-green bold"> Locking</tspan><tspan> 3 packages to latest compatible versions</tspan>
<tspan x="10px" y="46px"><tspan class="fg-green bold"> Locking</tspan><tspan> 2 packages to latest compatible versions</tspan>
</tspan>
<tspan x="10px" y="64px"><tspan class="fg-green bold"> Checking</tspan><tspan> foo v0.1.0 ([ROOT]/foo)</tspan>
</tspan>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,3 +1,4 @@
use cargo_test_support::compare::assert_match_exact;
use cargo_test_support::prelude::*;
use cargo_test_support::registry::Package;
use cargo_test_support::str;
@ -39,4 +40,16 @@ target-dep = { version = "0.1.0", optional = true }
.success()
.stdout_matches(str![""])
.stderr_matches(file!["stderr.term.svg"]);
let expected_lockfile = r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "foo"
version = "0.1.0"
"#;
let lock = p.read_lockfile();
assert_match_exact(expected_lockfile, &lock);
}

View File

@ -1,4 +1,4 @@
<svg width="740px" height="506px" xmlns="http://www.w3.org/2000/svg">
<svg width="740px" height="470px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
@ -66,15 +66,11 @@
</tspan>
<tspan x="10px" y="406px"><tspan> </tspan><tspan class="fg-bright-blue bold">=</tspan><tspan> </tspan><tspan class="fg-bright-cyan bold">help</tspan><tspan>: remove the dependency or activate it in a feature with `dep:target-dep`</tspan>
</tspan>
<tspan x="10px" y="424px"><tspan class="fg-green bold"> Updating</tspan><tspan> `dummy-registry` index</tspan>
<tspan x="10px" y="424px"><tspan class="fg-green bold"> Checking</tspan><tspan> foo v0.1.0 ([ROOT]/foo)</tspan>
</tspan>
<tspan x="10px" y="442px"><tspan class="fg-green bold"> Locking</tspan><tspan> 4 packages to latest compatible versions</tspan>
<tspan x="10px" y="442px"><tspan class="fg-green bold"> Finished</tspan><tspan> [..]</tspan>
</tspan>
<tspan x="10px" y="460px"><tspan class="fg-green bold"> Checking</tspan><tspan> foo v0.1.0 ([ROOT]/foo)</tspan>
</tspan>
<tspan x="10px" y="478px"><tspan class="fg-green bold"> Finished</tspan><tspan> `dev` profile [unoptimized + debuginfo] target(s) in 0.08s</tspan>
</tspan>
<tspan x="10px" y="496px">
<tspan x="10px" y="460px">
</tspan>
</text>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,2 +1,3 @@
mod edition_2021;
mod edition_2024;
mod renamed_deps;

View File

@ -0,0 +1,42 @@
use cargo_test_support::prelude::*;
use cargo_test_support::registry::Package;
use cargo_test_support::str;
use cargo_test_support::{file, project};
#[cargo_test(nightly, reason = "edition2024 is not stable")]
fn case() {
Package::new("bar", "0.1.0").publish();
Package::new("bar", "0.2.0").publish();
Package::new("target-dep", "0.1.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
cargo-features = ["edition2024"]
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
[dependencies]
bar = { version = "0.1.0", optional = true }
[build-dependencies]
baz = { version = "0.2.0", package = "bar", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
target-dep = { version = "0.1.0", optional = true }
"#,
)
.file("src/lib.rs", "")
.build();
snapbox::cmd::Command::cargo_ui()
.masquerade_as_nightly_cargo(&["edition2024"])
.current_dir(p.root())
.arg("check")
.assert()
.success()
.stdout_matches(str![""])
.stderr_matches(file!["stderr.term.svg"]);
}

View File

@ -0,0 +1,77 @@
<svg width="740px" height="470px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-cyan { fill: #55FFFF }
.fg-bright-green { fill: #55FF55 }
.fg-green { fill: #00AA00 }
.fg-yellow { fill: #AA5500 }
.container {
padding: 0 10px;
line-height: 18px;
}
.bold { font-weight: bold; }
tspan {
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: pre;
line-height: 18px;
}
</style>
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan class="fg-yellow bold">warning</tspan><tspan>: </tspan><tspan class="bold">unused optional dependency</tspan>
</tspan>
<tspan x="10px" y="46px"><tspan> </tspan><tspan class="fg-bright-blue bold">--&gt;</tspan><tspan> Cargo.toml:9:1</tspan>
</tspan>
<tspan x="10px" y="64px"><tspan class="fg-bright-blue bold"> |</tspan>
</tspan>
<tspan x="10px" y="82px"><tspan class="fg-bright-blue bold">9 |</tspan><tspan> bar = { version = "0.1.0", optional = true }</tspan>
</tspan>
<tspan x="10px" y="100px"><tspan class="fg-bright-blue bold"> |</tspan><tspan class="fg-yellow bold"> ---</tspan>
</tspan>
<tspan x="10px" y="118px"><tspan class="fg-bright-blue bold"> |</tspan>
</tspan>
<tspan x="10px" y="136px"><tspan> </tspan><tspan class="fg-bright-blue bold">=</tspan><tspan> </tspan><tspan class="fg-bright-green bold">note</tspan><tspan>: `cargo::unused_optional_dependency` is set to `warn`</tspan>
</tspan>
<tspan x="10px" y="154px"><tspan> </tspan><tspan class="fg-bright-blue bold">=</tspan><tspan> </tspan><tspan class="fg-bright-cyan bold">help</tspan><tspan>: remove the dependency or activate it in a feature with `dep:bar`</tspan>
</tspan>
<tspan x="10px" y="172px"><tspan class="fg-yellow bold">warning</tspan><tspan>: </tspan><tspan class="bold">unused optional dependency</tspan>
</tspan>
<tspan x="10px" y="190px"><tspan> </tspan><tspan class="fg-bright-blue bold">--&gt;</tspan><tspan> Cargo.toml:12:1</tspan>
</tspan>
<tspan x="10px" y="208px"><tspan class="fg-bright-blue bold"> |</tspan>
</tspan>
<tspan x="10px" y="226px"><tspan class="fg-bright-blue bold">12 |</tspan><tspan> baz = { version = "0.2.0", package = "bar", optional = true }</tspan>
</tspan>
<tspan x="10px" y="244px"><tspan class="fg-bright-blue bold"> |</tspan><tspan class="fg-yellow bold"> ---</tspan>
</tspan>
<tspan x="10px" y="262px"><tspan class="fg-bright-blue bold"> |</tspan>
</tspan>
<tspan x="10px" y="280px"><tspan> </tspan><tspan class="fg-bright-blue bold">=</tspan><tspan> </tspan><tspan class="fg-bright-cyan bold">help</tspan><tspan>: remove the dependency or activate it in a feature with `dep:baz`</tspan>
</tspan>
<tspan x="10px" y="298px"><tspan class="fg-yellow bold">warning</tspan><tspan>: </tspan><tspan class="bold">unused optional dependency</tspan>
</tspan>
<tspan x="10px" y="316px"><tspan> </tspan><tspan class="fg-bright-blue bold">--&gt;</tspan><tspan> Cargo.toml:15:1</tspan>
</tspan>
<tspan x="10px" y="334px"><tspan class="fg-bright-blue bold"> |</tspan>
</tspan>
<tspan x="10px" y="352px"><tspan class="fg-bright-blue bold">15 |</tspan><tspan> target-dep = { version = "0.1.0", optional = true }</tspan>
</tspan>
<tspan x="10px" y="370px"><tspan class="fg-bright-blue bold"> |</tspan><tspan class="fg-yellow bold"> ----------</tspan>
</tspan>
<tspan x="10px" y="388px"><tspan class="fg-bright-blue bold"> |</tspan>
</tspan>
<tspan x="10px" y="406px"><tspan> </tspan><tspan class="fg-bright-blue bold">=</tspan><tspan> </tspan><tspan class="fg-bright-cyan bold">help</tspan><tspan>: remove the dependency or activate it in a feature with `dep:target-dep`</tspan>
</tspan>
<tspan x="10px" y="424px"><tspan class="fg-green bold"> Checking</tspan><tspan> foo v0.1.0 ([ROOT]/foo)</tspan>
</tspan>
<tspan x="10px" y="442px"><tspan class="fg-green bold"> Finished</tspan><tspan> [..]</tspan>
</tspan>
<tspan x="10px" y="460px">
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -3392,3 +3392,55 @@ error: `foo` cannot be published.
)
.run();
}
#[cargo_test(nightly, reason = "edition2024 is not stable")]
fn unused_deps_edition_2024() {
let registry = RegistryBuilder::new().http_api().http_index().build();
let p = project()
.file(
"Cargo.toml",
r#"
cargo-features = ["edition2024"]
[package]
name = "foo"
version = "0.0.1"
authors = []
license = "MIT"
description = "foo"
edition = "2024"
[dependencies]
bar = { version = "0.1.0", optional = true }
[build-dependencies]
baz = { version = "0.1.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
target-dep = { version = "0.1.0", optional = true }
"#,
)
.file("src/main.rs", "")
.build();
p.cargo("publish --no-verify")
.masquerade_as_nightly_cargo(&["edition2024"])
.replace_crates_io(registry.index_url())
.with_stderr(
"\
[UPDATING] crates.io index
[WARNING] manifest has no documentation, [..]
See [..]
[PACKAGING] foo v0.0.1 ([CWD])
[PACKAGED] [..] files, [..] ([..] compressed)
[UPLOADING] foo v0.0.1 ([CWD])
[UPLOADED] foo v0.0.1 to registry `crates-io`
[NOTE] waiting for `foo v0.0.1` to be available at registry `crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
[PUBLISHED] foo v0.0.1 at registry `crates-io`
",
)
.run();
validate_upload_foo();
}