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,
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,
};

View File

@ -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)
}

View File

@ -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<Suggestion> {
let mut suggestions = self
.recipes
.keys()
.map(|suggestion| (edit_distance(suggestion, name), suggestion))
.collect::<Vec<_>>();
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::<Vec<(usize, Suggestion)>>();
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

View File

@ -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;

View File

@ -56,7 +56,7 @@ pub(crate) enum RuntimeError<'src> {
},
UnknownRecipes {
recipes: Vec<&'src str>,
suggestion: Option<&'src str>,
suggestion: Option<Suggestion<'src>>,
},
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 } => {

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,
}
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#"