Repository: casey/just
Pull request #821 (2021-05-07 05:40:52)

Fix bang lexing
Fix bug where `!` followed by EoF would produce an internal error.
Instead return `CompilationError::UnexpectedEndOfToken`.


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 9450201b..777ed641 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -75,7 +75,7 @@ jobs:
       run: cargo test --all --verbose
 
     - name: Clippy
-      run: cargo clippy --all
+      run: cargo clippy --all --all-targets --all-features
 
     - name: Lint
       if: matrix.os != 'windows-2016'
diff --git a/justfile b/justfile
index ca43cec5..6176c054 100755
--- a/justfile
+++ b/justfile
@@ -113,7 +113,7 @@ install-dev-deps-homebrew:
 
 # everyone's favorite animate paper clip
 clippy:
-	cargo clippy
+	cargo clippy --all --all-targets --all-features
 
 # count non-empty lines of code
 sloc:
diff --git a/src/compilation_error.rs b/src/compilation_error.rs
index 62ac6537..56d23b12 100644
--- a/src/compilation_error.rs
+++ b/src/compilation_error.rs
@@ -223,6 +223,9 @@ impl Display for CompilationError<'_> {
       UnknownStartOfToken => {
         writeln!(f, "Unknown start of token:")?;
       },
+      UnexpectedEndOfToken { expected } => {
+        writeln!(f, "Expected character `{}` but found end-of-file", expected)?;
+      },
       MismatchedClosingDelimiter {
         open,
         open_line,
diff --git a/src/compilation_error_kind.rs b/src/compilation_error_kind.rs
index 86fc08b6..b15c858d 100644
--- a/src/compilation_error_kind.rs
+++ b/src/compilation_error_kind.rs
@@ -95,6 +95,9 @@ pub(crate) enum CompilationErrorKind<'src> {
   UnexpectedCharacter {
     expected: char,
   },
+  UnexpectedEndOfToken {
+    expected: char,
+  },
   UnknownSetting {
     setting: &'src str,
   },
diff --git a/src/config.rs b/src/config.rs
index f3e48355..10ed7e92 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -340,7 +340,7 @@ impl Config {
     let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS));
 
     for (name, value) in positional.overrides {
-      overrides.insert(name.to_owned(), value.to_owned());
+      overrides.insert(name.clone(), value.clone());
     }
 
     let search_config = {
@@ -798,7 +798,7 @@ impl Config {
     arguments: &[String],
   ) -> Result<(), i32> {
     if let Err(error) = InterruptHandler::install(self.verbosity) {
-      warn!("Failed to set CTRL-C handler: {}", error)
+      warn!("Failed to set CTRL-C handler: {}", error);
     }
 
     let result = justfile.run(&self, search, overrides, arguments);
@@ -851,7 +851,7 @@ impl Config {
       if i > 0 {
         print!(" ");
       }
-      print!("{}", assignment.name)
+      print!("{}", assignment.name);
     }
     println!();
   }
@@ -958,7 +958,7 @@ ARGS:
           $(dry_run: $dry_run,)?
           $(highlight: $highlight,)?
           $(search_config: $search_config,)?
-          $(shell: $shell.to_string(),)?
+          $(shell: $shell.to_owned(),)?
           $(shell_args: $shell_args,)?
           $(shell_present: $shell_present,)?
           $(subcommand: $subcommand,)?
@@ -1411,7 +1411,7 @@ ARGS:
     name: arguments_leading_equals,
     args: ["=foo"],
     subcommand: Subcommand::Run {
-      arguments: vec!["=foo".to_string()],
+      arguments: vec!["=foo".to_owned()],
       overrides: map!{},
     },
   }
