Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggest similar feature names on CLI #15133

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 72 additions & 13 deletions src/cargo/core/resolver/dep_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ use crate::core::{
Dependency, FeatureValue, PackageId, PackageIdSpec, PackageIdSpecQuery, Registry, Summary,
};
use crate::sources::source::QueryKind;
use crate::util::closest_msg;
use crate::util::errors::CargoResult;
use crate::util::interning::{InternedString, INTERNED_DEFAULT};

use anyhow::Context as _;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::Write;
use std::rc::Rc;
use std::task::Poll;
use tracing::debug;
Expand Down Expand Up @@ -514,25 +516,53 @@ impl RequirementError {
.collect();
if deps.is_empty() {
return match parent {
None => ActivateError::Fatal(anyhow::format_err!(
"Package `{}` does not have the feature `{}`",
summary.package_id(),
feat
)),
None => {
let closest = closest_msg(
&feat.as_str(),
summary.features().keys(),
|key| &key,
"feature",
);
ActivateError::Fatal(anyhow::format_err!(
"Package `{}` does not have the feature `{}`{}",
summary.package_id(),
feat,
closest
))
}
Some(p) => {
ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
}
};
}
if deps.iter().any(|dep| dep.is_optional()) {
match parent {
None => ActivateError::Fatal(anyhow::format_err!(
"Package `{}` does not have feature `{}`. It has an optional dependency \
with that name, but that dependency uses the \"dep:\" \
syntax in the features table, so it does not have an implicit feature with that name.",
summary.package_id(),
feat
)),
None => {
let mut features =
features_enabling_dependency_sorted(summary, feat).peekable();
let mut suggestion = String::new();
if features.peek().is_some() {
suggestion = format!(
"\nDependency `{}` would be enabled by these features:",
feat
);
for feature in (&mut features).take(3) {
let _ = write!(&mut suggestion, "\n\t- `{}`", feature);
}
if features.peek().is_some() {
suggestion.push_str("\n\t ...");
}
}
ActivateError::Fatal(anyhow::format_err!(
"\
Package `{}` does not have feature `{}`. It has an optional dependency \
with that name, but that dependency uses the \"dep:\" \
syntax in the features table, so it does not have an implicit feature with that name.{}",
summary.package_id(),
feat,
suggestion
))
}
Some(p) => ActivateError::Conflict(
p,
ConflictReason::NonImplicitDependencyAsFeature(feat),
Expand All @@ -544,7 +574,7 @@ impl RequirementError {
"Package `{}` does not have feature `{}`. It has a required dependency \
with that name, but only optional dependencies can be used as features.",
summary.package_id(),
feat
feat,
)),
Some(p) => ActivateError::Conflict(
p,
Expand Down Expand Up @@ -574,3 +604,32 @@ impl RequirementError {
}
}
}

/// Collect any features which enable the optional dependency "target_dep".
///
/// The returned value will be sorted.
fn features_enabling_dependency_sorted(
summary: &Summary,
target_dep: InternedString,
) -> impl Iterator<Item = InternedString> + '_ {
let iter = summary
.features()
.iter()
.filter(move |(_, values)| {
for value in *values {
match value {
FeatureValue::Dep { dep_name }
| FeatureValue::DepFeature {
dep_name,
weak: false,
..
} if dep_name == &target_dep => return true,
_ => (),
}
}
false
})
.map(|(name, _)| *name);
// iter is already sorted because it was constructed from a BTreeMap.
iter
}
2 changes: 2 additions & 0 deletions tests/testsuite/features_namespaced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ regex
p.cargo("run --features lazy_static")
.with_stderr_data(str![[r#"
[ERROR] Package `foo v0.1.0 ([ROOT]/foo)` does not have feature `lazy_static`. It has an optional dependency with that name, but that dependency uses the "dep:" syntax in the features table, so it does not have an implicit feature with that name.
Dependency `lazy_static` would be enabled by these features:
- `regex`

"#]])
.with_status(101)
Expand Down
169 changes: 169 additions & 0 deletions tests/testsuite/package_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ f3f4
.with_stderr_data(str![[r#"
[ERROR] Package `foo v0.1.0 ([ROOT]/foo)` does not have the feature `f2`

[HELP] a feature with a similar name exists: `f1`

"#]])
.run();

Expand Down Expand Up @@ -406,6 +408,8 @@ fn feature_default_resolver() {
.with_stderr_data(str![[r#"
[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have the feature `testt`

[HELP] a feature with a similar name exists: `test`

"#]])
.run();

Expand All @@ -426,6 +430,169 @@ feature set
.run();
}

#[cargo_test]
fn command_line_optional_dep() {
// Enabling a dependency used as a `dep:` errors helpfully
Package::new("bar", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
edition = "2015"

[features]
foo = ["dep:bar"]

[dependencies]
bar = { version = "1.0.0", optional = true }
"#,
)
.file("src/lib.rs", r#""#)
.build();

p.cargo("check --features bar")
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version
[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`. It has an optional dependency with that name, but that dependency uses the "dep:" syntax in the features table, so it does not have an implicit feature with that name.
Dependency `bar` would be enabled by these features:
- `foo`

"#]])
.run();
}

#[cargo_test]
fn command_line_optional_dep_three_options() {
// Trying to enable an optional dependency used as a `dep:` errors helpfully, when there are three features which would enable the dependency
Package::new("bar", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
edition = "2015"

[features]
f1 = ["dep:bar"]
f2 = ["dep:bar"]
f3 = ["dep:bar"]

[dependencies]
bar = { version = "1.0.0", optional = true }
"#,
)
.file("src/lib.rs", r#""#)
.build();

p.cargo("check --features bar")
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version
[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`. It has an optional dependency with that name, but that dependency uses the "dep:" syntax in the features table, so it does not have an implicit feature with that name.
Dependency `bar` would be enabled by these features:
- `f1`
- `f2`
- `f3`

"#]])
.run();
}

#[cargo_test]
fn command_line_optional_dep_many_options() {
// Trying to enable an optional dependency used as a `dep:` errors helpfully, when there are many features which would enable the dependency
Package::new("bar", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
edition = "2015"

[features]
f1 = ["dep:bar"]
f2 = ["dep:bar"]
f3 = ["dep:bar"]
f4 = ["dep:bar"]

[dependencies]
bar = { version = "1.0.0", optional = true }
"#,
)
.file("src/lib.rs", r#""#)
.build();

p.cargo("check --features bar")
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version
[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`. It has an optional dependency with that name, but that dependency uses the "dep:" syntax in the features table, so it does not have an implicit feature with that name.
Dependency `bar` would be enabled by these features:
- `f1`
- `f2`
- `f3`
...

"#]])
.run();
}

#[cargo_test]
fn command_line_optional_dep_many_paths() {
// Trying to enable an optional dependency used as a `dep:` errors helpfully, when a features would enable the dependency in multiple ways
Package::new("bar", "1.0.0")
.feature("a", &[])
.feature("b", &[])
.feature("c", &[])
.feature("d", &[])
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
edition = "2015"

[features]
f1 = ["dep:bar", "bar/a", "bar/b"] # Remove the implicit feature
f2 = ["bar/b", "bar/c"] # Overlaps with previous
f3 = ["bar/d"] # No overlap with previous

[dependencies]
bar = { version = "1.0.0", optional = true }
"#,
)
.file("src/lib.rs", r#""#)
.build();

p.cargo("check --features bar")
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version
[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`. It has an optional dependency with that name, but that dependency uses the "dep:" syntax in the features table, so it does not have an implicit feature with that name.
Dependency `bar` would be enabled by these features:
- `f1`
- `f2`
- `f3`

"#]])
.run();
}

#[cargo_test]
fn virtual_member_slash() {
// member slash feature syntax
Expand Down Expand Up @@ -655,6 +822,8 @@ m1-feature set
.with_stderr_data(str![[r#"
[ERROR] Package `member1 v0.1.0 ([ROOT]/foo/member1)` does not have the feature `m2-feature`

[HELP] a feature with a similar name exists: `m1-feature`

"#]])
.run();
}
Expand Down