Suggest aliases for unknown recipes (#624)

This commit is contained in:
Matt Boulanger 2020-04-26 14:19:21 -07:00 committed by GitHub
parent 875fb41e40
commit dc7210bca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 102 additions and 16 deletions

View File

@ -61,8 +61,8 @@ pub(crate) use crate::{
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token, string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning, verbosity::Verbosity, warning::Warning,
}; };

View File

@ -632,7 +632,7 @@ impl Config {
} else { } else {
eprintln!("Justfile does not contain recipe `{}`.", name); eprintln!("Justfile does not contain recipe `{}`.", name);
if let Some(suggestion) = justfile.suggest(name) { if let Some(suggestion) = justfile.suggest(name) {
eprintln!("Did you mean `{}`?", suggestion); eprintln!("{}", suggestion);
} }
Err(EXIT_FAILURE) Err(EXIT_FAILURE)
} }

View File

@ -28,19 +28,30 @@ impl<'src> Justfile<'src> {
self.recipes.len() self.recipes.len()
} }
pub(crate) fn suggest(&self, name: &str) -> Option<&'src str> { pub(crate) fn suggest(&self, input: &str) -> Option<Suggestion> {
let mut suggestions = self let mut suggestions = self
.recipes .recipes
.keys() .keys()
.map(|suggestion| (edit_distance(suggestion, name), suggestion)) .map(|name| {
.collect::<Vec<_>>(); (edit_distance(name, input), Suggestion {
suggestions.sort(); name,
if let Some(&(distance, suggestion)) = suggestions.first() { target: None,
if distance < 3 { })
return Some(suggestion); })
} .chain(self.aliases.iter().map(|(name, alias)| {
} (edit_distance(name, input), Suggestion {
None name,
target: Some(alias.target.name.lexeme()),
})
}))
.filter(|(distance, _suggestion)| distance < &3)
.collect::<Vec<(usize, Suggestion)>>();
suggestions.sort_by_key(|(distance, _suggestion)| *distance);
suggestions
.into_iter()
.map(|(_distance, suggestion)| suggestion)
.next()
} }
pub(crate) fn run<'run>( 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 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 // 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 // a variable and continuing even though a command fails, whereas in plain

View File

@ -23,10 +23,12 @@
clippy::result_expect_used, clippy::result_expect_used,
clippy::shadow_unrelated, clippy::shadow_unrelated,
clippy::string_add, clippy::string_add,
clippy::struct_excessive_bools,
clippy::too_many_lines, clippy::too_many_lines,
clippy::unreachable, clippy::unreachable,
clippy::use_debug, clippy::use_debug,
clippy::wildcard_enum_match_arm clippy::wildcard_enum_match_arm,
clippy::wildcard_imports
)] )]
#[macro_use] #[macro_use]
@ -111,6 +113,7 @@ mod shebang;
mod show_whitespace; mod show_whitespace;
mod string_literal; mod string_literal;
mod subcommand; mod subcommand;
mod suggestion;
mod table; mod table;
mod thunk; mod thunk;
mod token; mod token;

View File

@ -56,7 +56,7 @@ pub(crate) enum RuntimeError<'src> {
}, },
UnknownRecipes { UnknownRecipes {
recipes: Vec<&'src str>, recipes: Vec<&'src str>,
suggestion: Option<&'src str>, suggestion: Option<Suggestion<'src>>,
}, },
Unknown { Unknown {
recipe: &'src str, recipe: &'src str,
@ -117,7 +117,7 @@ impl<'src> Display for RuntimeError<'src> {
List::or_ticked(recipes), List::or_ticked(recipes),
)?; )?;
if let Some(suggestion) = *suggestion { if let Some(suggestion) = *suggestion {
write!(f, "\nDid you mean `{}`?", suggestion)?; write!(f, "\n{}", suggestion)?;
} }
}, },
UnknownOverrides { overrides } => { UnknownOverrides { overrides } => {

17
src/suggestion.rs Normal file
View File

@ -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, "?")
}
}

View File

@ -1224,6 +1224,22 @@ a Z="\t z":
status: EXIT_FAILURE, 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! { test! {
name: show_no_suggestion, name: show_no_suggestion,
justfile: r#" justfile: r#"
@ -1238,6 +1254,22 @@ a Z="\t z":
status: EXIT_FAILURE, 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! { test! {
name: run_suggestion, name: run_suggestion,
justfile: r#" justfile: r#"