fix: Suggest similar looking feature names when feature is missing (#15454)
### What does this PR try to resolve?
I recently depended on a package with `preserve-order` rather than
`preserve_order` and the error message didn't help me with the problem
so I figure I'd fix that. I also found other improvements along the way
- Suggest an alternative feature when a feature includes a missing
feature
- Suggest an alternative feature when a dependency includes a missing
feature
- Lower case error messages
- Re-frame prescriptive information as help
- Change plural "features" error messages to singular "feature" as they
can only ever have one (knowing an the `MissingFeature` string only has
one feature in it was important for doing a `closest` match on the
feature).
### How should we test and review this PR?
### Additional information
diff --git a/src/cargo/core/resolver/dep_cache.rs b/src/cargo/core/resolver/dep_cache.rs
index f0b56db..2a93ced 100644
--- a/src/cargo/core/resolver/dep_cache.rs
+++ b/src/cargo/core/resolver/dep_cache.rs
@@ -524,15 +524,13 @@
"feature",
);
ActivateError::Fatal(anyhow::format_err!(
- "Package `{}` does not have the feature `{}`{}",
+ "package `{}` does not have the feature `{}`{}",
summary.package_id(),
feat,
closest
))
}
- Some(p) => {
- ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
- }
+ Some(p) => ActivateError::Conflict(p, ConflictReason::MissingFeature(feat)),
};
}
if deps.iter().any(|dep| dep.is_optional()) {
@@ -555,9 +553,11 @@
}
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.{}",
+package `{}` does not have feature `{}`
+
+help: an optional dependency \
+with that name exists, but the `features` table includes it with the \"dep:\" \
+syntax so it does not have an implicit feature with that name{}",
summary.package_id(),
feat,
suggestion
@@ -571,8 +571,9 @@
} else {
match parent {
None => ActivateError::Fatal(anyhow::format_err!(
- "Package `{}` does not have feature `{}`. It has a required dependency \
- with that name, but only optional dependencies can be used as features.",
+ "package `{}` does not have feature `{}`
+
+help: a depednency with that name exists but it is required dependency and only optional dependencies can be used as features.",
summary.package_id(),
feat,
)),
@@ -592,9 +593,7 @@
)),
// This code path currently isn't used, since `foo/bar`
// and `dep:` syntax is not allowed in a dependency.
- Some(p) => {
- ActivateError::Conflict(p, ConflictReason::MissingFeatures(dep_name))
- }
+ Some(p) => ActivateError::Conflict(p, ConflictReason::MissingFeature(dep_name)),
}
}
RequirementError::Cycle(feat) => ActivateError::Fatal(anyhow::format_err!(
diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs
index 6c8679e..169fcd1 100644
--- a/src/cargo/core/resolver/errors.rs
+++ b/src/cargo/core/resolver/errors.rs
@@ -5,7 +5,7 @@
use crate::core::{Dependency, PackageId, Registry, Summary};
use crate::sources::source::QueryKind;
use crate::sources::IndexSummary;
-use crate::util::edit_distance::edit_distance;
+use crate::util::edit_distance::{closest, edit_distance};
use crate::util::errors::CargoResult;
use crate::util::{GlobalContext, OptVersionReq, VersionExt};
use anyhow::Error;
@@ -137,7 +137,7 @@
has_semver = true;
}
ConflictReason::Links(link) => {
- msg.push_str("\n\nthe package `");
+ msg.push_str("\n\npackage `");
msg.push_str(&*dep.package_name());
msg.push_str("` links to the native library `");
msg.push_str(link);
@@ -150,46 +150,54 @@
msg.push_str(link);
msg.push_str("\"` value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.");
}
- ConflictReason::MissingFeatures(features) => {
- msg.push_str("\n\nthe package `");
+ ConflictReason::MissingFeature(feature) => {
+ msg.push_str("\n\npackage `");
msg.push_str(&*p.name());
msg.push_str("` depends on `");
msg.push_str(&*dep.package_name());
- msg.push_str("`, with features: `");
- msg.push_str(features);
+ msg.push_str("` with feature `");
+ msg.push_str(feature);
msg.push_str("` but `");
msg.push_str(&*dep.package_name());
- msg.push_str("` does not have these features.\n");
+ msg.push_str("` does not have that feature.\n");
+ let latest = candidates.last().expect("in the non-empty branch");
+ if let Some(closest) = closest(feature, latest.features().keys(), |k| k) {
+ msg.push_str(" package `");
+ msg.push_str(&*dep.package_name());
+ msg.push_str("` does have feature `");
+ msg.push_str(closest);
+ msg.push_str("`\n");
+ }
// p == parent so the full path is redundant.
}
- ConflictReason::RequiredDependencyAsFeature(features) => {
- msg.push_str("\n\nthe package `");
+ ConflictReason::RequiredDependencyAsFeature(feature) => {
+ msg.push_str("\n\npackage `");
msg.push_str(&*p.name());
msg.push_str("` depends on `");
msg.push_str(&*dep.package_name());
- msg.push_str("`, with features: `");
- msg.push_str(features);
+ msg.push_str("` with feature `");
+ msg.push_str(feature);
msg.push_str("` but `");
msg.push_str(&*dep.package_name());
- msg.push_str("` does not have these features.\n");
+ msg.push_str("` does not have that feature.\n");
msg.push_str(
- " It has a required dependency with that name, \
+ " A required dependency with that name exists, \
but only optional dependencies can be used as features.\n",
);
// p == parent so the full path is redundant.
}
- ConflictReason::NonImplicitDependencyAsFeature(features) => {
- msg.push_str("\n\nthe package `");
+ ConflictReason::NonImplicitDependencyAsFeature(feature) => {
+ msg.push_str("\n\npackage `");
msg.push_str(&*p.name());
msg.push_str("` depends on `");
msg.push_str(&*dep.package_name());
- msg.push_str("`, with features: `");
- msg.push_str(features);
+ msg.push_str("` with feature `");
+ msg.push_str(feature);
msg.push_str("` but `");
msg.push_str(&*dep.package_name());
- msg.push_str("` does not have these features.\n");
+ msg.push_str("` does not have that feature.\n");
msg.push_str(
- " It has an optional dependency with that name, \
+ " An optional dependency with that name exists, \
but that dependency uses the \"dep:\" \
syntax in the features table, so it does not have an \
implicit feature with that name.\n",
diff --git a/src/cargo/core/resolver/types.rs b/src/cargo/core/resolver/types.rs
index 2a5c51a..8bc1c9f 100644
--- a/src/cargo/core/resolver/types.rs
+++ b/src/cargo/core/resolver/types.rs
@@ -339,10 +339,10 @@
/// we're only allowed one per dependency graph.
Links(InternedString),
- /// A dependency listed features that weren't actually available on the
+ /// A dependency listed a feature that wasn't actually available on the
/// candidate. For example we tried to activate feature `foo` but the
/// candidate we're activating didn't actually have the feature `foo`.
- MissingFeatures(InternedString),
+ MissingFeature(InternedString),
/// A dependency listed a feature that ended up being a required dependency.
/// For example we tried to activate feature `foo` but the
@@ -360,8 +360,8 @@
matches!(self, ConflictReason::Links(_))
}
- pub fn is_missing_features(&self) -> bool {
- matches!(self, ConflictReason::MissingFeatures(_))
+ pub fn is_missing_feature(&self) -> bool {
+ matches!(self, ConflictReason::MissingFeature(_))
}
pub fn is_required_dependency_as_features(&self) -> bool {
diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs
index 168a20f..d12a6f4 100644
--- a/src/cargo/core/summary.rs
+++ b/src/cargo/core/summary.rs
@@ -1,4 +1,5 @@
use crate::core::{Dependency, PackageId, SourceId};
+use crate::util::closest_msg;
use crate::util::interning::InternedString;
use crate::util::CargoResult;
use anyhow::bail;
@@ -241,9 +242,10 @@
Feature(f) => {
if !features.contains_key(f) {
if !is_any_dep {
+ let closest = closest_msg(f, features.keys(), |k| k, "feature");
bail!(
"feature `{feature}` includes `{fv}` which is neither a dependency \
- nor another feature"
+ nor another feature{closest}"
);
}
if is_optional_dep {
diff --git a/tests/testsuite/build_script.rs b/tests/testsuite/build_script.rs
index 17d8228..f3ed272 100644
--- a/tests/testsuite/build_script.rs
+++ b/tests/testsuite/build_script.rs
@@ -1032,7 +1032,7 @@
... required by package `foo v0.5.0 ([ROOT]/foo)`
versions that meet the requirements `*` are: 0.5.0
-the package `a-sys` links to the native library `a`, but it conflicts with a previous package which links to `a` as well:
+package `a-sys` links to the native library `a`, but it conflicts with a previous package which links to `a` as well:
package `foo v0.5.0 ([ROOT]/foo)`
Only one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. Try to adjust your dependencies so that only one package uses the `links = "a"` value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.
@@ -1159,7 +1159,7 @@
... which satisfies path dependency `a` of package `foo v0.5.0 ([ROOT]/foo)`
versions that meet the requirements `*` are: 0.5.0
-the package `a-sys` links to the native library `a`, but it conflicts with a previous package which links to `a` as well:
+package `a-sys` links to the native library `a`, but it conflicts with a previous package which links to `a` as well:
package `foo v0.5.0 ([ROOT]/foo)`
Only one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. Try to adjust your dependencies so that only one package uses the `links = "a"` value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.
@@ -4767,7 +4767,7 @@
... required by package `foo v0.5.0 ([ROOT]/foo)`
versions that meet the requirements `*` are: 0.5.0
-the package `a` links to the native library `a`, but it conflicts with a previous package which links to `a` as well:
+package `a` links to the native library `a`, but it conflicts with a previous package which links to `a` as well:
package `foo v0.5.0 ([ROOT]/foo)`
Only one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. Try to adjust your dependencies so that only one package uses the `links = "a"` value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.
diff --git a/tests/testsuite/features.rs b/tests/testsuite/features.rs
index 94a69cc..da39e16 100644
--- a/tests/testsuite/features.rs
+++ b/tests/testsuite/features.rs
@@ -6,7 +6,7 @@
use cargo_test_support::{basic_manifest, project};
#[cargo_test]
-fn invalid1() {
+fn feature_activates_missing_feature() {
let p = project()
.file(
"Cargo.toml",
@@ -32,6 +32,42 @@
Caused by:
feature `bar` includes `baz` which is neither a dependency nor another feature
+ [HELP] a feature with a similar name exists: `bar`
+
+"#]])
+ .run();
+}
+
+#[cargo_test]
+fn feature_activates_typoed_feature() {
+ let p = project()
+ .file(
+ "Cargo.toml",
+ r#"
+ [package]
+ name = "foo"
+ version = "0.0.1"
+ edition = "2015"
+ authors = []
+
+ [features]
+ bar = ["baz"]
+ jaz = []
+ "#,
+ )
+ .file("src/main.rs", "")
+ .build();
+
+ p.cargo("check")
+ .with_status(101)
+ .with_stderr_data(str![[r#"
+[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
+
+Caused by:
+ feature `bar` includes `baz` which is neither a dependency nor another feature
+
+ [HELP] a feature with a similar name exists: `bar`
+
"#]])
.run();
}
@@ -120,7 +156,7 @@
}
#[cargo_test]
-fn invalid3() {
+fn feature_activates_required_dependency() {
let p = project()
.file(
"Cargo.toml",
@@ -155,7 +191,7 @@
}
#[cargo_test]
-fn invalid4() {
+fn dependency_activates_missing_feature() {
let p = project()
.file(
"Cargo.toml",
@@ -183,7 +219,7 @@
... required by package `foo v0.0.1 ([ROOT]/foo)`
versions that meet the requirements `*` are: 0.0.1
-the package `foo` depends on `bar`, with features: `bar` but `bar` does not have these features.
+package `foo` depends on `bar` with feature `bar` but `bar` does not have that feature.
failed to select a version for `bar` which could resolve this conflict
@@ -196,14 +232,65 @@
p.cargo("check --features test")
.with_status(101)
.with_stderr_data(str![[r#"
-[ERROR] Package `foo v0.0.1 ([ROOT]/foo)` does not have the feature `test`
+[ERROR] package `foo v0.0.1 ([ROOT]/foo)` does not have the feature `test`
"#]])
.run();
}
#[cargo_test]
-fn invalid5() {
+fn dependency_activates_typoed_feature() {
+ let p = project()
+ .file(
+ "Cargo.toml",
+ r#"
+ [package]
+ name = "foo"
+ version = "0.0.1"
+ edition = "2015"
+ authors = []
+
+ [dependencies.bar]
+ path = "bar"
+ features = ["bar"]
+ "#,
+ )
+ .file("src/main.rs", "")
+ .file(
+ "bar/Cargo.toml",
+ r#"
+ [package]
+ name = "bar"
+ version = "0.0.1"
+ edition = "2015"
+ authors = []
+
+ [features]
+ baz = []
+"#,
+ )
+ .file("bar/src/lib.rs", "")
+ .build();
+
+ p.cargo("check")
+ .with_status(101)
+ .with_stderr_data(str![[r#"
+[ERROR] failed to select a version for `bar`.
+ ... required by package `foo v0.0.1 ([ROOT]/foo)`
+versions that meet the requirements `*` are: 0.0.1
+
+package `foo` depends on `bar` with feature `bar` but `bar` does not have that feature.
+ package `bar` does have feature `baz`
+
+
+failed to select a version for `bar` which could resolve this conflict
+
+"#]])
+ .run();
+}
+
+#[cargo_test]
+fn optional_dev_dependency() {
let p = project()
.file(
"Cargo.toml",
@@ -235,7 +322,7 @@
}
#[cargo_test]
-fn invalid6() {
+fn feature_activates_missing_dep_feature() {
let p = project()
.file(
"Cargo.toml",
@@ -269,7 +356,7 @@
}
#[cargo_test]
-fn invalid7() {
+fn feature_activates_feature_inside_feature() {
let p = project()
.file(
"Cargo.toml",
@@ -304,7 +391,7 @@
}
#[cargo_test]
-fn invalid8() {
+fn dependency_activates_dep_feature() {
let p = project()
.file(
"Cargo.toml",
@@ -339,7 +426,7 @@
}
#[cargo_test]
-fn invalid9() {
+fn cli_activates_required_dependency() {
let p = project()
.file(
"Cargo.toml",
@@ -362,7 +449,9 @@
p.cargo("check --features bar")
.with_stderr_data(str![[r#"
[LOCKING] 1 package to latest compatible version
-[ERROR] Package `foo v0.0.1 ([ROOT]/foo)` does not have feature `bar`. It has a required dependency with that name, but only optional dependencies can be used as features.
+[ERROR] package `foo v0.0.1 ([ROOT]/foo)` does not have feature `bar`
+
+[HELP] a depednency with that name exists but it is required dependency and only optional dependencies can be used as features.
"#]])
.with_status(101)
@@ -370,7 +459,7 @@
}
#[cargo_test]
-fn invalid10() {
+fn dependency_activates_required_dependency() {
let p = project()
.file(
"Cargo.toml",
@@ -411,8 +500,8 @@
... required by package `foo v0.0.1 ([ROOT]/foo)`
versions that meet the requirements `*` are: 0.0.1
-the package `foo` depends on `bar`, with features: `baz` but `bar` does not have these features.
- It has a required dependency with that name, but only optional dependencies can be used as features.
+package `foo` depends on `bar` with feature `baz` but `bar` does not have that feature.
+ A required dependency with that name exists, but only optional dependencies can be used as features.
failed to select a version for `bar` which could resolve this conflict
diff --git a/tests/testsuite/features_namespaced.rs b/tests/testsuite/features_namespaced.rs
index f7315fe..f104825 100644
--- a/tests/testsuite/features_namespaced.rs
+++ b/tests/testsuite/features_namespaced.rs
@@ -75,6 +75,8 @@
Caused by:
feature `bar` includes `baz` which is neither a dependency nor another feature
+ [HELP] a feature with a similar name exists: `bar`
+
"#]])
.run();
}
@@ -417,7 +419,9 @@
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.
+[ERROR] package `foo v0.1.0 ([ROOT]/foo)` does not have feature `lazy_static`
+
+[HELP] an optional dependency with that name exists, but the `features` table includes it with the "dep:" syntax so it does not have an implicit feature with that name
Dependency `lazy_static` would be enabled by these features:
- `regex`
diff --git a/tests/testsuite/package_features.rs b/tests/testsuite/package_features.rs
index 2aeb390..23a7402 100644
--- a/tests/testsuite/package_features.rs
+++ b/tests/testsuite/package_features.rs
@@ -338,7 +338,7 @@
p.cargo("run -p bar --features f1,f2")
.with_status(101)
.with_stderr_data(str![[r#"
-[ERROR] Package `foo v0.1.0 ([ROOT]/foo)` does not have the feature `f2`
+[ERROR] package `foo v0.1.0 ([ROOT]/foo)` does not have the feature `f2`
[HELP] a feature with a similar name exists: `f1`
@@ -406,7 +406,7 @@
p.cargo("check --features testt")
.with_status(101)
.with_stderr_data(str![[r#"
-[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have the feature `testt`
+[ERROR] package `a v0.1.0 ([ROOT]/foo)` does not have the feature `testt`
[HELP] a feature with a similar name exists: `test`
@@ -458,7 +458,9 @@
.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.
+[ERROR] package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`
+
+[HELP] an optional dependency with that name exists, but the `features` table includes it with the "dep:" syntax so it does not have an implicit feature with that name
Dependency `bar` would be enabled by these features:
- `foo`
@@ -496,7 +498,9 @@
.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.
+[ERROR] package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`
+
+[HELP] an optional dependency with that name exists, but the `features` table includes it with the "dep:" syntax so it does not have an implicit feature with that name
Dependency `bar` would be enabled by these features:
- `f1`
- `f2`
@@ -537,7 +541,9 @@
.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.
+[ERROR] package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`
+
+[HELP] an optional dependency with that name exists, but the `features` table includes it with the "dep:" syntax so it does not have an implicit feature with that name
Dependency `bar` would be enabled by these features:
- `f1`
- `f2`
@@ -583,7 +589,9 @@
.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.
+[ERROR] package `a v0.1.0 ([ROOT]/foo)` does not have feature `bar`
+
+[HELP] an optional dependency with that name exists, but the `features` table includes it with the "dep:" syntax so it does not have an implicit feature with that name
Dependency `bar` would be enabled by these features:
- `f1`
- `f2`
@@ -820,7 +828,7 @@
.cwd("member2")
.with_status(101)
.with_stderr_data(str![[r#"
-[ERROR] Package `member1 v0.1.0 ([ROOT]/foo/member1)` does not have the feature `m2-feature`
+[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`
diff --git a/tests/testsuite/test.rs b/tests/testsuite/test.rs
index 13eeac1..711fde2 100644
--- a/tests/testsuite/test.rs
+++ b/tests/testsuite/test.rs
@@ -4007,7 +4007,7 @@
... required by package `foo v0.1.0 ([ROOT]/foo)`
versions that meet the requirements `*` are: 0.1.0
-the package `foo` depends on `foo`, with features: `missing` but `foo` does not have these features.
+package `foo` depends on `foo` with feature `missing` but `foo` does not have that feature.
failed to select a version for `foo` which could resolve this conflict