diff --git a/src/justfile.rs b/src/justfile.rs index 4eb474fb1f..2346332bc3 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -198,6 +198,7 @@ impl<'src> Justfile<'src> { } let mut ran = Ran::default(); + for invocation in invocations { let context = ExecutionContext { config, @@ -207,18 +208,32 @@ impl<'src> Justfile<'src> { search, }; - Self::run_recipe( - &invocation - .arguments - .iter() - .copied() - .map(str::to_string) - .collect::>(), - &context, - &mut ran, - invocation.recipe, - false, - )?; + let evaluated = &invocation + .arguments + .iter() + .copied() + .map(str::to_string) + .collect::>(); + + let result = Self::run_recipe(evaluated, &context, &mut ran, invocation.recipe, false); + + let mut evaluator = Evaluator::new(&context, true, &scope); + if let Err(err) = result { + if invocation.recipe.recoveries().next().is_none() || context.config.no_dependencies { + return Err(err); + } + + let mut ran = Ran::default(); + + for Dependency { recipe, arguments } in invocation.recipe.recoveries() { + let evaluated = arguments + .iter() + .map(|argument| evaluator.evaluate_expression(argument)) + .collect::>>()?; + + Self::run_recipe(&evaluated, &context, &mut ran, recipe, true)?; + } + } } Ok(()) @@ -344,6 +359,7 @@ impl<'src> Justfile<'src> { } } + // eprintln!("name:{} scope:{}", recipe.name, is_dependency); recipe.run(context, &scope, &positional, is_dependency)?; if !context.config.no_dependencies { diff --git a/src/lexer.rs b/src/lexer.rs index cd2fe82104..cbe9d0f47a 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1117,6 +1117,12 @@ mod tests { tokens: (AmpersandAmpersand), } + test! { + name: bar_bar, + text: "||", + tokens: (BarBar), + } + test! { name: equals, text: "=", diff --git a/src/parser.rs b/src/parser.rs index a72003bf3e..eea0b0d594 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -956,6 +956,22 @@ impl<'run, 'src> Parser<'run, 'src> { dependencies.append(&mut subsequents); } + let if_error = dependencies.len(); + + if self.accepted(BarBar)? { + let mut recoveries = Vec::new(); + + while let Some(recovery) = self.accept_dependency()? { + recoveries.push(recovery); + } + + if recoveries.is_empty() { + return Err(self.unexpected_token()?); + } + + dependencies.append(&mut recoveries); + } + self.expect_eol()?; let body = self.parse_body()?; @@ -1007,6 +1023,7 @@ impl<'run, 'src> Parser<'run, 'src> { dependencies, doc: doc.filter(|doc| !doc.is_empty()), file_depth: self.file_depth, + if_error, import_offsets: self.import_offsets.clone(), name, namepath: self @@ -2533,7 +2550,7 @@ mod tests { column: 9, width: 1, kind: UnexpectedToken{ - expected: vec![AmpersandAmpersand, Comment, Eof, Eol, Identifier, ParenL], + expected: vec![AmpersandAmpersand, BarBar, Comment, Eof, Eol, Identifier, ParenL], found: Equals }, } diff --git a/src/recipe.rs b/src/recipe.rs index 83bb9ee3f2..77d341dfd2 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -25,6 +25,7 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) doc: Option, #[serde(skip)] pub(crate) file_depth: u32, + pub(crate) if_error: usize, #[serde(skip)] pub(crate) import_offsets: Vec, pub(crate) name: Name<'src>, @@ -503,7 +504,11 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn subsequents(&self) -> impl Iterator { - self.dependencies.iter().skip(self.priors) + self.dependencies[self.priors..self.if_error].iter() + } + + pub(crate) fn recoveries(&self) -> impl Iterator { + self.dependencies[self.if_error..].iter() } } @@ -535,10 +540,14 @@ impl ColorDisplay for Recipe<'_, D> { write!(f, ":")?; for (i, dependency) in self.dependencies.iter().enumerate() { - if i == self.priors { + if i == self.priors && self.subsequents().next().is_some() { write!(f, " &&")?; } + if i == self.if_error { + write!(f, " ||")?; + } + write!(f, " {dependency}")?; } diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs index 661d75649b..f127972b88 100644 --- a/src/unresolved_recipe.rs +++ b/src/unresolved_recipe.rs @@ -50,6 +50,7 @@ impl<'src> UnresolvedRecipe<'src> { dependencies, doc: self.doc, file_depth: self.file_depth, + if_error: self.if_error, import_offsets: self.import_offsets, name: self.name, namepath: self.namepath, diff --git a/tests/format.rs b/tests/format.rs index f1e225fc96..212d0bb185 100644 --- a/tests/format.rs +++ b/tests/format.rs @@ -1459,6 +1459,27 @@ fn no_trailing_newline() { .run(); } +#[test] +fn recovery() { + Test::new() + .arg("--dump") + .justfile( + " + bar: + foo: || bar + echo foo", + ) + .stdout( + " + bar: + + foo: || bar + echo foo + ", + ) + .run(); +} + #[test] fn subsequent() { Test::new() diff --git a/tests/json.rs b/tests/json.rs index 30f713bea8..571f34f04c 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -63,6 +63,7 @@ struct Recipe<'a> { body: Vec, dependencies: Vec>, doc: Option<&'a str>, + if_error: u32, name: &'a str, namepath: &'a str, parameters: Vec>, @@ -277,6 +278,7 @@ fn dependencies() { ..default() }] .into(), + if_error: 1, priors: 1, ..default() }, @@ -356,6 +358,7 @@ fn dependency_argument() { .into(), }] .into(), + if_error: 1, priors: 1, ..default() }, @@ -601,6 +604,7 @@ fn priors() { .into(), name: "b", namepath: "b", + if_error: 2, priors: 1, ..default() }, diff --git a/tests/lib.rs b/tests/lib.rs index 45845d5839..e93a328724 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -103,6 +103,7 @@ mod private; mod quiet; mod quote; mod readme; +mod recoveries; mod recursion_limit; mod regexes; mod request; diff --git a/tests/misc.rs b/tests/misc.rs index 3c572bbe52..140b3f3de5 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -1634,7 +1634,7 @@ fn unexpected_token_in_dependency_position() { .arg("foo") .justfile("foo: 'bar'") .stderr( - "error: Expected '&&', comment, end of file, end of line, \ + "error: Expected '&&', '||', comment, end of file, end of line, \ identifier, or '(', but found string ——▶ justfile:1:6 │ diff --git a/tests/no_dependencies.rs b/tests/no_dependencies.rs index 68a3ac1030..7e77ff3460 100644 --- a/tests/no_dependencies.rs +++ b/tests/no_dependencies.rs @@ -32,6 +32,31 @@ fn skip_prior_dependency() { .run(); } +#[test] +fn skip_recovery_deps() { + Test::new() + .justfile( + " + a: || b + @echo 'a' + exit 1 + b: + @echo 'b' + + ", + ) + .args(["--no-deps"]) + .stdout("a\n") + .stderr( + " + exit 1 + error: Recipe `a` failed on line 3 with exit code 1 + ", + ) + .status(EXIT_FAILURE) + .run(); +} + #[test] fn skip_dependency_multi() { Test::new() diff --git a/tests/recoveries.rs b/tests/recoveries.rs new file mode 100644 index 0000000000..f89704d96f --- /dev/null +++ b/tests/recoveries.rs @@ -0,0 +1,398 @@ +use super::*; + +#[test] +fn one_successful_recovery() { + Test::new() + .justfile( + " + foo: || bar + echo foo + exit 1 + + bar: + echo bar + ", + ) + .stdout( + " + foo + bar + ", + ) + .stderr( + " + echo foo + exit 1 + echo bar + ", + ) + .run(); +} + +#[test] +fn two_successful_recoveries() { + Test::new() + .justfile( + " + foo: || bar bar2 + echo foo + exit 1 + + bar: + echo bar + + bar2: + echo bar2 + ", + ) + .stdout( + " + foo + bar + bar2 + ", + ) + .stderr( + " + echo foo + exit 1 + echo bar + echo bar2 + ", + ) + .run(); +} + +#[test] +fn one_failed_recovery() { + Test::new() + .justfile( + " + foo: || bar + echo foo + exit 2 + + bar: + echo bar + exit 1 + ", + ) + .stdout( + " + foo + bar + ", + ) + .stderr( + " + echo foo + exit 2 + echo bar + exit 1 + error: Recipe `bar` failed on line 7 with exit code 1 + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn first_of_two_recoveries_fail() { + Test::new() + .justfile( + " + foo: || bar bar2 + echo foo + exit 2 + + bar: + echo bar + exit 1 + + bar2: + echo bar2 + ", + ) + .stdout( + " + foo + bar + ", + ) + .stderr( + " + echo foo + exit 2 + echo bar + exit 1 + error: Recipe `bar` failed on line 7 with exit code 1 + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn recoveries_with_other_dependencies() { + Test::new() + .justfile( + " + a: b && c || d + echo a + exit 1 + + b: + echo b + + c: + echo c + + d: + echo d + + ", + ) + .stdout( + " + b + a + d + ", + ) + .stderr( + " + echo b + echo a + exit 1 + echo d + ", + ) + .run(); +} + +#[test] +fn circular_dependency() { + Test::new() + .justfile( + " + foo: || foo + ", + ) + .stderr( + " + error: Recipe `foo` depends on itself + ——▶ justfile:1:9 + │ + 1 │ foo: || foo + │ ^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn unknown() { + Test::new() + .justfile( + " + foo: || bar + ", + ) + .stderr( + " + error: Recipe `foo` has unknown dependency `bar` + ——▶ justfile:1:9 + │ + 1 │ foo: || bar + │ ^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn unknown_argument() { + Test::new() + .justfile( + " + bar x: + + foo: || (bar y) + ", + ) + .stderr( + " + error: Variable `y` not defined + ——▶ justfile:3:14 + │ + 3 │ foo: || (bar y) + │ ^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn argument() { + Test::new() + .justfile( + " + foo: || (bar 'hello') + exit 1 + + bar x: + echo {{ x }} + ", + ) + .stdout( + " + hello + ", + ) + .stderr( + " + exit 1 + echo hello + ", + ) + .run(); +} + +#[test] +fn duplicate_recoveries_dont_run() { + Test::new() + .justfile( + " + a: || b c + echo a + exit 1 + + b: d + echo b + + c: d + echo c + + d: + echo d + ", + ) + .stdout( + " + a + d + b + c + ", + ) + .stderr( + " + echo a + exit 1 + echo d + echo b + echo c + ", + ) + .run(); +} + +#[test] +fn recoveries_run_even_if_already_ran_as_prior() { + Test::new() + .justfile( + " + a: b || b + echo a + exit 1 + + b: + echo b + ", + ) + .stdout( + " + b + a + b + ", + ) + .stderr( + " + echo b + echo a + exit 1 + echo b + ", + ) + .run(); +} + +#[test] +fn recoveries_in_pre_deps() { + Test::new() + .justfile( + " + a: b || c + echo a + + b: + echo b + exit 1 + + c: + echo c + ", + ) + .stdout( + " + b + c + ", + ) + .stderr( + " + echo b + exit 1 + echo c + ", + ) + .run(); +} + +#[test] +fn recoveries_in_post_deps() { + Test::new() + .justfile( + " + a: && b || c + echo a + + b: + echo b + exit 1 + + c: + echo c + ", + ) + .stdout( + " + a + b + c + ", + ) + .stderr( + " + echo a + echo b + exit 1 + echo c + ", + ) + .run(); +}