Skip to content

Commit ce83c84

Browse files
committed
More helpful missing feature error message
1 parent 9e152bb commit ce83c84

File tree

2 files changed

+98
-22
lines changed

2 files changed

+98
-22
lines changed

src/cargo/core/workspace.rs

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,12 +1363,12 @@ impl<'gctx> Workspace<'gctx> {
13631363
}
13641364
}
13651365

1366-
fn report_unknown_features_error(
1366+
fn missing_feature_spelling_suggestions(
13671367
&self,
1368-
specs: &[PackageIdSpec],
1368+
selected_members: &[&Package],
13691369
cli_features: &CliFeatures,
13701370
found_features: &BTreeSet<FeatureValue>,
1371-
) -> CargoResult<()> {
1371+
) -> Vec<String> {
13721372
// Keeps track of which features were contained in summary of `member` to suggest similar features in errors
13731373
let mut summary_features: Vec<InternedString> = Default::default();
13741374

@@ -1387,10 +1387,7 @@ impl<'gctx> Workspace<'gctx> {
13871387
let mut optional_dependency_names_per_member: BTreeMap<&Package, BTreeSet<InternedString>> =
13881388
Default::default();
13891389

1390-
for member in self
1391-
.members()
1392-
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
1393-
{
1390+
for &member in selected_members {
13941391
// Only include features this member defines.
13951392
let summary = member.summary();
13961393

@@ -1428,7 +1425,7 @@ impl<'gctx> Workspace<'gctx> {
14281425
edit_distance(a.as_str(), b.as_str(), 3).is_some()
14291426
};
14301427

1431-
let suggestions: Vec<_> = cli_features
1428+
cli_features
14321429
.features
14331430
.difference(found_features)
14341431
.map(|feature| match feature {
@@ -1522,27 +1519,88 @@ impl<'gctx> Workspace<'gctx> {
15221519
})
15231520
.sorted()
15241521
.take(5)
1525-
.collect();
1522+
.collect()
1523+
}
15261524

1525+
fn report_unknown_features_error(
1526+
&self,
1527+
specs: &[PackageIdSpec],
1528+
cli_features: &CliFeatures,
1529+
found_features: &BTreeSet<FeatureValue>,
1530+
) -> CargoResult<()> {
15271531
let unknown: Vec<_> = cli_features
15281532
.features
15291533
.difference(found_features)
15301534
.map(|feature| feature.to_string())
15311535
.sorted()
15321536
.collect();
15331537

1534-
if suggestions.is_empty() {
1535-
bail!(
1536-
"none of the selected packages contains these features: {}",
1538+
let (selected_members, unselected_members): (Vec<_>, Vec<_>) = self
1539+
.members()
1540+
.partition(|member| specs.iter().any(|spec| spec.matches(member.package_id())));
1541+
1542+
let missing_packages_with_the_features = unselected_members
1543+
.into_iter()
1544+
.filter(|member| {
1545+
unknown
1546+
.iter()
1547+
.any(|feature| member.summary().features().contains_key(&**feature))
1548+
})
1549+
.map(|m| m.name())
1550+
.collect_vec();
1551+
1552+
let these_features = if unknown.len() == 1 {
1553+
"this feature"
1554+
} else {
1555+
"these features"
1556+
};
1557+
let mut msg = if let [singular] = &selected_members[..] {
1558+
format!(
1559+
"the package '{}' does not contain {these_features}: {}",
1560+
singular.name(),
15371561
unknown.join(", ")
1538-
);
1562+
)
15391563
} else {
1540-
bail!(
1541-
"none of the selected packages contains these features: {}, did you mean: {}?",
1542-
unknown.join(", "),
1543-
suggestions.join(", ")
1564+
let names = selected_members.iter().map(|m| m.name()).join(", ");
1565+
format!("none of the selected packages contains {these_features}: {}\nselected packages: {names}", unknown.join(", "))
1566+
};
1567+
1568+
use std::fmt::Write;
1569+
if !missing_packages_with_the_features.is_empty() {
1570+
write!(
1571+
&mut msg,
1572+
"\nhelp: include package{} with the missing feature{}:",
1573+
if missing_packages_with_the_features.len() != 1 {
1574+
"s"
1575+
} else {
1576+
""
1577+
},
1578+
if unknown.len() != 1 { "s" } else { "" }
1579+
)?;
1580+
for member in &missing_packages_with_the_features {
1581+
write!(&mut msg, " --package {member}")?;
1582+
}
1583+
} else {
1584+
let suggestions = self.missing_feature_spelling_suggestions(
1585+
&selected_members,
1586+
cli_features,
1587+
found_features,
15441588
);
1589+
if !suggestions.is_empty() {
1590+
write!(
1591+
&mut msg,
1592+
"\nhelp: there {}: {}",
1593+
if suggestions.len() == 1 {
1594+
"is a similarly named feature"
1595+
} else {
1596+
"are similarly named features"
1597+
},
1598+
suggestions.join(", ")
1599+
)?;
1600+
}
15451601
}
1602+
1603+
bail!("{msg}")
15461604
}
15471605

15481606
/// New command-line feature selection behavior with resolver = "2" or the

tests/testsuite/package_features.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,31 +75,48 @@ fn virtual_no_default_features() {
7575
p.cargo("check --features foo")
7676
.with_status(101)
7777
.with_stderr_data(str![[r#"
78-
[ERROR] none of the selected packages contains these features: foo, did you mean: f1?
78+
[ERROR] none of the selected packages contains this feature: foo
79+
selected packages: a, b
80+
[HELP] there is a similarly named feature: f1
7981
8082
"#]])
8183
.run();
8284

8385
p.cargo("check --features a/dep1,b/f1,b/f2,f2")
8486
.with_status(101)
8587
.with_stderr_data(str![[r#"
86-
[ERROR] none of the selected packages contains these features: b/f2, f2, did you mean: f1?
88+
[ERROR] none of the selected packages contains these features: b/f2, f2
89+
selected packages: a, b
90+
[HELP] there is a similarly named feature: f1
8791
8892
"#]])
8993
.run();
9094

9195
p.cargo("check --features a/dep,b/f1,b/f2,f2")
9296
.with_status(101)
9397
.with_stderr_data(str![[r#"
94-
[ERROR] none of the selected packages contains these features: a/dep, b/f2, f2, did you mean: a/dep1, f1?
98+
[ERROR] none of the selected packages contains these features: a/dep, b/f2, f2
99+
selected packages: a, b
100+
[HELP] there are similarly named features: a/dep1, f1
95101
96102
"#]])
97103
.run();
98104

99105
p.cargo("check --features a/dep,a/dep1")
100106
.with_status(101)
101107
.with_stderr_data(str![[r#"
102-
[ERROR] none of the selected packages contains these features: a/dep, did you mean: b/f1?
108+
[ERROR] none of the selected packages contains this feature: a/dep
109+
selected packages: a, b
110+
[HELP] there is a similarly named feature: b/f1
111+
112+
"#]])
113+
.run();
114+
115+
p.cargo("check -p b --features=dep1")
116+
.with_status(101)
117+
.with_stderr_data(str![[r#"
118+
[ERROR] the package 'b' does not contain this feature: dep1
119+
[HELP] include package with the missing feature: --package a
103120
104121
"#]])
105122
.run();
@@ -126,7 +143,8 @@ fn virtual_typo_member_feature() {
126143
.cargo("check --features a/deny-warning")
127144
.with_status(101)
128145
.with_stderr_data(str![[r#"
129-
[ERROR] none of the selected packages contains these features: a/deny-warning, did you mean: a/deny-warnings?
146+
[ERROR] the package 'a' does not contain this feature: a/deny-warning
147+
[HELP] there is a similarly named feature: a/deny-warnings
130148
131149
"#]])
132150
.run();

0 commit comments

Comments
 (0)