Repository: casey/just
Pull request #624 (2020-04-19 19:52:36)

Show aliases in suggestions
https://github.com/casey/just/issues/445

casey: Thanks for the PR! Added some comments inline.
Celeo: Heya,

Anything else I can do for this?
casey: Just an integration test! 

> A couple of very minor comments. Also, this could use an integration test that checks the output is correct.
> 
> If you look in `tests/integration.rs`, you can find a test called `show_suggestion`, which you can make a copy of that adds an alias and checks that it suggests the alias.

This will mostly be copy pasta from the existing `show_suggestion` test, but should check that the actual text output when an alias is suggested is correct.
Celeo: Sure thing! I remember seeing that request earlier, but must've hidden it or something.

Patch:
diff --git a/src/common.rs b/src/common.rs
index 7dee2b41..1ada469f 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 8e8000af..52df3958 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 e4c9cb32..a027db40 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<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
diff --git a/src/lib.rs b/src/lib.rs
index 24d17e56..398ba509 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 70d73c3b..511ae455 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<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 } => {
diff --git a/src/suggestion.rs b/src/suggestion.rs
new file mode 100644
index 00000000..cb5d9d68
--- /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, "?")
+  }
+}


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index 6ed38b86..57bca015 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#"