Suggest aliases for unknown recipes (#624)
This commit is contained in:
parent
875fb41e40
commit
dc7210bca3
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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
17
src/suggestion.rs
Normal 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, "?")
|
||||||
|
}
|
||||||
|
}
|
@ -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#"
|
||||||
|
Loading…
Reference in New Issue
Block a user