Skip to content

Commit

Permalink
Suggest similar feature names on CLI (#15133)
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
epage authored Feb 4, 2025
2 parents f327379 + 378f021 commit 83c11ee
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 13 deletions.
85 changes: 72 additions & 13 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 @@ -514,25 +516,53 @@ 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))
}
};
}
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),
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
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.
Dependency `lazy_static` 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`
[HELP] a feature with a similar name exists: `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`
[HELP] a feature with a similar name exists: `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 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
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`
[HELP] a feature with a similar name exists: `m1-feature`
"#]])
.run();
}
Expand Down

0 comments on commit 83c11ee

Please sign in to comment.