Skip to content

Commit 64e3cd1

Browse files
authored
feat(add): suggest similarly named features (#15438)
### What does this PR try to resolve? Fixes #15436 ### How should we test and review this PR? There are 3 tests for each test case: - there are no feature suggestions - there's only one feature suggestion (most common) - there are several feature suggestions
2 parents 39b492c + ba494bc commit 64e3cd1

File tree

19 files changed

+269
-28
lines changed

19 files changed

+269
-28
lines changed

src/cargo/ops/cargo_add/mod.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::core::Summary;
2929
use crate::core::Workspace;
3030
use crate::sources::source::QueryKind;
3131
use crate::util::cache_lock::CacheLockMode;
32+
use crate::util::edit_distance;
3233
use crate::util::style;
3334
use crate::util::toml::lookup_path_base;
3435
use crate::util::toml_mut::dependency::Dependency;
@@ -159,19 +160,32 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
159160
activated.retain(|f| !unknown_features.contains(f));
160161

161162
let mut message = format!(
162-
"unrecognized feature{} for crate {}: {}\n",
163+
"unrecognized feature{} for crate {}: {}",
163164
if unknown_features.len() == 1 { "" } else { "s" },
164165
dep.name,
165166
unknown_features.iter().format(", "),
166167
);
167168
if activated.is_empty() && deactivated.is_empty() {
168-
write!(message, "no features available for crate {}", dep.name)?;
169+
write!(message, "\n\nno features available for crate {}", dep.name)?;
169170
} else {
170-
if !deactivated.is_empty() {
171+
let mut suggested = false;
172+
for unknown_feature in &unknown_features {
173+
let suggestion = edit_distance::closest_msg(
174+
unknown_feature,
175+
deactivated.iter().chain(activated.iter()),
176+
|dep| *dep,
177+
"feature",
178+
);
179+
if !suggestion.is_empty() {
180+
write!(message, "{suggestion}")?;
181+
suggested = true;
182+
}
183+
}
184+
if !deactivated.is_empty() && !suggested {
171185
if deactivated.len() <= MAX_FEATURE_PRINTS {
172-
writeln!(
186+
write!(
173187
message,
174-
"disabled features:\n {}",
188+
"\n\ndisabled features:\n {}",
175189
deactivated
176190
.iter()
177191
.map(|s| s.to_string())
@@ -184,14 +198,18 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
184198
.format("\n ")
185199
)?;
186200
} else {
187-
writeln!(message, "{} disabled features available", deactivated.len())?;
201+
write!(
202+
message,
203+
"\n\n{} disabled features available",
204+
deactivated.len()
205+
)?;
188206
}
189207
}
190-
if !activated.is_empty() {
208+
if !activated.is_empty() && !suggested {
191209
if deactivated.len() + activated.len() <= MAX_FEATURE_PRINTS {
192210
writeln!(
193211
message,
194-
"enabled features:\n {}",
212+
"\n\nenabled features:\n {}",
195213
activated
196214
.iter()
197215
.map(|s| s.to_string())
@@ -204,7 +222,11 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
204222
.format("\n ")
205223
)?;
206224
} else {
207-
writeln!(message, "{} enabled features available", activated.len())?;
225+
writeln!(
226+
message,
227+
"\n\n{} enabled features available",
228+
activated.len()
229+
)?;
208230
}
209231
}
210232
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2024"
7+

tests/testsuite/cargo_add/feature_suggestion_multiple/in/src/lib.rs

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use cargo_test_support::current_dir;
2+
use cargo_test_support::file;
3+
use cargo_test_support::prelude::*;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(current_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
12+
cargo_test_support::registry::Package::new("my-package", "0.1.0+my-package")
13+
.feature("bar", &[])
14+
.feature("foo", &[])
15+
.publish();
16+
17+
snapbox::cmd::Command::cargo_ui()
18+
.arg("add")
19+
.arg_line("my-package --features baz --features feo")
20+
.current_dir(cwd)
21+
.assert()
22+
.failure()
23+
.stderr_eq(file!["stderr.term.svg"]);
24+
}
Lines changed: 40 additions & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2024"
7+

tests/testsuite/cargo_add/feature_suggestion_none/in/src/lib.rs

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use cargo_test_support::current_dir;
2+
use cargo_test_support::file;
3+
use cargo_test_support::prelude::*;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(current_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
12+
cargo_test_support::registry::Package::new("my-package", "0.1.0+my-package")
13+
.feature("bar", &[])
14+
.feature("foo", &[])
15+
.publish();
16+
17+
snapbox::cmd::Command::cargo_ui()
18+
.arg("add")
19+
.arg_line("my-package --features none_existent")
20+
.current_dir(cwd)
21+
.assert()
22+
.failure()
23+
.stderr_eq(file!["stderr.term.svg"]);
24+
}
Lines changed: 38 additions & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2024"
7+

tests/testsuite/cargo_add/feature_suggestion_single/in/src/lib.rs

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use cargo_test_support::current_dir;
2+
use cargo_test_support::file;
3+
use cargo_test_support::prelude::*;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(current_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
12+
cargo_test_support::registry::Package::new("my-package", "0.1.0+my-package")
13+
.feature("bar", &[])
14+
.publish();
15+
16+
snapbox::cmd::Command::cargo_ui()
17+
.arg("add")
18+
.arg_line("my-package --features baz")
19+
.current_dir(cwd)
20+
.assert()
21+
.failure()
22+
.stderr_eq(file!["stderr.term.svg"]);
23+
}
Lines changed: 36 additions & 0 deletions
Loading

tests/testsuite/cargo_add/features_error_activated_over_limit/stderr.term.svg

Lines changed: 4 additions & 4 deletions
Loading

tests/testsuite/cargo_add/features_error_deactivated_over_limit/stderr.term.svg

Lines changed: 7 additions & 3 deletions
Loading

tests/testsuite/cargo_add/features_unknown/stderr.term.svg

Lines changed: 2 additions & 2 deletions
Loading

tests/testsuite/cargo_add/features_unknown_no_features/stderr.term.svg

Lines changed: 5 additions & 3 deletions
Loading

tests/testsuite/cargo_add/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ mod dev_existing_path_base;
2121
mod dev_prefer_existing_version;
2222
mod dry_run;
2323
mod empty_dep_name;
24+
mod feature_suggestion_multiple;
25+
mod feature_suggestion_none;
26+
mod feature_suggestion_single;
2427
mod features;
2528
mod features_activated_over_limit;
2629
mod features_deactivated_over_limit;

0 commit comments

Comments
 (0)