Skip to content

Commit

Permalink
Suggest aliases for unknown recipes (#624)
Browse files Browse the repository at this point in the history
  • Loading branch information
Celeo authored Apr 26, 2020
1 parent 875fb41 commit dc7210b
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
54 changes: 44 additions & 10 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -111,6 +113,7 @@ mod shebang;
mod show_whitespace;
mod string_literal;
mod subcommand;
mod suggestion;
mod table;
mod thunk;
mod token;
Expand Down
4 changes: 2 additions & 2 deletions src/runtime_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 } => {
Expand Down
17 changes: 17 additions & 0 deletions src/suggestion.rs
Original file line number Diff line number Diff line change
@@ -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, "?")
}
}
32 changes: 32 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand All @@ -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#"
Expand Down

0 comments on commit dc7210b

Please sign in to comment.