Add subsequent dependencies (#820)

Subsequents are dependencies which run after a recipe instead of prior.
Subsequents to a recipe only run if the recipe succeeds. Subsequents
will run even if a matching invocation already ran as a prior
dependencies.
This commit is contained in:
Casey Rodarmor 2021-07-22 00:20:25 -07:00 committed by GitHub
parent 7bbc38a261
commit 77bba3ee0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 311 additions and 38 deletions

View File

@ -302,7 +302,7 @@ impl<'src> Justfile<'src> {
let mut evaluator = let mut evaluator =
Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search); 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()]; let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments { for argument in arguments {
@ -321,6 +321,27 @@ impl<'src> Justfile<'src> {
recipe.run(context, dotenv, scope.child(), search, &positional)?; 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()]; let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments.iter().cloned() { for argument in arguments.iter().cloned() {
invocation.push(argument.to_owned()); invocation.push(argument.to_owned());

View File

@ -479,7 +479,8 @@ impl<'src> Lexer<'src> {
/// Lex token beginning with `start` outside of a recipe body /// Lex token beginning with `start` outside of a recipe body
fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> { fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
match start { match start {
'!' => self.lex_bang(), '&' => self.lex_digraph('&', '&', AmpersandAmpersand),
'!' => self.lex_digraph('!', '=', BangEquals),
'*' => self.lex_single(Asterisk), '*' => self.lex_single(Asterisk),
'$' => self.lex_single(Dollar), '$' => self.lex_single(Dollar),
'@' => self.lex_single(At), '@' => self.lex_single(At),
@ -679,25 +680,30 @@ impl<'src> Lexer<'src> {
!self.open_delimiters.is_empty() !self.open_delimiters.is_empty()
} }
/// Lex a token starting with '!' /// Lex a two-character digraph
fn lex_bang(&mut self) -> CompilationResult<'src, ()> { fn lex_digraph(
self.presume('!')?; &mut self,
left: char,
right: char,
token: TokenKind,
) -> CompilationResult<'src, ()> {
self.presume(left)?;
if self.accepted('=')? { if self.accepted(right)? {
self.token(BangEquals); self.token(token);
Ok(()) Ok(())
} else { } else {
// Emit an unspecified token to consume the current character, // Emit an unspecified token to consume the current character,
self.token(Unspecified); self.token(Unspecified);
if self.at_eof() { if self.at_eof() {
return Err(self.error(UnexpectedEndOfToken { expected: '=' })); return Err(self.error(UnexpectedEndOfToken { expected: right }));
} }
// …and advance past another character, // …and advance past another character,
self.advance()?; self.advance()?;
// …so that the error we produce highlights the unexpected character. // …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 { fn default_lexeme(kind: TokenKind) -> &'static str {
match kind { match kind {
// Fixed lexemes // Fixed lexemes
AmpersandAmpersand => "&&",
Asterisk => "*", Asterisk => "*",
At => "@", At => "@",
BangEquals => "!=", BangEquals => "!=",
@ -1048,6 +1055,12 @@ mod tests {
tokens: (StringToken:"\"\"\"hello\ngoodbye\"\"\""), tokens: (StringToken:"\"\"\"hello\ngoodbye\"\"\""),
} }
test! {
name: ampersand_ampersand,
text: "&&",
tokens: (AmpersandAmpersand),
}
test! { test! {
name: equals, name: equals,
text: "=", text: "=",
@ -2109,16 +2122,6 @@ mod tests {
kind: UnpairedCarriageReturn, kind: UnpairedCarriageReturn,
} }
error! {
name: unknown_start_of_token_ampersand,
input: " \r\n&",
offset: 3,
line: 1,
column: 0,
width: 1,
kind: UnknownStartOfToken,
}
error! { error! {
name: unknown_start_of_token_tilde, name: unknown_start_of_token_tilde,
input: "~", 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] #[test]
fn presume_error() { fn presume_error() {
assert_matches!( assert_matches!(

View File

@ -145,18 +145,29 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
if !self.dependencies.is_empty() { if !self.dependencies.is_empty() {
let mut dependencies = Tree::atom("deps"); 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()); let mut d = Tree::atom(dependency.recipe.lexeme());
for argument in &dependency.arguments { for argument in &dependency.arguments {
d.push_mut(argument.tree()); 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() { if !self.body.is_empty() {

View File

@ -627,19 +627,36 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
dependencies.push(dependency); 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()?; self.expect_eol()?;
let body = self.parse_body()?; let body = self.parse_body()?;
Ok(Recipe { Ok(Recipe {
parameters: positional.into_iter().chain(variadic).collect(),
private: name.lexeme().starts_with('_'), private: name.lexeme().starts_with('_'),
shebang: body.first().map(Line::is_shebang).unwrap_or(false), shebang: body.first().map(Line::is_shebang).unwrap_or(false),
parameters: positional.into_iter().chain(variadic).collect(), priors,
body,
dependencies,
doc, doc,
name, name,
quiet, quiet,
dependencies,
body,
}) })
} }
@ -1102,6 +1119,12 @@ mod tests {
tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))), tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))),
} }
test! {
name: recipe_subsequent,
text: "foo: && bar",
tree: (justfile (recipe foo (sups bar))),
}
test! { test! {
name: recipe_line_single, name: recipe_line_single,
text: "foo:\n bar", text: "foo:\n bar",
@ -1885,10 +1908,13 @@ mod tests {
name: missing_eol, name: missing_eol,
input: "a b c: z =", input: "a b c: z =",
offset: 9, offset: 9,
line: 0, line: 0,
column: 9, column: 9,
width: 1, width: 1,
kind: UnexpectedToken{expected: vec![Comment, Eof, Eol, Identifier, ParenL], found: Equals}, kind: UnexpectedToken{
expected: vec![AmpersandAmpersand, Comment, Eof, Eol, Identifier, ParenL],
found: Equals
},
} }
error! { error! {

View File

@ -25,14 +25,15 @@ fn error_from_signal(
/// A recipe, e.g. `foo: bar baz` /// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) body: Vec<Line<'src>>,
pub(crate) dependencies: Vec<D>, pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'src str>, pub(crate) doc: Option<&'src str>,
pub(crate) body: Vec<Line<'src>>,
pub(crate) name: Name<'src>, pub(crate) name: Name<'src>,
pub(crate) parameters: Vec<Parameter<'src>>, pub(crate) parameters: Vec<Parameter<'src>>,
pub(crate) private: bool, pub(crate) private: bool,
pub(crate) quiet: bool, pub(crate) quiet: bool,
pub(crate) shebang: bool, pub(crate) shebang: bool,
pub(crate) priors: usize,
} }
impl<'src, D> Recipe<'src, D> { impl<'src, D> Recipe<'src, D> {
@ -330,7 +331,12 @@ impl<'src, D: Display> Display for Recipe<'src, D> {
write!(f, " {}", parameter)?; write!(f, " {}", parameter)?;
} }
write!(f, ":")?; write!(f, ":")?;
for dependency in &self.dependencies {
for (i, dependency) in self.dependencies.iter().enumerate() {
if i == self.priors {
write!(f, " &&")?;
}
write!(f, " {}", dependency)?; write!(f, " {}", dependency)?;
} }

View File

@ -113,9 +113,10 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
} }
} }
stack.pop();
let resolved = Rc::new(recipe.resolve(dependencies)?); let resolved = Rc::new(recipe.resolve(dependencies)?);
self.resolved_recipes.insert(Rc::clone(&resolved)); self.resolved_recipes.insert(Rc::clone(&resolved));
stack.pop();
Ok(resolved) Ok(resolved)
} }
} }

View File

@ -2,6 +2,7 @@ use crate::common::*;
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)] #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) enum TokenKind { pub(crate) enum TokenKind {
AmpersandAmpersand,
Asterisk, Asterisk,
At, At,
Backtick, Backtick,
@ -37,6 +38,7 @@ impl Display for TokenKind {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
use TokenKind::*; use TokenKind::*;
write!(f, "{}", match *self { write!(f, "{}", match *self {
AmpersandAmpersand => "'&&'",
Asterisk => "'*'", Asterisk => "'*'",
At => "'@'", At => "'@'",
Backtick => "backtick", Backtick => "backtick",

View File

@ -42,6 +42,12 @@ macro_rules! tree {
$crate::tree::Tree::atom("*") $crate::tree::Tree::atom("*")
}; };
{
&&
} => {
$crate::tree::Tree::atom("&&")
};
{ {
== ==
} => { } => {

View File

@ -7,7 +7,14 @@ impl<'src> UnresolvedRecipe<'src> {
self, self,
resolved: Vec<Rc<Recipe<'src>>>, resolved: Vec<Rc<Recipe<'src>>>,
) -> CompilationResult<'src, 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) { for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) {
assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme()); assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme());
if !resolved if !resolved
@ -36,13 +43,14 @@ impl<'src> UnresolvedRecipe<'src> {
.collect(); .collect();
Ok(Recipe { Ok(Recipe {
doc: self.doc,
body: self.body, body: self.body,
doc: self.doc,
name: self.name, name: self.name,
parameters: self.parameters, parameters: self.parameters,
private: self.private, private: self.private,
quiet: self.quiet, quiet: self.quiet,
shebang: self.shebang, shebang: self.shebang,
priors: self.priors,
dependencies, dependencies,
}) })
} }

View File

@ -944,3 +944,18 @@ test! {
echo foo echo foo
", ",
} }
test! {
name: subsequent,
justfile: "
bar:
foo: && bar
echo foo",
args: ("--dump"),
stdout: "
bar:
foo: && bar
echo foo
",
}

View File

@ -3,11 +3,9 @@ mod test;
mod assert_stdout; mod assert_stdout;
mod assert_success; mod assert_success;
mod common;
mod tempdir;
mod choose; mod choose;
mod command; mod command;
mod common;
mod completions; mod completions;
mod conditional; mod conditional;
mod delimiters; mod delimiters;
@ -31,4 +29,6 @@ mod shebang;
mod shell; mod shell;
mod string; mod string;
mod sublime_syntax; mod sublime_syntax;
mod subsequents;
mod tempdir;
mod working_directory; mod working_directory;

View File

@ -1336,7 +1336,7 @@ test! {
justfile: "foo: 'bar'", justfile: "foo: 'bar'",
args: ("foo"), args: ("foo"),
stdout: "", 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 identifier, or '(', but found string
| |
1 | foo: 'bar' 1 | foo: 'bar'

151
tests/subsequents.rs Normal file
View File

@ -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
",
}