Skip to content

Commit 83c11ee

Browse files
authored
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
2 parents f327379 + 378f021 commit 83c11ee

File tree

3 files changed

+243
-13
lines changed

3 files changed

+243
-13
lines changed

src/cargo/core/resolver/dep_cache.rs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ use crate::core::{
2020
Dependency, FeatureValue, PackageId, PackageIdSpec, PackageIdSpecQuery, Registry, Summary,
2121
};
2222
use crate::sources::source::QueryKind;
23+
use crate::util::closest_msg;
2324
use crate::util::errors::CargoResult;
2425
use crate::util::interning::{InternedString, INTERNED_DEFAULT};
2526

2627
use anyhow::Context as _;
2728
use std::collections::{BTreeSet, HashMap, HashSet};
29+
use std::fmt::Write;
2830
use std::rc::Rc;
2931
use std::task::Poll;
3032
use tracing::debug;
@@ -514,25 +516,53 @@ impl RequirementError {
514516
.collect();
515517
if deps.is_empty() {
516518
return match parent {
517-
None => ActivateError::Fatal(anyhow::format_err!(
518-
"Package `{}` does not have the feature `{}`",
519-
summary.package_id(),
520-
feat
521-
)),
519+
None => {
520+
let closest = closest_msg(
521+
&feat.as_str(),
522+
summary.features().keys(),
523+
|key| &key,
524+
"feature",
525+
);
526+
ActivateError::Fatal(anyhow::format_err!(
527+
"Package `{}` does not have the feature `{}`{}",
528+
summary.package_id(),
529+
feat,
530+
closest
531+
))
532+
}
522533
Some(p) => {
523534
ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
524535
}
525536
};
526537
}
527538
if deps.iter().any(|dep| dep.is_optional()) {
528539
match parent {
529-
None => ActivateError::Fatal(anyhow::format_err!(
530-
"Package `{}` does not have feature `{}`. It has an optional dependency \
531-
with that name, but that dependency uses the \"dep:\" \
532-
syntax in the features table, so it does not have an implicit feature with that name.",
533-
summary.package_id(),
534-
feat
535-
)),
540+
None => {
541+
let mut features =
542+
features_enabling_dependency_sorted(summary, feat).peekable();
543+
let mut suggestion = String::new();
544+
if features.peek().is_some() {
545+
suggestion = format!(
546+
"\nDependency `{}` would be enabled by these features:",
547+
feat
548+
);
549+
for feature in (&mut features).take(3) {
550+
let _ = write!(&mut suggestion, "\n\t- `{}`", feature);
551+
}
552+
if features.peek().is_some() {
553+
suggestion.push_str("\n\t ...");
554+
}
555+
}
556+
ActivateError::Fatal(anyhow::format_err!(
557+
"\
558+
Package `{}` does not have feature `{}`. It has an optional dependency \
559+
with that name, but that dependency uses the \"dep:\" \
560+
syntax in the features table, so it does not have an implicit feature with that name.{}",
561+
summary.package_id(),
562+
feat,
563+
suggestion
564+
))
565+
}
536566
Some(p) => ActivateError::Conflict(
537567
p,
538568
ConflictReason::NonImplicitDependencyAsFeature(feat),
@@ -544,7 +574,7 @@ impl RequirementError {
544574
"Package `{}` does not have feature `{}`. It has a required dependency \
545575
with that name, but only optional dependencies can be used as features.",
546576
summary.package_id(),
547-
feat
577+
feat,
548578
)),
549579
Some(p) => ActivateError::Conflict(
550580
p,
@@ -574,3 +604,32 @@ impl RequirementError {
574604
}
575605
}
576606
}
607+
608+
/// Collect any features which enable the optional dependency "target_dep".
609+
///
610+
/// The returned value will be sorted.
611+
fn features_enabling_dependency_sorted(
612+
summary: &Summary,
613+
target_dep: InternedString,
614+
) -> impl Iterator<Item = InternedString> + '_ {
615+
let iter = summary
616+
.features()
617+
.iter()
618+
.filter(move |(_, values)| {
619+
for value in *values {
620+
match value {
621+
FeatureValue::Dep { dep_name }
622+
| FeatureValue::DepFeature {
623+
dep_name,
624+
weak: false,
625+
..
626+
} if dep_name == &target_dep => return true,
627+
_ => (),
628+
}
629+
}
630+
false
631+
})
632+
.map(|(name, _)| *name);
633+
// iter is already sorted because it was constructed from a BTreeMap.
634+
iter
635+
}

tests/testsuite/features_namespaced.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,8 @@ regex
418418
p.cargo("run --features lazy_static")
419419
.with_stderr_data(str![[r#"
420420
[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.
421+
Dependency `lazy_static` would be enabled by these features:
422+
- `regex`
421423
422424
"#]])
423425
.with_status(101)

tests/testsuite/package_features.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ f3f4
340340
.with_stderr_data(str![[r#"
341341
[ERROR] Package `foo v0.1.0 ([ROOT]/foo)` does not have the feature `f2`
342342
343+
[HELP] a feature with a similar name exists: `f1`
344+
343345
"#]])
344346
.run();
345347

@@ -406,6 +408,8 @@ fn feature_default_resolver() {
406408
.with_stderr_data(str![[r#"
407409
[ERROR] Package `a v0.1.0 ([ROOT]/foo)` does not have the feature `testt`
408410
411+
[HELP] a feature with a similar name exists: `test`
412+
409413
"#]])
410414
.run();
411415

@@ -426,6 +430,169 @@ feature set
426430
.run();
427431
}
428432

433+
#[cargo_test]
434+
fn command_line_optional_dep() {
435+
// Enabling a dependency used as a `dep:` errors helpfully
436+
Package::new("bar", "1.0.0").publish();
437+
let p = project()
438+
.file(
439+
"Cargo.toml",
440+
r#"
441+
[package]
442+
name = "a"
443+
version = "0.1.0"
444+
edition = "2015"
445+
446+
[features]
447+
foo = ["dep:bar"]
448+
449+
[dependencies]
450+
bar = { version = "1.0.0", optional = true }
451+
"#,
452+
)
453+
.file("src/lib.rs", r#""#)
454+
.build();
455+
456+
p.cargo("check --features bar")
457+
.with_status(101)
458+
.with_stderr_data(str![[r#"
459+
[UPDATING] `dummy-registry` index
460+
[LOCKING] 1 package to latest compatible version
461+
[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.
462+
Dependency `bar` would be enabled by these features:
463+
- `foo`
464+
465+
"#]])
466+
.run();
467+
}
468+
469+
#[cargo_test]
470+
fn command_line_optional_dep_three_options() {
471+
// Trying to enable an optional dependency used as a `dep:` errors helpfully, when there are three features which would enable the dependency
472+
Package::new("bar", "1.0.0").publish();
473+
let p = project()
474+
.file(
475+
"Cargo.toml",
476+
r#"
477+
[package]
478+
name = "a"
479+
version = "0.1.0"
480+
edition = "2015"
481+
482+
[features]
483+
f1 = ["dep:bar"]
484+
f2 = ["dep:bar"]
485+
f3 = ["dep:bar"]
486+
487+
[dependencies]
488+
bar = { version = "1.0.0", optional = true }
489+
"#,
490+
)
491+
.file("src/lib.rs", r#""#)
492+
.build();
493+
494+
p.cargo("check --features bar")
495+
.with_status(101)
496+
.with_stderr_data(str![[r#"
497+
[UPDATING] `dummy-registry` index
498+
[LOCKING] 1 package to latest compatible version
499+
[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.
500+
Dependency `bar` would be enabled by these features:
501+
- `f1`
502+
- `f2`
503+
- `f3`
504+
505+
"#]])
506+
.run();
507+
}
508+
509+
#[cargo_test]
510+
fn command_line_optional_dep_many_options() {
511+
// Trying to enable an optional dependency used as a `dep:` errors helpfully, when there are many features which would enable the dependency
512+
Package::new("bar", "1.0.0").publish();
513+
let p = project()
514+
.file(
515+
"Cargo.toml",
516+
r#"
517+
[package]
518+
name = "a"
519+
version = "0.1.0"
520+
edition = "2015"
521+
522+
[features]
523+
f1 = ["dep:bar"]
524+
f2 = ["dep:bar"]
525+
f3 = ["dep:bar"]
526+
f4 = ["dep:bar"]
527+
528+
[dependencies]
529+
bar = { version = "1.0.0", optional = true }
530+
"#,
531+
)
532+
.file("src/lib.rs", r#""#)
533+
.build();
534+
535+
p.cargo("check --features bar")
536+
.with_status(101)
537+
.with_stderr_data(str![[r#"
538+
[UPDATING] `dummy-registry` index
539+
[LOCKING] 1 package to latest compatible version
540+
[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.
541+
Dependency `bar` would be enabled by these features:
542+
- `f1`
543+
- `f2`
544+
- `f3`
545+
...
546+
547+
"#]])
548+
.run();
549+
}
550+
551+
#[cargo_test]
552+
fn command_line_optional_dep_many_paths() {
553+
// Trying to enable an optional dependency used as a `dep:` errors helpfully, when a features would enable the dependency in multiple ways
554+
Package::new("bar", "1.0.0")
555+
.feature("a", &[])
556+
.feature("b", &[])
557+
.feature("c", &[])
558+
.feature("d", &[])
559+
.publish();
560+
let p = project()
561+
.file(
562+
"Cargo.toml",
563+
r#"
564+
[package]
565+
name = "a"
566+
version = "0.1.0"
567+
edition = "2015"
568+
569+
[features]
570+
f1 = ["dep:bar", "bar/a", "bar/b"] # Remove the implicit feature
571+
f2 = ["bar/b", "bar/c"] # Overlaps with previous
572+
f3 = ["bar/d"] # No overlap with previous
573+
574+
[dependencies]
575+
bar = { version = "1.0.0", optional = true }
576+
"#,
577+
)
578+
.file("src/lib.rs", r#""#)
579+
.build();
580+
581+
p.cargo("check --features bar")
582+
.with_status(101)
583+
.with_stderr_data(str![[r#"
584+
[UPDATING] `dummy-registry` index
585+
[LOCKING] 1 package to latest compatible version
586+
[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.
587+
Dependency `bar` would be enabled by these features:
588+
- `f1`
589+
- `f2`
590+
- `f3`
591+
592+
"#]])
593+
.run();
594+
}
595+
429596
#[cargo_test]
430597
fn virtual_member_slash() {
431598
// member slash feature syntax
@@ -655,6 +822,8 @@ m1-feature set
655822
.with_stderr_data(str![[r#"
656823
[ERROR] Package `member1 v0.1.0 ([ROOT]/foo/member1)` does not have the feature `m2-feature`
657824
825+
[HELP] a feature with a similar name exists: `m1-feature`
826+
658827
"#]])
659828
.run();
660829
}

0 commit comments

Comments
 (0)