Skip to content

Commit bee9c89

Browse files
committed
Auto merge of #11098 - Emilgardis:early-fail, r=epage
make unknown features on `cargo add` more discoverable When adding an unknown feature via `cargo add -F krate/feat`, it can be easy to miss the fact that the change failed. This fixes that by showing the following output on fail <img width="474" alt="image" src="https://user-images.githubusercontent.com/1502855/191100141-3603cc9a-d4b6-4d6a-bbc6-41b34144b3f0.png">
2 parents a74a8d0 + 1cc21b6 commit bee9c89

File tree

11 files changed

+161
-58
lines changed

11 files changed

+161
-58
lines changed

src/cargo/ops/cargo_add/mod.rs

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ mod crate_spec;
55
use std::collections::BTreeMap;
66
use std::collections::BTreeSet;
77
use std::collections::VecDeque;
8+
use std::fmt::Write;
89
use std::path::Path;
910

1011
use anyhow::Context as _;
1112
use cargo_util::paths;
1213
use indexmap::IndexSet;
14+
use itertools::Itertools;
1315
use termcolor::Color::Green;
1416
use termcolor::Color::Red;
1517
use termcolor::ColorSpec;
@@ -99,7 +101,7 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
99101
table_option.map_or(true, |table| is_sorted(table.iter().map(|(name, _)| name)))
100102
});
101103
for dep in deps {
102-
print_msg(&mut options.config.shell(), &dep, &dep_table)?;
104+
print_action_msg(&mut options.config.shell(), &dep, &dep_table)?;
103105
if let Some(Source::Path(src)) = dep.source() {
104106
if src.path == manifest.path.parent().unwrap_or_else(|| Path::new("")) {
105107
anyhow::bail!(
@@ -124,11 +126,63 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
124126
inherited_features.iter().map(|s| s.as_str()).collect();
125127
unknown_features.extend(inherited_features.difference(&available_features).copied());
126128
}
129+
127130
unknown_features.sort();
131+
128132
if !unknown_features.is_empty() {
129-
anyhow::bail!("unrecognized features: {unknown_features:?}");
133+
let (mut activated, mut deactivated) = dep.features();
134+
// Since the unknown features have been added to the DependencyUI we need to remove
135+
// them to present the "correct" features that can be specified for the crate.
136+
deactivated.retain(|f| !unknown_features.contains(f));
137+
activated.retain(|f| !unknown_features.contains(f));
138+
139+
let mut message = format!(
140+
"unrecognized feature{} for crate {}: {}\n",
141+
if unknown_features.len() == 1 { "" } else { "s" },
142+
dep.name,
143+
unknown_features.iter().format(", "),
144+
);
145+
if activated.is_empty() && deactivated.is_empty() {
146+
write!(message, "no features available for crate {}", dep.name)?;
147+
} else {
148+
if !deactivated.is_empty() {
149+
writeln!(
150+
message,
151+
"disabled features:\n {}",
152+
deactivated
153+
.iter()
154+
.map(|s| s.to_string())
155+
.coalesce(|x, y| if x.len() + y.len() < 78 {
156+
Ok(format!("{x}, {y}"))
157+
} else {
158+
Err((x, y))
159+
})
160+
.into_iter()
161+
.format("\n ")
162+
)?
163+
}
164+
if !activated.is_empty() {
165+
writeln!(
166+
message,
167+
"enabled features:\n {}",
168+
activated
169+
.iter()
170+
.map(|s| s.to_string())
171+
.coalesce(|x, y| if x.len() + y.len() < 78 {
172+
Ok(format!("{x}, {y}"))
173+
} else {
174+
Err((x, y))
175+
})
176+
.into_iter()
177+
.format("\n ")
178+
)?
179+
}
180+
}
181+
anyhow::bail!(message.trim().to_owned());
130182
}
131183

184+
print_dep_table_msg(&mut options.config.shell(), &dep)?;
185+
132186
manifest.insert_into_table(&dep_table, &dep)?;
133187
manifest.gc_dep(dep.toml_key());
134188
}
@@ -634,6 +688,42 @@ impl DependencyUI {
634688
})
635689
.collect();
636690
}
691+
692+
fn features(&self) -> (IndexSet<&str>, IndexSet<&str>) {
693+
let mut activated: IndexSet<_> =
694+
self.features.iter().flatten().map(|s| s.as_str()).collect();
695+
if self.default_features().unwrap_or(true) {
696+
activated.insert("default");
697+
}
698+
activated.extend(self.inherited_features.iter().flatten().map(|s| s.as_str()));
699+
let mut walk: VecDeque<_> = activated.iter().cloned().collect();
700+
while let Some(next) = walk.pop_front() {
701+
walk.extend(
702+
self.available_features
703+
.get(next)
704+
.into_iter()
705+
.flatten()
706+
.map(|s| s.as_str()),
707+
);
708+
activated.extend(
709+
self.available_features
710+
.get(next)
711+
.into_iter()
712+
.flatten()
713+
.map(|s| s.as_str()),
714+
);
715+
}
716+
activated.remove("default");
717+
activated.sort();
718+
let mut deactivated = self
719+
.available_features
720+
.keys()
721+
.filter(|f| !activated.contains(f.as_str()) && *f != "default")
722+
.map(|f| f.as_str())
723+
.collect::<IndexSet<_>>();
724+
deactivated.sort();
725+
(activated, deactivated)
726+
}
637727
}
638728

