Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 1 addition & 39 deletions src/cli/errors.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
#![allow(clippy::large_enum_variant)]
#![allow(dead_code)]
use std::{io, path::PathBuf};

use std::io;
use std::path::PathBuf;
use std::sync::LazyLock;

use regex::Regex;
use strsim::damerau_levenshtein;
use thiserror::Error as ThisError;

#[derive(ThisError, Debug)]
Expand All @@ -20,34 +13,3 @@ pub enum CliError {
#[error("failure during windows uninstall")]
WindowsUninstallMadness,
}

fn maybe_suggest_toolchain(bad_name: &str) -> String {
let bad_name = &bad_name.to_ascii_lowercase();
static VALID_CHANNELS: &[&str] = &["stable", "beta", "nightly"];
static NUMBERED: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+\.[0-9]+$").unwrap());
if NUMBERED.is_match(bad_name) {
return format!(". Toolchain numbers tend to have three parts, e.g. {bad_name}.0");
}

// Suggest only for very small differences
// High number can result in inaccurate suggestions for short queries e.g. `rls`
const MAX_DISTANCE: usize = 3;

let mut scored: Vec<_> = VALID_CHANNELS
.iter()
.filter_map(|s| {
let distance = damerau_levenshtein(bad_name, s);
if distance <= MAX_DISTANCE {
Some((distance, s))
} else {
None
}
})
.collect();
scored.sort();
if scored.is_empty() {
String::new()
} else {
format!(". Did you mean '{}'?", scored[0].1)
}
}
49 changes: 39 additions & 10 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#![allow(clippy::large_enum_variant)]

use std::ffi::OsString;
use std::fmt::{Debug, Write as FmtWrite};
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::{
borrow::Cow,
ffi::OsString,
fmt::{Debug, Write as FmtWrite},
io::{self, Write},
path::PathBuf,
};

use platforms::Platform;
use thiserror::Error as ThisError;
Expand Down Expand Up @@ -119,11 +121,14 @@ pub enum RustupError {
ToolchainNotInstallable(String),
#[error(
"toolchain '{name}' is not installed{}",
if let ToolchainName::Official(t) = name {
let t = if *is_active { "" } else { &format!(" {t}") };
format!("\nhelp: run `rustup toolchain install{t}` to install it")
} else {
String::new()
match name {
ToolchainName::Official(t) => {
let t = if *is_active { "" } else { &format!(" {t}") };
Cow::Owned(format!(
"\nhelp: run `rustup toolchain install{t}` to install it",
))
}
ToolchainName::Custom(t) => maybe_suggest_toolchain(t),
},
)]
ToolchainNotInstalled {
Expand Down Expand Up @@ -191,6 +196,30 @@ fn suggest_message(suggestion: &Option<String>) -> String {
}
}

fn maybe_suggest_toolchain(bad_name: &str) -> Cow<'static, str> {
if bad_name.bytes().all(|b| b".0123456789".contains(&b)) {
return Cow::Borrowed("\nhelp: official versioned channels take the form X.Y or X.Y.Z");
}

// Suggest only for very small differences
// High number can result in inaccurate suggestions for short queries e.g. `rls`
const MAX_DISTANCE: usize = 3;

let bad_name = &bad_name.to_lowercase();
let suggestion = ["stable", "beta", "nightly"]
.into_iter()
.filter_map(|s| {
let distance = strsim::damerau_levenshtein(bad_name, s);
(distance <= MAX_DISTANCE).then_some((distance, s))
})
.max();

match suggestion {
Some((_, s)) => Cow::Owned(format!("\nhelp: did you mean '{s}'?")),
None => Cow::Borrowed("\nhelp: maybe you have mistyped the toolchain name?"),
}
}

pub(crate) const NIGHTLY_COMPONENT_NOTE: &str =
"note: sometimes not all components are available in any given nightly";

Expand Down
4 changes: 2 additions & 2 deletions tests/suite/cli_exact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ async fn default_custom_not_installed_toolchain() {
.is_err()
.with_stderr(snapbox::str![[r#"
error: toolchain 'nightly-2016-03-1' is not installed

...
"#]]);
}

Expand Down Expand Up @@ -823,7 +823,7 @@ async fn undefined_linked_toolchain() {
.with_stdout(snapbox::str![[""]])
.with_stderr(snapbox::str![[r#"
error: toolchain 'bogus' is not installed

...
"#]]);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/suite/cli_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,7 @@ active because: overridden by +toolchain on the command line
.await
.with_stderr(snapbox::str![[r#"
error:[..] toolchain 'foo' is not installed[..]

...
"#]])
.is_err();
cx.config
Expand Down
34 changes: 34 additions & 0 deletions tests/suite/cli_rustup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,40 @@ rustc-[HOST_TUPLE]
"#]]);
}

#[tokio::test]
async fn default_typo_guess() {
let cx = CliTestContext::new(Scenario::SimpleV2).await;
cx.config
.expect(["rustup", "default", "fable"])
.await
.is_err()
.with_stderr(snapbox::str![[r#"
error: toolchain 'fable' is not installed
help: did you mean 'stable'?

"#]]);

cx.config
.expect(["rustup", "default", "1.23.4."])
.await
.is_err()
.with_stderr(snapbox::str![[r#"
error: toolchain '1.23.4.' is not installed
help: official versioned channels take the form X.Y or X.Y.Z

"#]]);

cx.config
.expect(["rustup", "default", "invalid-toolchain"])
.await
.is_err()
.with_stderr(snapbox::str![[r#"
error: toolchain 'invalid-toolchain' is not installed
help: maybe you have mistyped the toolchain name?

"#]]);
}

#[tokio::test]
async fn default_override() {
let cx = CliTestContext::new(Scenario::SimpleV2).await;
Expand Down
Loading