mirror of
https://github.com/rust-lang/cargo.git
synced 2025-10-01 11:30:39 +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,
|
Dependency, FeatureValue, PackageId, PackageIdSpec, PackageIdSpecQuery, Registry, Summary,
|
||||||
};
|
};
|
||||||
use crate::sources::source::QueryKind;
|
use crate::sources::source::QueryKind;
|
||||||
|
use crate::util::closest_msg;
|
||||||
use crate::util::errors::CargoResult;
|
use crate::util::errors::CargoResult;
|
||||||
use crate::util::interning::{InternedString, INTERNED_DEFAULT};
|
use crate::util::interning::{InternedString, INTERNED_DEFAULT};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
|
use std::fmt::Write;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@ -514,11 +516,20 @@ impl RequirementError {
|
|||||||
.collect();
|
.collect();
|
||||||
if deps.is_empty() {
|
if deps.is_empty() {
|
||||||
return match parent {
|
return match parent {
|
||||||
None => ActivateError::Fatal(anyhow::format_err!(
|
None => {
|
||||||
"Package `{}` does not have the feature `{}`",
|
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(),
|
summary.package_id(),
|
||||||
feat
|
feat,
|
||||||
)),
|
closest
|
||||||
|
))
|
||||||
|
}
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
|
ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
|
||||||
}
|
}
|
||||||
@ -526,13 +537,32 @@ impl RequirementError {
|
|||||||
}
|
}
|
||||||
if deps.iter().any(|dep| dep.is_optional()) {
|
if deps.iter().any(|dep| dep.is_optional()) {
|
||||||
match parent {
|
match parent {
|
||||||
None => ActivateError::Fatal(anyhow::format_err!(
|
None => {
|
||||||
"Package `{}` does not have feature `{}`. It has an optional dependency \
|
let mut features =
|
||||||
with that name, but that dependency uses the \"dep:\" \
|
features_enabling_dependency_sorted(summary, feat).peekable();
|
||||||
syntax in the features table, so it does not have an implicit feature with that name.",
|
let mut suggestion = String::new();
|
||||||
summary.package_id(),
|
if features.peek().is_some() {
|
||||||
|
suggestion = format!(
|
||||||
|
"\nDependency `{}` would be enabled by these features:",
|
||||||
feat
|
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(
|
Some(p) => ActivateError::Conflict(
|
||||||
p,
|
p,
|
||||||
ConflictReason::NonImplicitDependencyAsFeature(feat),
|
ConflictReason::NonImplicitDependencyAsFeature(feat),
|
||||||
@ -544,7 +574,7 @@ impl RequirementError {
|
|||||||
"Package `{}` does not have feature `{}`. It has a required dependency \
|
"Package `{}` does not have feature `{}`. It has a required dependency \
|
||||||
with that name, but only optional dependencies can be used as features.",
|
with that name, but only optional dependencies can be used as features.",
|
||||||
summary.package_id(),
|
summary.package_id(),
|
||||||
feat
|
feat,
|
||||||
)),
|
)),
|
||||||
Some(p) => ActivateError::Conflict(
|
Some(p) => ActivateError::Conflict(
|
||||||
p,
|
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")
|
p.cargo("run --features lazy_static")
|
||||||
.with_stderr_data(str![[r#"
|
.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`. 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)
|
.with_status(101)
|
||||||
|
@ -340,6 +340,8 @@ f3f4
|
|||||||
.with_stderr_data(str![[r#"
|
.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`
|
||||||
|
|
||||||
"#]])
|
"#]])
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
@ -406,6 +408,8 @@ fn feature_default_resolver() {
|
|||||||
.with_stderr_data(str![[r#"
|
.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`
|
||||||
|
|
||||||
"#]])
|
"#]])
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
@ -426,6 +430,169 @@ feature set
|
|||||||
.run();
|
.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]
|
#[cargo_test]
|
||||||
fn virtual_member_slash() {
|
fn virtual_member_slash() {
|
||||||
// member slash feature syntax
|
// member slash feature syntax
|
||||||
@ -655,6 +822,8 @@ m1-feature set
|
|||||||
.with_stderr_data(str![[r#"
|
.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`
|
||||||
|
|
||||||
"#]])
|
"#]])
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user