639729
impl<'s> From<&'s Summary> for DependencyUI {
@@ -697,9 +787,7 @@ fn populate_available_features(
697787
Ok(dependency)
698788
}
699789

700-
fn print_msg(shell: &mut Shell, dep: &DependencyUI, section: &[String]) -> CargoResult<()> {
701-
use std::fmt::Write;
702-
790+
fn print_action_msg(shell: &mut Shell, dep: &DependencyUI, section: &[String]) -> CargoResult<()> {
703791
if matches!(shell.verbosity(), crate::core::shell::Verbosity::Quiet) {
704792
return Ok(());
705793
}
@@ -736,38 +824,14 @@ fn print_msg(shell: &mut Shell, dep: &DependencyUI, section: &[String]) -> Cargo
736824
};
737825
write!(message, " {section}")?;
738826
write!(message, ".")?;
739-
shell.status("Adding", message)?;
740-
741-
let mut activated: IndexSet<_> = dep.features.iter().flatten().map(|s| s.as_str()).collect();
742-
if dep.default_features().unwrap_or(true) {
743-
activated.insert("default");
744-
}
745-
activated.extend(dep.inherited_features.iter().flatten().map(|s| s.as_str()));
746-
let mut walk: VecDeque<_> = activated.iter().cloned().collect();
747-
while let Some(next) = walk.pop_front() {
748-
walk.extend(
749-
dep.available_features
750-
.get(next)
751-
.into_iter()
752-
.flatten()
753-
.map(|s| s.as_str()),
754-
);
755-
activated.extend(
756-
dep.available_features
757-
.get(next)
758-
.into_iter()
759-
.flatten()
760-
.map(|s| s.as_str()),
761-
);
827+
shell.status("Adding", message)
828+
}
829+
830+
fn print_dep_table_msg(shell: &mut Shell, dep: &DependencyUI) -> CargoResult<()> {
831+
if matches!(shell.verbosity(), crate::core::shell::Verbosity::Quiet) {
832+
return Ok(());
762833
}
763-
activated.remove("default");
764-
activated.sort();
765-
let mut deactivated = dep
766-
.available_features
767-
.keys()
768-
.filter(|f| !activated.contains(f.as_str()) && *f != "default")
769-
.collect::<Vec<_>>();
770-
deactivated.sort();
834+
let (activated, deactivated) = dep.features();
771835
if !activated.is_empty() || !deactivated.is_empty() {
772836
let prefix = format!("{:>13}", " ");
773837
let suffix = if let Some(version) = &dep.available_version {
Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
Updating `dummy-registry` index
22
Adding your-face v99999.0.0 to dependencies.
3-
Features:
4-
+ noze
5-
- ears
6-
- eyes
7-
- mouth
8-
- nose
9-
error: unrecognized features: ["noze"]
3+
error: unrecognized feature for crate your-face: noze
4+
disabled features:
5+
ears, eyes, mouth, nose
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../add-basic.in
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::Project;
4+
5+
use crate::cargo_add::init_registry;
6+
use cargo_test_support::curr_dir;
7+
8+
#[cargo_test]
9+
fn features_unknown_no_features() {
10+
init_registry();
11+
let project = Project::from_template(curr_dir!().join("in"));
12+
let project_root = project.root();
13+
let cwd = &project_root;
14+
15+
snapbox::cmd::Command::cargo_ui()
16+
.arg("add")
17+
.arg_line("my-package --features noze")
18+
.current_dir(cwd)
19+
.assert()
20+
.code(101)
21+
.stdout_matches_path(curr_dir!().join("stdout.log"))
22+
.stderr_matches_path(curr_dir!().join("stderr.log"));
23+
24+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
25+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Updating `dummy-registry` index
2+
Adding my-package v99999.0.0 to dependencies.
3+
error: unrecognized feature for crate my-package: noze
4+
no features available for crate my-package

tests/testsuite/cargo_add/features_unknown_no_features/stdout.log

Whitespace-only changes.

tests/testsuite/cargo_add/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ mod features_multiple_occurrences;
2020
mod features_preserve;
2121
mod features_spaced_values;
2222
mod features_unknown;
23+
mod features_unknown_no_features;
2324
mod git;
2425
mod git_branch;
2526
mod git_conflicts_namever;

tests/testsuite/cargo_add/unknown_inherited_feature/in/dependency/Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ version = "0.0.0"
66
default-base = []
77
default-test-base = []
88
default-merge-base = []
9-
default = ["default-base", "default-test-base", "default-merge-base"]
9+
long-feature-name-because-of-formatting-reasons = []
10+
default = [
11+
"default-base",
12+
"default-test-base",
13+
"default-merge-base",
14+
"long-feature-name-because-of-formatting-reasons",
15+
]
1016
test-base = []
1117
test = ["test-base", "default-test-base"]
1218
merge-base = []
1319
merge = ["merge-base", "default-merge-base"]
14-
unrelated = []
20+
unrelated = []

tests/testsuite/cargo_add/unknown_inherited_feature/out/dependency/Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ version = "0.0.0"
66
default-base = []
77
default-test-base = []
88
default-merge-base = []
9-
default = ["default-base", "default-test-base", "default-merge-base"]
9+
long-feature-name-because-of-formatting-reasons = []
10+
default = [
11+
"default-base",
12+
"default-test-base",
13+
"default-merge-base",
14+
"long-feature-name-because-of-formatting-reasons",
15+
]
1016
test-base = []
1117
test = ["test-base", "default-test-base"]
1218
merge-base = []
1319
merge = ["merge-base", "default-merge-base"]
14-
unrelated = []
20+
unrelated = []
Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
Adding foo (workspace) to dependencies.
2-
Features as of v0.0.0:
3-
+ default-base
4-
+ default-merge-base
5-
+ default-test-base
6-
+ not_recognized
7-
+ test
8-
+ test-base
9-
- merge
10-
- merge-base
11-
- unrelated
12-
error: unrecognized features: ["not_recognized"]
2+
error: unrecognized feature for crate foo: not_recognized
3+
disabled features:
4+
merge, merge-base, unrelated
5+
enabled features:
6+
default-base, default-merge-base, default-test-base
7+
long-feature-name-because-of-formatting-reasons, test, test-base

0 commit comments

Comments
 (0)