From dc7210bca30bc9bcc45887f555b78fa8b02e3b89 Mon Sep 17 00:00:00 2001 From: Matt Boulanger Date: Sun, 26 Apr 2020 14:19:21 -0700 Subject: [PATCH] Suggest aliases for unknown recipes (#624) --- src/common.rs | 4 ++-- src/config.rs | 2 +- src/justfile.rs | 54 ++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 5 +++- src/runtime_error.rs | 4 ++-- src/suggestion.rs | 17 ++++++++++++++ tests/integration.rs | 32 ++++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 src/suggestion.rs diff --git a/src/common.rs b/src/common.rs index 7dee2b4..1ada469 100644 --- a/src/common.rs +++ b/src/common.rs @@ -61,8 +61,8 @@ pub(crate) use crate::{ recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, - string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token, - token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, + string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, + thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, }; diff --git a/src/config.rs b/src/config.rs index 8e8000a..52df395 100644 --- a/src/config.rs +++ b/src/config.rs @@ -632,7 +632,7 @@ impl Config { } else { eprintln!("Justfile does not contain recipe `{}`.", name); if let Some(suggestion) = justfile.suggest(name) { - eprintln!("Did you mean `{}`?", suggestion); + eprintln!("{}", suggestion); } Err(EXIT_FAILURE) } diff --git a/src/justfile.rs b/src/justfile.rs index e4c9cb3..a027db4 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -28,19 +28,30 @@ impl<'src> Justfile<'src> { self.recipes.len() } - pub(crate) fn suggest(&self, name: &str) -> Option<&'src str> { + pub(crate) fn suggest(&self, input: &str) -> Option { let mut suggestions = self .recipes .keys() - .map(|suggestion| (edit_distance(suggestion, name), suggestion)) - .collect::>(); - suggestions.sort(); - if let Some(&(distance, suggestion)) = suggestions.first() { - if distance < 3 { - return Some(suggestion); - } - } - None + .map(|name| { + (edit_distance(name, input), Suggestion { + name, + target: None, + }) + }) + .chain(self.aliases.iter().map(|(name, alias)| { + (edit_distance(name, input), Suggestion { + name, + target: Some(alias.target.name.lexeme()), + }) + })) + .filter(|(distance, _suggestion)| distance < &3) + .collect::>(); + suggestions.sort_by_key(|(distance, _suggestion)| *distance); + + suggestions + .into_iter() + .map(|(_distance, suggestion)| suggestion) + .next() } pub(crate) fn run<'run>( @@ -301,6 +312,29 @@ mod tests { } } + run_error! { + name: unknown_recipes_show_alias_suggestion, + src: " + foo: + echo foo + + alias z := foo + ", + args: ["zz"], + error: UnknownRecipes { + recipes, + suggestion, + }, + check: { + assert_eq!(recipes, &["zz"]); + assert_eq!(suggestion, Some(Suggestion { + name: "z", + target: Some("foo"), + } + )); + } + } + // This test exists to make sure that shebang recipes run correctly. Although // this script is still executed by a shell its behavior depends on the value of // a variable and continuing even though a command fails, whereas in plain diff --git a/src/lib.rs b/src/lib.rs index 24d17e5..398ba50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,10 +23,12 @@ clippy::result_expect_used, clippy::shadow_unrelated, clippy::string_add, + clippy::struct_excessive_bools, clippy::too_many_lines, clippy::unreachable, clippy::use_debug, - clippy::wildcard_enum_match_arm + clippy::wildcard_enum_match_arm, + clippy::wildcard_imports )] #[macro_use] @@ -111,6 +113,7 @@ mod shebang; mod show_whitespace; mod string_literal; mod subcommand; +mod suggestion; mod table; mod thunk; mod token; diff --git a/src/runtime_error.rs b/src/runtime_error.rs index 70d73c3..511ae45 100644 --- a/src/runtime_error.rs +++ b/src/runtime_error.rs @@ -56,7 +56,7 @@ pub(crate) enum RuntimeError<'src> { }, UnknownRecipes { recipes: Vec<&'src str>, - suggestion: Option<&'src str>, + suggestion: Option>, }, Unknown { recipe: &'src str, @@ -117,7 +117,7 @@ impl<'src> Display for RuntimeError<'src> { List::or_ticked(recipes), )?; if let Some(suggestion) = *suggestion { - write!(f, "\nDid you mean `{}`?", suggestion)?; + write!(f, "\n{}", suggestion)?; } }, UnknownOverrides { overrides } => { diff --git a/src/suggestion.rs b/src/suggestion.rs new file mode 100644 index 0000000..cb5d9d6 --- /dev/null +++ b/src/suggestion.rs @@ -0,0 +1,17 @@ +use crate::common::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct Suggestion<'src> { + pub(crate) name: &'src str, + pub(crate) target: Option<&'src str>, +} + +impl<'src> Display for Suggestion<'src> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Did you mean `{}`", self.name)?; + if let Some(target) = self.target { + write!(f, ", an alias for `{}`", target)?; + } + write!(f, "?") + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 6ed38b8..57bca01 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1224,6 +1224,22 @@ a Z="\t z": status: EXIT_FAILURE, } +test! { + name: show_alias_suggestion, + justfile: r#" +hello a b='B ' c='C': + echo {{a}} {{b}} {{c}} + +alias foo := hello + +a Z="\t z": +"#, + args: ("--show", "fo"), + stdout: "", + stderr: "Justfile does not contain recipe `fo`.\nDid you mean `foo`, an alias for `hello`?\n", + status: EXIT_FAILURE, +} + test! { name: show_no_suggestion, justfile: r#" @@ -1238,6 +1254,22 @@ a Z="\t z": status: EXIT_FAILURE, } +test! { + name: show_no_alias_suggestion, + justfile: r#" +hello a b='B ' c='C': + echo {{a}} {{b}} {{c}} + +alias foo := hello + +a Z="\t z": +"#, + args: ("--show", "fooooooo"), + stdout: "", + stderr: "Justfile does not contain recipe `fooooooo`.\n", + status: EXIT_FAILURE, +} + test! { name: run_suggestion, justfile: r#"