From 41aa1631f5c3e859eaf59cd4586c6bb40c432433 Mon Sep 17 00:00:00 2001 From: rami3l Date: Fri, 4 Sep 2026 21:19:01 +0200 Subject: [PATCH 1/2] style(errors): reorganize imports --- src/cli/errors.rs | 5 +---- src/errors.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/cli/errors.rs b/src/cli/errors.rs index 598b71d9c3..60d1c0e447 100644 --- a/src/cli/errors.rs +++ b/src/cli/errors.rs @@ -1,9 +1,6 @@ -#![allow(clippy::large_enum_variant)] #![allow(dead_code)] -use std::io; -use std::path::PathBuf; -use std::sync::LazyLock; +use std::{io, path::PathBuf, sync::LazyLock}; use regex::Regex; use strsim::damerau_levenshtein; diff --git a/src/errors.rs b/src/errors.rs index b9867dd875..ef284bccfc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,10 +1,11 @@ #![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::{ + ffi::OsString, + fmt::{Debug, Write as FmtWrite}, + io::{self, Write}, + path::PathBuf, +}; use platforms::Platform; use thiserror::Error as ThisError; From 0033704f9cfaa3e4453b416e0b194416d9c7f42b Mon Sep 17 00:00:00 2001 From: rami3l Date: Fri, 4 Sep 2026 20:49:20 +0200 Subject: [PATCH 2/2] feat(errors): guess toolchain typo in `RustupError::ToolchainNotInstalled` --- src/cli/errors.rs | 37 +------------------------------------ src/errors.rs | 38 +++++++++++++++++++++++++++++++++----- tests/suite/cli_exact.rs | 4 ++-- tests/suite/cli_misc.rs | 2 +- tests/suite/cli_rustup.rs | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 44 deletions(-) diff --git a/src/cli/errors.rs b/src/cli/errors.rs index 60d1c0e447..9949f6bcfe 100644 --- a/src/cli/errors.rs +++ b/src/cli/errors.rs @@ -1,9 +1,5 @@ -#![allow(dead_code)] +use std::{io, path::PathBuf}; -use std::{io, path::PathBuf, sync::LazyLock}; - -use regex::Regex; -use strsim::damerau_levenshtein; use thiserror::Error as ThisError; #[derive(ThisError, Debug)] @@ -17,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 ef284bccfc..70c57edc98 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,7 @@ #![allow(clippy::large_enum_variant)] use std::{ + borrow::Cow, ffi::OsString, fmt::{Debug, Write as FmtWrite}, io::{self, Write}, @@ -120,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 { @@ -192,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;