diff --git a/src/cli/errors.rs b/src/cli/errors.rs index 598b71d9c3..9949f6bcfe 100644 --- a/src/cli/errors.rs +++ b/src/cli/errors.rs @@ -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)] @@ -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 = 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) - } -} diff --git a/src/errors.rs b/src/errors.rs index b9867dd875..70c57edc98 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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; @@ -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 { @@ -191,6 +196,30 @@ fn suggest_message(suggestion: &Option) -> 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"; diff --git a/tests/suite/cli_exact.rs b/tests/suite/cli_exact.rs index 5081f08b5a..584772c699 100644 --- a/tests/suite/cli_exact.rs +++ b/tests/suite/cli_exact.rs @@ -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 - +... "#]]); } @@ -823,7 +823,7 @@ async fn undefined_linked_toolchain() { .with_stdout(snapbox::str![[""]]) .with_stderr(snapbox::str![[r#" error: toolchain 'bogus' is not installed - +... "#]]); } diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index b4009ded65..65f2193d2f 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -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 diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 9ffba60f28..8e63f0e1e3 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -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;