diff --git a/src/enclosure.rs b/src/enclosure.rs
index fb0c7d4f..2e90332b 100644
--- a/src/enclosure.rs
+++ b/src/enclosure.rs
@@ -29,6 +29,6 @@ mod tests {
 
   #[test]
   fn tick() {
-    assert_eq!(Enclosure::tick("foo").to_string(), "`foo`")
+    assert_eq!(Enclosure::tick("foo").to_string(), "`foo`");
   }
 }
diff --git a/src/justfile.rs b/src/justfile.rs
index 8d1ac4aa..ed20f3fc 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -15,7 +15,7 @@ impl<'src> Justfile<'src> {
     for recipe in self.recipes.values() {
       if let Some(first_recipe) = first {
         if recipe.line_number() < first_recipe.line_number() {
-          first = Some(recipe)
+          first = Some(recipe);
         }
       } else {
         first = Some(recipe);
@@ -134,7 +134,7 @@ impl<'src> Justfile<'src> {
         } else {
           return Err(RuntimeError::EvalUnknownVariable {
             suggestion: self.suggest_variable(&variable),
-            variable:   variable.to_owned(),
+            variable:   variable.clone(),
           });
         }
       } else {
@@ -224,7 +224,7 @@ impl<'src> Justfile<'src> {
 
     let mut ran = BTreeSet::new();
     for (recipe, arguments) in grouped {
-      self.run_recipe(&context, recipe, arguments, &dotenv, &search, &mut ran)?
+      self.run_recipe(&context, recipe, arguments, &dotenv, &search, &mut ran)?;
     }
 
     Ok(())
@@ -235,13 +235,11 @@ impl<'src> Justfile<'src> {
   }
 
   pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
-    if let Some(recipe) = self.recipes.get(name) {
-      Some(recipe)
-    } else if let Some(alias) = self.aliases.get(name) {
-      Some(alias.target.as_ref())
-    } else {
-      None
-    }
+    self
+      .recipes
+      .get(name)
+      .map(Rc::as_ref)
+      .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
   }
 
   fn run_recipe<'run>(
@@ -599,9 +597,9 @@ mod tests {
     "#,
     args: ["--quiet", "wut"],
     error: Code {
-      code: _,
       line_number,
       recipe,
+      ..
     },
     check: {
       assert_eq!(recipe, "wut");
diff --git a/src/lexer.rs b/src/lexer.rs
index e87166c4..d6a4de18 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -162,9 +162,14 @@ impl<'src> Lexer<'src> {
     self.next_is('\n') || self.rest_starts_with("\r\n")
   }
 
+  /// Are we at end-of-file?
+  fn at_eof(&self) -> bool {
+    self.rest().is_empty()
+  }
+
   /// Are we at end-of-line or end-of-file?
   fn at_eol_or_eof(&self) -> bool {
-    self.at_eol() || self.rest().is_empty()
+    self.at_eol() || self.at_eof()
   }
 
   /// Get current indentation
@@ -293,11 +298,11 @@ impl<'src> Lexer<'src> {
       match self.next {
         Some(first) => {
           if let Some(&interpolation_start) = self.interpolation_stack.last() {
-            self.lex_interpolation(interpolation_start, first)?
+            self.lex_interpolation(interpolation_start, first)?;
           } else if self.recipe_body {
-            self.lex_body()?
+            self.lex_body()?;
           } else {
-            self.lex_normal(first)?
+            self.lex_normal(first)?;
           };
         },
         None => break,
@@ -558,7 +563,7 @@ impl<'src> Lexer<'src> {
         break Interpolation;
       }
 
-      if self.rest().is_empty() {
+      if self.at_eof() {
         break EndOfFile;
       }
 
@@ -684,6 +689,11 @@ impl<'src> Lexer<'src> {
     } else {
       // Emit an unspecified token to consume the current character,
       self.token(Unspecified);
+
+      if self.at_eof() {
+        return Err(self.error(UnexpectedEndOfToken { expected: '=' }));
+      }
+
       // …and advance past another character,
       self.advance()?;
       // …so that the error we produce highlights the unexpected character.
@@ -759,7 +769,7 @@ impl<'src> Lexer<'src> {
   /// Lex whitespace: [ \t]+
   fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
     while self.next_is_whitespace() {
-      self.advance()?
+      self.advance()?;
     }
 
     self.token(Whitespace);
@@ -870,10 +880,7 @@ mod tests {
       .map(|token| token.kind)
       .collect::<Vec<TokenKind>>();
 
-    let have_lexemes = have
-      .iter()
-      .map(|token| token.lexeme())
-      .collect::<Vec<&str>>();
+    let have_lexemes = have.iter().map(Token::lexeme).collect::<Vec<&str>>();
 
     assert_eq!(have_kinds, want_kinds, "Token kind mismatch");
     assert_eq!(have_lexemes, want_lexemes, "Token lexeme mismatch");
@@ -2226,6 +2233,30 @@ mod tests {
     },
   }
 
+  error! {
+    name:   bang_eof,
+    input:  "!",
+    offset: 1,
+    line:   0,
+    column: 1,
+    width:  0,
+    kind:   UnexpectedEndOfToken {
+      expected: '=',
+    },
+  }
+
+  error! {
+    name:   bang_unexpected,
+    input:  "!%",
+    offset: 1,
+    line:   0,
+    column: 1,
+    width:  1,
+    kind:   UnexpectedCharacter {
+      expected: '=',
+    },
+  }
+
   #[test]
   fn presume_error() {
     assert_matches!(
diff --git a/src/lib.rs b/src/lib.rs
index bec5d566..332b40eb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,8 +2,12 @@
 #![allow(
   clippy::blanket_clippy_restriction_lints,
   clippy::comparison_chain,
+  clippy::create_dir,
+  clippy::default_numeric_fallback,
   clippy::else_if_without_else,
   clippy::enum_glob_use,
+  clippy::exhaustive_enums,
+  clippy::exhaustive_structs,
   clippy::expect_used,
   clippy::filter_map,
   clippy::if_not_else,
diff --git a/src/node.rs b/src/node.rs
index b0413ad7..19be563f 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -62,9 +62,9 @@ impl<'src> Node<'src> for Expression<'src> {
         let mut tree = Tree::atom(Keyword::If.lexeme());
         tree.push_mut(lhs.tree());
         if *inverted {
-          tree.push_mut("!=")
+          tree.push_mut("!=");
         } else {
-          tree.push_mut("==")
+          tree.push_mut("==");
         }
         tree.push_mut(rhs.tree());
         tree.push_mut(then.tree());
@@ -72,9 +72,10 @@ impl<'src> Node<'src> for Expression<'src> {
         tree
       },
       Expression::Call { thunk } => {
+        use Thunk::*;
+
         let mut tree = Tree::atom("call");
 
-        use Thunk::*;
         match thunk {
           Nullary { name, .. } => tree.push_mut(name.lexeme()),
           Unary { name, arg, .. } => {
@@ -157,8 +158,7 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
 
 impl<'src> Node<'src> for Parameter<'src> {
   fn tree(&self) -> Tree<'src> {
-    let mut children = Vec::new();
-    children.push(Tree::atom(self.name.lexeme()));
+    let mut children = vec![Tree::atom(self.name.lexeme())];
 
     if let Some(default) = &self.default {
       children.push(default.tree());
@@ -185,11 +185,11 @@ impl<'src> Node<'src> for Fragment<'src> {
 
 impl<'src> Node<'src> for Set<'src> {
   fn tree(&self) -> Tree<'src> {
-    let mut set = Tree::atom(Keyword::Set.lexeme());
+    use Setting::*;
 
+    let mut set = Tree::atom(Keyword::Set.lexeme());
     set.push_mut(self.name.lexeme().replace('-', "_"));
 
-    use Setting::*;
     match &self.value {
       DotenvLoad(value) | Export(value) | PositionalArguments(value) =>
         set.push_mut(value.to_string()),
diff --git a/src/parser.rs b/src/parser.rs
index 37bcf806..6f1286be 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -387,8 +387,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
     let value = self.parse_expression()?;
     self.expect_eol()?;
     Ok(Assignment {
-      name,
       export,
+      name,
       value,
     })
   }
@@ -530,7 +530,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       unindented
     };
 
-    Ok(StringLiteral { cooked, raw, kind })
+    Ok(StringLiteral { kind, raw, cooked })
   }
 
   /// Parse a name from an identifier token
diff --git a/src/positional.rs b/src/positional.rs
index 04923d1c..3e3396b4 100644
--- a/src/positional.rs
+++ b/src/positional.rs
@@ -116,8 +116,8 @@ mod tests {
               .cloned()
               .map(|(key, value): (&str, &str)| (key.to_owned(), value.to_owned()))
               .collect(),
-            search_directory: $search_directory.map(|dir: &str| dir.to_owned()),
-            arguments: $arguments.iter().cloned().map(|arg: &str| arg.to_owned()).collect(),
+            search_directory: $search_directory.map(str::to_owned),
+            arguments: $arguments.iter().cloned().map(str::to_owned).collect(),
           },
         )
       }
diff --git a/src/recipe.rs b/src/recipe.rs
index f7ae982b..9f35855d 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -128,7 +128,7 @@ impl<'src, D> Recipe<'src, D> {
         // add blank lines so that lines in the generated script have the same line
         // number as the corresponding lines in the justfile
         for _ in 1..(self.line_number() + 2) {
-          text += "\n"
+          text += "\n";
         }
         for line in &evaluated_lines[1..] {
           text += line;
diff --git a/src/scope.rs b/src/scope.rs
index d9bee226..5785d5da 100644
--- a/src/scope.rs
+++ b/src/scope.rs
@@ -23,8 +23,8 @@ impl<'src, 'run> Scope<'src, 'run> {
 
   pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, value: String) {
     self.bindings.insert(Binding {
-      name,
       export,
+      name,
       value,
     });
   }
diff --git a/src/search.rs b/src/search.rs
index e96e7780..6671b477 100644
--- a/src/search.rs
+++ b/src/search.rs
@@ -203,9 +203,7 @@ mod tests {
   fn not_found() {
     let tmp = testing::tempdir();
     match Search::justfile(tmp.path()) {
-      Err(SearchError::NotFound) => {
-        assert!(true);
-      },
+      Err(SearchError::NotFound) => {},
       _ => panic!("No justfile found error was expected"),
     }
   }
@@ -218,16 +216,14 @@ mod tests {
     fs::write(&path, "default:\n\techo ok").unwrap();
     path.pop();
     path.push(FILENAME.to_uppercase());
-    if let Ok(_) = fs::File::open(path.as_path()) {
+    if fs::File::open(path.as_path()).is_ok() {
       // We are in case-insensitive file system
       return;
     }
     fs::write(&path, "default:\n\techo ok").unwrap();
     path.pop();
     match Search::justfile(path.as_path()) {
-      Err(SearchError::MultipleCandidates { .. }) => {
-        assert!(true);
-      },
+      Err(SearchError::MultipleCandidates { .. }) => {},
       _ => panic!("Multiple candidates error was expected"),
     }
   }
@@ -239,11 +235,8 @@ mod tests {
     path.push(FILENAME);
     fs::write(&path, "default:\n\techo ok").unwrap();
     path.pop();
-    match Search::justfile(path.as_path()) {
-      Ok(_path) => {
-        assert!(true);
-      },
-      _ => panic!("No errors were expected"),
+    if let Err(err) = Search::justfile(path.as_path()) {
+      panic!("No errors were expected: {}", err);
     }
   }
 
@@ -265,11 +258,8 @@ mod tests {
     path.push(spongebob_case);
     fs::write(&path, "default:\n\techo ok").unwrap();
     path.pop();
-    match Search::justfile(path.as_path()) {
-      Ok(_path) => {
-        assert!(true);
-      },
-      _ => panic!("No errors were expected"),
+    if let Err(err) = Search::justfile(path.as_path()) {
+      panic!("No errors were expected: {}", err);
     }
   }
 
@@ -284,11 +274,8 @@ mod tests {
     fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
     path.push("b");
     fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
-    match Search::justfile(path.as_path()) {
-      Ok(_path) => {
-        assert!(true);
-      },
-      _ => panic!("No errors were expected"),
+    if let Err(err) = Search::justfile(path.as_path()) {
+      panic!("No errors were expected: {}", err);
     }
   }
 
@@ -312,7 +299,7 @@ mod tests {
         path.push(FILENAME);
         assert_eq!(found_path, path);
       },
-      _ => panic!("No errors were expected"),
+      Err(err) => panic!("No errors were expected: {}", err),
     }
   }
 
diff --git a/src/search_error.rs b/src/search_error.rs
index dbbdc947..c058f23a 100644
--- a/src/search_error.rs
+++ b/src/search_error.rs
@@ -46,6 +46,6 @@ mod tests {
     assert_eq!(
       error.to_string(),
       "Multiple candidate justfiles found in `/foo`: `justfile` and `JUSTFILE`"
-    )
+    );
   }
 }
diff --git a/src/string_kind.rs b/src/string_kind.rs
index 45e5988f..2b1cca2b 100644
--- a/src/string_kind.rs
+++ b/src/string_kind.rs
@@ -2,8 +2,8 @@ use crate::common::*;
 
 #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
 pub(crate) struct StringKind {
-  indented:  bool,
   delimiter: StringDelimiter,
+  indented:  bool,
 }
 
 #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
diff --git a/src/summary.rs b/src/summary.rs
index ac372a22..9e4c7608 100644
--- a/src/summary.rs
+++ b/src/summary.rs
@@ -56,7 +56,7 @@ impl Summary {
         .into_iter()
         .map(|(name, recipe)| {
           (
-            name.to_string(),
+            name.to_owned(),
             Recipe::new(&recipe, aliases.remove(name).unwrap_or_default()),
           )
         })
@@ -64,7 +64,7 @@ impl Summary {
       assignments: justfile
         .assignments
         .iter()
-        .map(|(name, assignment)| (name.to_string(), Assignment::new(assignment)))
+        .map(|(name, assignment)| ((*name).to_owned(), Assignment::new(assignment)))
         .collect(),
     }
   }
@@ -213,7 +213,7 @@ impl Expression {
     use full::Expression::*;
     match expression {
       Backtick { contents, .. } => Expression::Backtick {
-        command: (*contents).to_owned(),
+        command: (*contents).clone(),
       },
       Call { thunk } => match thunk {
         full::Thunk::Nullary { name, .. } => Expression::Call {
@@ -249,7 +249,7 @@ impl Expression {
         inverted:  *inverted,
       },
       StringLiteral { string_literal } => Expression::String {
-        text: string_literal.cooked.to_string(),
+        text: string_literal.cooked.clone(),
       },
       Variable { name, .. } => Expression::Variable {
         name: name.lexeme().to_owned(),
diff --git a/src/token.rs b/src/token.rs
index a0ef4deb..47a7b3d2 100644
--- a/src/token.rs
+++ b/src/token.rs
@@ -70,7 +70,7 @@ impl<'src> Token<'src> {
             f,
             "internal error: Error has invalid line number: {}",
             line_number
-          )?
+          )?;
         },
     }
     Ok(())


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index 2af210ae..1ac4c45a 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -25,8 +25,8 @@ pub(crate) fn search(config: &Config) -> Search {
   let justfile = working_directory.join(crate::search::FILENAME);
 
   Search {
-    working_directory,
     justfile,
+    working_directory,
   }
 }