Skip to content

Commit

Permalink
Suggest similar feature names on CLI
Browse files Browse the repository at this point in the history
Extend tests
  • Loading branch information
DJMcNab committed Feb 4, 2025
1 parent 6d1f6be commit 7b9140f
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 5 deletions.
47 changes: 42 additions & 5 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 @@ -513,25 +515,60 @@ impl RequirementError {
.filter(|dep| dep.name_in_toml() == feat)
.collect();
if deps.is_empty() {
let closest =
closest_msg(&feat.as_str(), summary.features().keys(), |key| &key);
return match parent {
None => ActivateError::Fatal(anyhow::format_err!(
"Package `{}` does not have the feature `{}`",
"Package `{}` does not have the feature `{}`{}",
summary.package_id(),
feat
feat,
closest
)),
Some(p) => {
ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
}
};
}
if deps.iter().any(|dep| dep.is_optional()) {
let mut features = vec![];
for (name, values) in summary.features() {
for value in values {
match value {
FeatureValue::Dep { dep_name }
| FeatureValue::DepFeature {
dep_name,
weak: false,
..
} if dep_name == &feat => {
features.push(*name);
// Don't add this feature multiple times
break;
}
_ => (),
}
}
}
let mut suggestion = String::new();
if !features.is_empty() {
// Features is already sorted because it was constructed from a BTreeMap.
suggestion =
String::from("\nThis feature would be enabled by these features:");
let mut features = features.iter();
for feature in (&mut features).take(3) {
let _ = write!(&mut suggestion, "\n\t- `{}`", feature);
}
if features.next().is_some() {
suggestion.push_str("\n\t ...");
}
}
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.",
syntax in the features table, so it does not have an implicit feature with that name.{}",
summary.package_id(),
feat
feat,
suggestion
)),
Some(p) => ActivateError::Conflict(
p,
Expand All @@ -544,7 +581,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
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.
This feature 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`
Did you mean `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`
Did you mean `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
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.
This feature 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, 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.
This feature 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, 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.
This feature 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, 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.
This feature 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`
Did you mean `m1-feature`?
"#]])
.run();
}
Expand Down

0 comments on commit 7b9140f

Please sign in to comment.