mirror of
https://github.com/rust-lang/cargo.git
synced 2025-09-25 11:14:46 +00:00
Suggest similar feature names on CLI (#15133)
### What does this PR try to resolve? When you typo a feature name on the CLI, the error message isn't very helpful. Concretely, I was testing a PR which adds a feature called `cosmic_text` to enable a `cosmic-text` dependency, and got a correct but unhelpful error message: ```rust error: Package `scenes v0.0.0 ([ELIDED]/linebender/vello/examples/scenes)` does not have feature `cosmic-text`. 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. ``` I had to dig into the Cargo.lock file to find out how to fix this. ### How should we test and review this PR? Observe the new test cases
This commit is contained in:
commit
83c11eebef
@ -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;
|
||||
@ -514,11 +516,20 @@ 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))
|
||||
}
|
||||
@ -526,13 +537,32 @@ impl RequirementError {
|
||||
}
|
||||
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),
|
||||
@ -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,
|
||||
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user