Skip to content

Commit 7b9140f

Browse files
committed
Suggest similar feature names on CLI
Extend tests
1 parent 6d1f6be commit 7b9140f

File tree

3 files changed

+213
-5
lines changed

3 files changed

+213
-5
lines changed

src/cargo/core/resolver/dep_cache.rs

Lines changed: 42 additions & 5 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;
@@ -513,25 +515,60 @@ impl RequirementError {
513515
.filter(|dep| dep.name_in_toml() == feat)
514516
.collect();
515517
if deps.is_empty() {
518+
let closest =
519+
closest_msg(&feat.as_str(), summary.features().keys(), |key| &key);
516520
return match parent {
517521
None => ActivateError::Fatal(anyhow::format_err!(
518-
"Package `{}` does not have the feature `{}`",
522+
"Package `{}` does not have the feature `{}`{}",
519523
summary.package_id(),
520-
feat
524+
feat,
525+
closest
521526
)),
522527
Some(p) => {
523528
ActivateError::Conflict(p, ConflictReason::MissingFeatures(feat))
524529
}
525530
};
526531
}
527532
if deps.iter().any(|dep| dep.is_optional()) {
533+
let mut features = vec![];
534+
for (name, values) in summary.features() {
535+
for value in values {
536+
match value {
537+
FeatureValue::Dep { dep_name }
538+
| FeatureValue::DepFeature {
539+
dep_name,
540+
weak: false,
541+
..
542+
} if dep_name == &feat => {
543+
features.push(*name);
544+
// Don't add this feature multiple times
545+
break;
546+
}
547+
_ => (),
548+
}
549+
}
550+
}
551+
let mut suggestion = String::new();
552+
if !features.is_empty() {
553+
// Features is already sorted because it was constructed from a BTreeMap.
554+
suggestion =
555+
String::from("\nThis feature would be enabled by these features:");
556+
let mut features = features.iter();
557+
for feature in (&mut features).take(3) {
558+
let _ = write!(&mut suggestion, "\n\t- `{}`", feature);
559+
}
560+
if features.next().is_some() {
561+
suggestion.push_str("\n\t ...");
562+
}
563+
}
528564
match parent {
529565
None => ActivateError::Fatal(anyhow::format_err!(
530566
"Package `{}` does not have feature `{}`. It has an optional dependency \
531567
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.",
568+
syntax in the features table, so it does not have an implicit feature with that name.{}",
533569
summary.package_id(),
534-
feat
570+
feat,
571+
suggestion
535572
)),
536573
Some(p) => ActivateError::Conflict(
537574
p,
@@ -544,7 +581,7 @@ impl RequirementError {
544581
"Package `{}` does not have feature `{}`. It has a required dependency \
545582
with that name, but only optional dependencies can be used as features.",
546583
summary.package_id(),
547-
feat
584+
feat,
548585
)),
549586
Some(p) => ActivateError::Conflict(
550587
p,

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+
This feature 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+
Did you mean `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+
Did you mean `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
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+
This feature 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, 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+
This feature 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, 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+
This feature 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, 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+
This feature 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+
Did you mean `m1-feature`?
826+
658827
"#]])
659828
.run();
660829
}

0 commit comments

Comments
 (0)