Repository: casey/just
Pull request #820 (2021-05-07 05:29:58)

Add subsequents


Patch:
diff --git a/src/justfile.rs b/src/justfile.rs
index 08a8bd2f..709a29ec 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -302,7 +302,7 @@ impl<'src> Justfile<'src> {
     let mut evaluator =
       Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
 
-    for Dependency { recipe, arguments } in &recipe.dependencies {
+    for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
       let mut invocation = vec![recipe.name().to_owned()];
 
       for argument in arguments {
@@ -321,6 +321,27 @@ impl<'src> Justfile<'src> {
 
     recipe.run(context, dotenv, scope.child(), search, &positional)?;
 
+    {
+      let mut ran = BTreeSet::new();
+
+      for Dependency { recipe, arguments } in recipe.dependencies.iter().skip(recipe.priors) {
+        let mut evaluated = Vec::new();
+
+        for argument in arguments {
+          evaluated.push(evaluator.evaluate_expression(argument)?);
+        }
+
+        self.run_recipe(
+          context,
+          recipe,
+          &evaluated.iter().map(String::as_ref).collect::<Vec<&str>>(),
+          dotenv,
+          search,
+          &mut ran,
+        )?;
+      }
+    }
+
     let mut invocation = vec![recipe.name().to_owned()];
     for argument in arguments.iter().cloned() {
       invocation.push(argument.to_owned());
diff --git a/src/lexer.rs b/src/lexer.rs
index d6a4de18..04527d38 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -479,7 +479,8 @@ impl<'src> Lexer<'src> {
   /// Lex token beginning with `start` outside of a recipe body
   fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
     match start {
-      '!' => self.lex_bang(),
+      '&' => self.lex_digraph('&', '&', AmpersandAmpersand),
+      '!' => self.lex_digraph('!', '=', BangEquals),
       '*' => self.lex_single(Asterisk),
       '$' => self.lex_single(Dollar),
       '@' => self.lex_single(At),
@@ -679,25 +680,30 @@ impl<'src> Lexer<'src> {
     !self.open_delimiters.is_empty()
   }
 
-  /// Lex a token starting with '!'
-  fn lex_bang(&mut self) -> CompilationResult<'src, ()> {
-    self.presume('!')?;
+  /// Lex a two-character digraph
+  fn lex_digraph(
+    &mut self,
+    left: char,
+    right: char,
+    token: TokenKind,
+  ) -> CompilationResult<'src, ()> {
+    self.presume(left)?;
 
-    if self.accepted('=')? {
-      self.token(BangEquals);
+    if self.accepted(right)? {
+      self.token(token);
       Ok(())
     } else {
       // Emit an unspecified token to consume the current character,
       self.token(Unspecified);
 
       if self.at_eof() {
-        return Err(self.error(UnexpectedEndOfToken { expected: '=' }));
+        return Err(self.error(UnexpectedEndOfToken { expected: right }));
       }
 
       // …and advance past another character,
       self.advance()?;
       // …so that the error we produce highlights the unexpected character.
-      Err(self.error(UnexpectedCharacter { expected: '=' }))
+      Err(self.error(UnexpectedCharacter { expected: right }))
     }
   }
 
@@ -919,6 +925,7 @@ mod tests {
   fn default_lexeme(kind: TokenKind) -> &'static str {
     match kind {
       // Fixed lexemes
+      AmpersandAmpersand => "&&",
       Asterisk => "*",
       At => "@",
       BangEquals => "!=",
@@ -1048,6 +1055,12 @@ mod tests {
     tokens: (StringToken:"\"\"\"hello\ngoodbye\"\"\""),
   }
 
+  test! {
+    name:   ampersand_ampersand,
+    text:   "&&",
+    tokens: (AmpersandAmpersand),
+  }
+
   test! {
     name:   equals,
     text:   "=",
@@ -2109,16 +2122,6 @@ mod tests {
     kind:   UnpairedCarriageReturn,
   }
 
-  error! {
-    name:   unknown_start_of_token_ampersand,
-    input:  " \r\n&",
-    offset: 3,
-    line:   1,
-    column: 0,
-    width:  1,
-    kind:   UnknownStartOfToken,
-  }
-
   error! {
     name:   unknown_start_of_token_tilde,
     input:  "~",
@@ -2257,6 +2260,29 @@ mod tests {
     },
   }
 
+  error! {
+    name:   ampersand_eof,
+    input:  "&",
+    offset: 1,
+    line:   0,
+    column: 1,
+    width:  0,
+    kind:   UnexpectedEndOfToken {
+      expected: '&',
+    },
+  }
+  error! {
+    name:   ampersand_unexpected,
+    input:  "&%",
+    offset: 1,
+    line:   0,
+    column: 1,
+    width:  1,
+    kind:   UnexpectedCharacter {
+      expected: '&',
+    },
+  }
+
   #[test]
   fn presume_error() {
     assert_matches!(
diff --git a/src/node.rs b/src/node.rs
index ee666670..897da9a6 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -145,18 +145,29 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
 
     if !self.dependencies.is_empty() {
       let mut dependencies = Tree::atom("deps");
+      let mut subsequents = Tree::atom("sups");
 
-      for dependency in &self.dependencies {
+      for (i, dependency) in self.dependencies.iter().enumerate() {
         let mut d = Tree::atom(dependency.recipe.lexeme());
 
         for argument in &dependency.arguments {
           d.push_mut(argument.tree());
         }
 
-        dependencies.push_mut(d);
+        if i < self.priors {
+          dependencies.push_mut(d);
+        } else {
+          subsequents.push_mut(d);
+        }
       }
 
-      t.push_mut(dependencies);
+      if let Tree::List(_) = dependencies {
+        t.push_mut(dependencies);
+      }
+
+      if let Tree::List(_) = subsequents {
+        t.push_mut(subsequents);
+      }
     }
 
     if !self.body.is_empty() {
diff --git a/src/parser.rs b/src/parser.rs
index 0453177b..66d62cc2 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -627,19 +627,36 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       dependencies.push(dependency);
     }
 
+    let priors = dependencies.len();
+
+    if self.accepted(AmpersandAmpersand)? {
+      let mut subsequents = Vec::new();
+
+      while let Some(subsequent) = self.accept_dependency()? {
+        subsequents.push(subsequent);
+      }
+
+      if subsequents.is_empty() {
+        return Err(self.unexpected_token()?);
+      }
+
+      dependencies.append(&mut subsequents);
+    }
+
     self.expect_eol()?;
 
     let body = self.parse_body()?;
 
     Ok(Recipe {
+      parameters: positional.into_iter().chain(variadic).collect(),
       private: name.lexeme().starts_with('_'),
       shebang: body.first().map(Line::is_shebang).unwrap_or(false),
-      parameters: positional.into_iter().chain(variadic).collect(),
+      priors,
+      body,
+      dependencies,
       doc,
       name,
       quiet,
-      dependencies,
-      body,
     })
   }
 
@@ -1102,6 +1119,12 @@ mod tests {
     tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))),
   }
 
+  test! {
+    name: recipe_subsequent,
+    text: "foo: && bar",
+    tree: (justfile (recipe foo (sups bar))),
+  }
+
   test! {
     name: recipe_line_single,
     text: "foo:\n bar",
@@ -1885,10 +1908,13 @@ mod tests {
     name:   missing_eol,
     input:  "a b c: z =",
     offset:  9,
-    line:   0,
-    column: 9,
-    width:  1,
-    kind:   UnexpectedToken{expected: vec![Comment, Eof, Eol, Identifier, ParenL], found: Equals},
+    line:    0,
+    column:  9,
+    width:   1,
+    kind:    UnexpectedToken{
+      expected: vec![AmpersandAmpersand, Comment, Eof, Eol, Identifier, ParenL],
+      found: Equals
+    },
   }
 
   error! {
diff --git a/src/recipe.rs b/src/recipe.rs
index c7b2d71b..bc3832a8 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -25,14 +25,15 @@ fn error_from_signal(
 /// A recipe, e.g. `foo: bar baz`
 #[derive(PartialEq, Debug, Clone)]
 pub(crate) struct Recipe<'src, D = Dependency<'src>> {
+  pub(crate) body:         Vec<Line<'src>>,
   pub(crate) dependencies: Vec<D>,
   pub(crate) doc:          Option<&'src str>,
-  pub(crate) body:         Vec<Line<'src>>,
   pub(crate) name:         Name<'src>,
   pub(crate) parameters:   Vec<Parameter<'src>>,
   pub(crate) private:      bool,
   pub(crate) quiet:        bool,
   pub(crate) shebang:      bool,
+  pub(crate) priors:       usize,
 }
 
 impl<'src, D> Recipe<'src, D> {
@@ -330,7 +331,12 @@ impl<'src, D: Display> Display for Recipe<'src, D> {
       write!(f, " {}", parameter)?;
     }
     write!(f, ":")?;
-    for dependency in &self.dependencies {
+
+    for (i, dependency) in self.dependencies.iter().enumerate() {
+      if i == self.priors {
+        write!(f, " &&")?;
+      }
+
       write!(f, " {}", dependency)?;
     }
 
diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs
index 27e134c5..0c5e6cb9 100644
--- a/src/recipe_resolver.rs
+++ b/src/recipe_resolver.rs
@@ -113,9 +113,10 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
       }
     }
 
+    stack.pop();
+
     let resolved = Rc::new(recipe.resolve(dependencies)?);
     self.resolved_recipes.insert(Rc::clone(&resolved));
-    stack.pop();
     Ok(resolved)
   }
 }
diff --git a/src/token_kind.rs b/src/token_kind.rs
index a437762d..d76c8f19 100644
--- a/src/token_kind.rs
+++ b/src/token_kind.rs
@@ -2,6 +2,7 @@ use crate::common::*;
 
 #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
 pub(crate) enum TokenKind {
+  AmpersandAmpersand,
   Asterisk,
   At,
   Backtick,
@@ -37,6 +38,7 @@ impl Display for TokenKind {
   fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
     use TokenKind::*;
     write!(f, "{}", match *self {
+      AmpersandAmpersand => "'&&'",
       Asterisk => "'*'",
       At => "'@'",
       Backtick => "backtick",
diff --git a/src/tree.rs b/src/tree.rs
index fd138226..cc45f2fc 100644
--- a/src/tree.rs
+++ b/src/tree.rs
@@ -42,6 +42,12 @@ macro_rules! tree {
     $crate::tree::Tree::atom("*")
   };
 
+  {
+    &&
+  } => {
+    $crate::tree::Tree::atom("&&")
+  };
+
   {
     ==
   } => {
diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs
index 575c77aa..125eb02e 100644
--- a/src/unresolved_recipe.rs
+++ b/src/unresolved_recipe.rs
@@ -7,7 +7,14 @@ impl<'src> UnresolvedRecipe<'src> {
     self,
     resolved: Vec<Rc<Recipe<'src>>>,
   ) -> CompilationResult<'src, Recipe<'src>> {
-    assert_eq!(self.dependencies.len(), resolved.len());
+    assert_eq!(
+      self.dependencies.len(),
+      resolved.len(),
+      "UnresolvedRecipe::resolve: dependency count not equal to resolved count: {} != {}",
+      self.dependencies.len(),
+      resolved.len()
+    );
+
     for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) {
       assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme());
       if !resolved
@@ -36,13 +43,14 @@ impl<'src> UnresolvedRecipe<'src> {
       .collect();
 
     Ok(Recipe {
-      doc: self.doc,
       body: self.body,
+      doc: self.doc,
       name: self.name,
       parameters: self.parameters,
       private: self.private,
       quiet: self.quiet,
       shebang: self.shebang,
+      priors: self.priors,
       dependencies,
     })
   }


Test patch:
diff --git a/tests/fmt.rs b/tests/fmt.rs
index a0511176..392bca2e 100644
--- a/tests/fmt.rs
+++ b/tests/fmt.rs
@@ -944,3 +944,18 @@ test! {
         echo foo
   ",
 }
+
+test! {
+  name: subsequent,
+  justfile: "
+    bar:
+    foo: && bar
+        echo foo",
+  args: ("--dump"),
+  stdout: "
+    bar:
+
+    foo: && bar
+        echo foo
+  ",
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index a9e60db3..46aacfbb 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -3,11 +3,9 @@ mod test;
 
 mod assert_stdout;
 mod assert_success;
-mod common;
-mod tempdir;
-
 mod choose;
 mod command;
+mod common;
 mod completions;
 mod conditional;
 mod delimiters;
@@ -31,4 +29,6 @@ mod shebang;
 mod shell;
 mod string;
 mod sublime_syntax;
+mod subsequents;
+mod tempdir;
 mod working_directory;
diff --git a/tests/misc.rs b/tests/misc.rs
index 9ced5062..cc4b75d6 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -1336,7 +1336,7 @@ test! {
   justfile: "foo: 'bar'",
   args:     ("foo"),
   stdout:   "",
-  stderr:   "error: Expected comment, end of file, end of line, \
+  stderr:   "error: Expected '&&', comment, end of file, end of line, \
     identifier, or '(', but found string
   |
 1 | foo: 'bar'
diff --git a/tests/subsequents.rs b/tests/subsequents.rs
new file mode 100644
index 00000000..36230b99
--- /dev/null
+++ b/tests/subsequents.rs
@@ -0,0 +1,151 @@
+use crate::common::*;
+
+test! {
+  name: success,
+  justfile: "
+    foo: && bar
+      echo foo
+
+    bar:
+      echo bar
+  ",
+  stdout: "
+    foo
+    bar
+  ",
+  stderr: "
+    echo foo
+    echo bar
+  ",
+}
+
+test! {
+  name: failure,
+  justfile: "
+    foo: && bar
+      echo foo
+      false
+
+    bar:
+      echo bar
+  ",
+  stdout: "
+    foo
+  ",
+  stderr: "
+    echo foo
+    false
+    error: Recipe `foo` failed on line 3 with exit code 1
+  ",
+  status: EXIT_FAILURE,
+}
+
+test! {
+  name: circular_dependency,
+  justfile: "
+    foo: && foo
+  ",
+  stderr: "
+    error: Recipe `foo` depends on itself
+      |
+    1 | foo: && foo
+      |         ^^^
+  ",
+  status: EXIT_FAILURE,
+}
+
+test! {
+  name: unknown,
+  justfile: "
+    foo: && bar
+  ",
+  stderr: "
+    error: Recipe `foo` has unknown dependency `bar`
+      |
+    1 | foo: && bar
+      |         ^^^
+  ",
+  status: EXIT_FAILURE,
+}
+
+test! {
+  name: unknown_argument,
+  justfile: "
+    bar x:
+
+    foo: && (bar y)
+  ",
+  stderr: "
+    error: Variable `y` not defined
+      |
+    3 | foo: && (bar y)
+      |              ^
+  ",
+  status: EXIT_FAILURE,
+}
+
+test! {
+  name: argument,
+  justfile: "
+    foo: && (bar 'hello')
+
+    bar x:
+      echo {{ x }}
+  ",
+  stdout: "
+    hello
+  ",
+  stderr: "
+    echo hello
+  ",
+}
+
+test! {
+  name: duplicate_subsequents_dont_run,
+  justfile: "
+    a: && b c
+      echo a
+
+    b: d
+      echo b
+
+    c: d
+      echo c
+
+    d:
+      echo d
+  ",
+  stdout: "
+    a
+    d
+    b
+    c
+  ",
+  stderr: "
+    echo a
+    echo d
+    echo b
+    echo c
+  ",
+}
+
+test! {
+  name: subsequents_run_even_if_already_ran_as_prior,
+  justfile: "
+    a: b && b
+      echo a
+
+    b:
+      echo b
+  ",
+  stdout: "
+    b
+    a
+    b
+  ",
+  stderr: "
+    echo b
+    echo a
+    echo b
+  ",
+}