Skip to content

Commit

Permalink
Suggest relevant feature names on the CLI
Browse files Browse the repository at this point in the history
 The left-aligned error message is there to
workaround rustfmt refusing to format files
which contain string literals which are too
wide. I have not found a consistent way to
fix this behaviour, but left-aligning does
resolve it in this case. I believe that this
should have an explanatory comment, but code
review determined that to be "noise" and so
I removed it.
  • Loading branch information
DJMcNab committed Feb 4, 2025
1 parent b0dc18d commit c5444d0
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 17 deletions.
81 changes: 68 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,49 @@ 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);
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 +570,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 +600,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
29 changes: 25 additions & 4 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`
Did you mean `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`
Did you mean `test`?
"#]])
.run();

Expand All @@ -428,7 +432,7 @@ feature set

#[cargo_test]
fn command_line_optional_dep() {
// Enabling a dependency used as a `dep:` errors
// Enabling a dependency used as a `dep:` errors helpfully
Package::new("bar", "1.0.0").publish();
let p = project()
.file(
Expand All @@ -455,14 +459,16 @@ fn command_line_optional_dep() {
[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, when there are three features which would enable the dependency
// 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(
Expand Down Expand Up @@ -491,14 +497,18 @@ fn command_line_optional_dep_three_options() {
[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, when there are many features which would enable the dependency
// 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(
Expand Down Expand Up @@ -528,14 +538,19 @@ fn command_line_optional_dep_many_options() {
[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, when a features would enable the dependency in multiple ways
// 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", &[])
Expand Down Expand Up @@ -569,6 +584,10 @@ fn command_line_optional_dep_many_paths() {
[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();
Expand Down Expand Up @@ -803,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`
Did you mean `m1-feature`?
"#]])
.run();
}
Expand Down

0 comments on commit c5444d0

Please sign in to comment.