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:
parent
7bbc38a261
commit
77bba3ee0e
@ -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());
|
||||
|
62
src/lexer.rs
62
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!(
|
||||
|
13
src/node.rs
13
src/node.rs
@ -145,20 +145,31 @@ 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());
|
||||
}
|
||||
|
||||
if i < self.priors {
|
||||
dependencies.push_mut(d);
|
||||
} else {
|
||||
subsequents.push_mut(d);
|
||||
}
|
||||
}
|
||||
|
||||
if let Tree::List(_) = dependencies {
|
||||
t.push_mut(dependencies);
|
||||
}
|
||||
|
||||
if let Tree::List(_) = subsequents {
|
||||
t.push_mut(subsequents);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.body.is_empty() {
|
||||
t.push_mut(Tree::atom("body").extend(self.body.iter().map(|line| line.tree())));
|
||||
}
|
||||
|
@ -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",
|
||||
@ -1888,7 +1911,10 @@ mod tests {
|
||||
line: 0,
|
||||
column: 9,
|
||||
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! {
|
||||
|
@ -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)?;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -42,6 +42,12 @@ macro_rules! tree {
|
||||
$crate::tree::Tree::atom("*")
|
||||
};
|
||||
|
||||
{
|
||||
&&
|
||||
} => {
|
||||
$crate::tree::Tree::atom("&&")
|
||||
};
|
||||
|
||||
{
|
||||
==
|
||||
} => {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
15
tests/fmt.rs
15
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
|
||||
",
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
|
151
tests/subsequents.rs
Normal file
151
tests/subsequents.rs
Normal 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
|
||||
",
|
||||
}
|
Loading…
Reference in New Issue
Block a user