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
This commit is contained in:
Ed Page 2025-04-25 19:45:13 +00:00 committed by GitHub
commit fb3906d3ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 172 additions and 62 deletions

View File

@ -524,15 +524,13 @@ impl RequirementError {
"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 @@ impl RequirementError {
}
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 @@ syntax in the features table, so it does not have an implicit feature with that
} 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 @@ syntax in the features table, so it does not have an implicit feature with that
)),
// 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!(

View File

@ -5,7 +5,7 @@ use std::task::Poll;
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 @@ pub(super) fn activation_error(
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 @@ pub(super) fn activation_error(
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",

View File

@ -339,10 +339,10 @@ pub enum ConflictReason {
/// 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 @@ impl ConflictReason {
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 {

View File

@ -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 @@ fn build_feature_map(
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 {

View File

@ -1032,7 +1032,7 @@ fn links_duplicates() {
... 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 @@ fn links_duplicates_deep_dependency() {
... 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 @@ fn links_duplicates_with_cycle() {
... 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.

View File

@ -6,7 +6,7 @@ use cargo_test_support::str;
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 @@ fn invalid1() {
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 @@ foo v0.0.1 ([ROOT]/foo) [bar,baz]
}
#[cargo_test]
fn invalid3() {
fn feature_activates_required_dependency() {
let p = project()
.file(
"Cargo.toml",
@ -155,7 +191,7 @@ Caused by:
}
#[cargo_test]
fn invalid4() {
fn dependency_activates_missing_feature() {
let p = project()
.file(
"Cargo.toml",
@ -183,7 +219,7 @@ fn invalid4() {
... 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 @@ failed to select a version for `bar` which could resolve this conflict
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 @@ Caused by:
}
#[cargo_test]
fn invalid6() {
fn feature_activates_missing_dep_feature() {
let p = project()
.file(
"Cargo.toml",
@ -269,7 +356,7 @@ fn invalid6() {
}
#[cargo_test]
fn invalid7() {
fn feature_activates_feature_inside_feature() {
let p = project()
.file(
"Cargo.toml",
@ -304,7 +391,7 @@ fn invalid7() {
}
#[cargo_test]
fn invalid8() {
fn dependency_activates_dep_feature() {
let p = project()
.file(
"Cargo.toml",
@ -339,7 +426,7 @@ Caused by:
}
#[cargo_test]
fn invalid9() {
fn cli_activates_required_dependency() {
let p = project()
.file(
"Cargo.toml",
@ -362,7 +449,9 @@ fn invalid9() {
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 @@ fn invalid9() {
}
#[cargo_test]
fn invalid10() {
fn dependency_activates_required_dependency() {
let p = project()
.file(
"Cargo.toml",
@ -411,8 +500,8 @@ fn invalid10() {
... 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

View File

@ -75,6 +75,8 @@ fn namespaced_invalid_feature() {
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 @@ 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.
[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`

View File

@ -338,7 +338,7 @@ f3f4
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 @@ fn feature_default_resolver() {
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 @@ fn command_line_optional_dep() {
.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 @@ fn command_line_optional_dep_three_options() {
.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 @@ fn command_line_optional_dep_many_options() {
.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 @@ fn command_line_optional_dep_many_paths() {
.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 @@ m1-feature set
.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`

View File

@ -4007,7 +4007,7 @@ fn cyclical_dep_with_missing_feature() {
... 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