diff --git a/GRAMMAR.md b/GRAMMAR.md index 110ebf6b..fc851f2f 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -73,12 +73,13 @@ string : STRING sequence : expression ',' sequence | expression ','? -recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body? +recipe : '@'? NAME parameter* ('+' parameter)? ':' dependency* body? parameter : NAME | NAME '=' value -dependencies : NAME+ +dependency : NAME + | '(' NAME expression* ') body : INDENT line+ DEDENT diff --git a/README.adoc b/README.adoc index da7e8cf4..37edb902 100644 --- a/README.adoc +++ b/README.adoc @@ -547,9 +547,7 @@ build target: cd {{target}} && make ``` -Other recipes may not depend on a recipe with parameters. - -To pass arguments, put them after the recipe name: +To pass arguments on the command line, put them after the recipe name: ```sh $ just build my-awesome-project @@ -557,6 +555,16 @@ Building my-awesome-project... cd my-awesome-project && make ``` +To pass arguments to a dependency, put the dependency in parentheses along with the arguments: + +```make +default: (build "main") + +build target: + @echo 'Building {{target}}...' + cd {{target}} && make +``` + Parameters may have default values: ```make diff --git a/src/analyzer.rs b/src/analyzer.rs index eee291ad..8ec5d0a3 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -3,7 +3,7 @@ use crate::common::*; use CompilationErrorKind::*; pub(crate) struct Analyzer<'src> { - recipes: Table<'src, Recipe<'src, Name<'src>>>, + recipes: Table<'src, UnresolvedRecipe<'src>>, assignments: Table<'src, Assignment<'src>>, aliases: Table<'src, Alias<'src, Name<'src>>>, sets: Table<'src, Set<'src>>, @@ -91,7 +91,7 @@ impl<'src> Analyzer<'src> { }) } - fn analyze_recipe(&self, recipe: &Recipe<'src, Name<'src>>) -> CompilationResult<'src, ()> { + fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompilationResult<'src, ()> { if let Some(original) = self.recipes.get(recipe.name.lexeme()) { return Err(recipe.name.token().error(DuplicateRecipe { recipe: original.name(), @@ -125,17 +125,6 @@ impl<'src> Analyzer<'src> { } } - let mut dependencies = BTreeSet::new(); - for dependency in &recipe.dependencies { - if dependencies.contains(dependency.lexeme()) { - return Err(dependency.token().error(DuplicateDependency { - recipe: recipe.name.lexeme(), - dependency: dependency.lexeme(), - })); - } - dependencies.insert(dependency.lexeme()); - } - let mut continued = false; for line in &recipe.body { if !recipe.shebang && !continued { @@ -295,26 +284,6 @@ mod tests { kind: ParameterShadowsVariable{parameter: "foo"}, } - analysis_error! { - name: dependency_has_parameters, - input: "foo arg:\nb: foo", - offset: 12, - line: 1, - column: 3, - width: 3, - kind: DependencyHasParameters{recipe: "b", dependency: "foo"}, - } - - analysis_error! { - name: duplicate_dependency, - input: "a b c: b c z z", - offset: 13, - line: 0, - column: 13, - width: 1, - kind: DuplicateDependency{recipe: "a", dependency: "z"}, - } - analysis_error! { name: duplicate_recipe, input: "a:\nb:\na:", diff --git a/src/common.rs b/src/common.rs index 98dd3f9c..2fb50928 100644 --- a/src/common.rs +++ b/src/common.rs @@ -62,8 +62,9 @@ pub(crate) use crate::{ search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token, - token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity, - warning::Warning, + token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, + unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, + verbosity::Verbosity, warning::Warning, }; // type aliases diff --git a/src/compilation_error.rs b/src/compilation_error.rs index 5f9312e2..d43b2664 100644 --- a/src/compilation_error.rs +++ b/src/compilation_error.rs @@ -89,13 +89,6 @@ impl Display for CompilationError<'_> { self.token.line.ordinal(), )?; } - DuplicateDependency { recipe, dependency } => { - writeln!( - f, - "Recipe `{}` has duplicate dependency `{}`", - recipe, dependency - )?; - } DuplicateRecipe { recipe, first } => { writeln!( f, @@ -114,13 +107,28 @@ impl Display for CompilationError<'_> { self.token.line.ordinal(), )?; } - DependencyHasParameters { recipe, dependency } => { - writeln!( + DependencyArgumentCountMismatch { + dependency, + found, + min, + max, + } => { + write!( f, - "Recipe `{}` depends on `{}` which requires arguments. \ - Dependencies may not require arguments", - recipe, dependency + "Dependency `{}` got {} {} but takes ", + dependency, + found, + Count("argument", found), )?; + + if min == max { + let expected = min; + writeln!(f, "{} {}", expected, Count("argument", expected))?; + } else if found < min { + writeln!(f, "at least {} {}", min, Count("argument", min))?; + } else { + writeln!(f, "at most {} {}", max, Count("argument", max))?; + } } ParameterShadowsVariable { parameter } => { writeln!( diff --git a/src/compilation_error_kind.rs b/src/compilation_error_kind.rs index 4a3a9fbe..3e80cf6d 100644 --- a/src/compilation_error_kind.rs +++ b/src/compilation_error_kind.rs @@ -14,18 +14,16 @@ pub(crate) enum CompilationErrorKind<'src> { variable: &'src str, circle: Vec<&'src str>, }, - DependencyHasParameters { - recipe: &'src str, + DependencyArgumentCountMismatch { dependency: &'src str, + found: usize, + min: usize, + max: usize, }, DuplicateAlias { alias: &'src str, first: usize, }, - DuplicateDependency { - recipe: &'src str, - dependency: &'src str, - }, DuplicateParameter { recipe: &'src str, parameter: &'src str, diff --git a/src/dependency.rs b/src/dependency.rs index 38c9712f..aa620c43 100644 --- a/src/dependency.rs +++ b/src/dependency.rs @@ -1,4 +1,23 @@ use crate::common::*; #[derive(PartialEq, Debug)] -pub(crate) struct Dependency<'src>(pub(crate) Rc>); +pub(crate) struct Dependency<'src> { + pub(crate) recipe: Rc>, + pub(crate) arguments: Vec>, +} + +impl<'src> Display for Dependency<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + if self.arguments.is_empty() { + write!(f, "{}", self.recipe.name()) + } else { + write!(f, "({}", self.recipe.name())?; + + for argument in &self.arguments { + write!(f, " {}", argument)?; + } + + write!(f, ")") + } + } +} diff --git a/src/evaluator.rs b/src/evaluator.rs index 5586b5d7..c2577401 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -206,7 +206,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { Ok(scope) } - pub(crate) fn line_evaluator( + pub(crate) fn recipe_evaluator( config: &'run Config, dotenv: &'run BTreeMap, scope: &'run Scope<'src, 'run>, diff --git a/src/item.rs b/src/item.rs index b7cb147b..0af1d0bf 100644 --- a/src/item.rs +++ b/src/item.rs @@ -5,6 +5,6 @@ use crate::common::*; pub(crate) enum Item<'src> { Alias(Alias<'src, Name<'src>>), Assignment(Assignment<'src>), - Recipe(Recipe<'src, Name<'src>>), + Recipe(UnresolvedRecipe<'src>), Set(Set<'src>), } diff --git a/src/justfile.rs b/src/justfile.rs index ae5a6a20..86583eb1 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -175,7 +175,7 @@ impl<'src> Justfile<'src> { working_directory, }; - let mut ran = empty(); + let mut ran = BTreeSet::new(); for (recipe, arguments) in grouped { self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)? } @@ -201,17 +201,54 @@ impl<'src> Justfile<'src> { &self, context: &'run RecipeContext<'src, 'run>, recipe: &Recipe<'src>, - arguments: &[&'src str], + arguments: &[&'run str], dotenv: &BTreeMap, - ran: &mut BTreeSet<&'src str>, + ran: &mut BTreeSet>, ) -> RunResult<'src, ()> { - for Dependency(dependency) in &recipe.dependencies { - if !ran.contains(dependency.name()) { - self.run_recipe(context, dependency, &[], dotenv, ran)?; + let scope = Evaluator::evaluate_parameters( + context.config, + dotenv, + &recipe.parameters, + arguments, + &context.scope, + context.settings, + context.working_directory, + )?; + + let mut evaluator = Evaluator::recipe_evaluator( + context.config, + dotenv, + &scope, + context.settings, + context.working_directory, + ); + + for Dependency { recipe, arguments } in &recipe.dependencies { + let mut invocation = vec![recipe.name().to_owned()]; + + for argument in arguments { + invocation.push(evaluator.evaluate_expression(argument)?); + } + + if !ran.contains(&invocation) { + let arguments = invocation + .iter() + .skip(1) + .map(String::as_ref) + .collect::>(); + self.run_recipe(context, recipe, &arguments, dotenv, ran)?; } } - recipe.run(context, arguments, dotenv)?; - ran.insert(recipe.name()); + + recipe.run(context, dotenv, scope)?; + + let mut invocation = Vec::new(); + invocation.push(recipe.name().to_owned()); + for argument in arguments.iter().cloned() { + invocation.push(argument.to_owned()); + } + + ran.insert(invocation); Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 55b6090d..92c0b6f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,8 @@ mod table; mod thunk; mod token; mod token_kind; +mod unresolved_dependency; +mod unresolved_recipe; mod use_color; mod variables; mod verbosity; diff --git a/src/node.rs b/src/node.rs index cc4bed5d..acbf2c27 100644 --- a/src/node.rs +++ b/src/node.rs @@ -81,7 +81,7 @@ impl<'src> Node<'src> for Expression<'src> { } } -impl<'src> Node<'src> for Recipe<'src, Name<'src>> { +impl<'src> Node<'src> for UnresolvedRecipe<'src> { fn tree(&self) -> Tree<'src> { let mut t = Tree::atom("recipe"); @@ -111,14 +111,19 @@ impl<'src> Node<'src> for Recipe<'src, Name<'src>> { } if !self.dependencies.is_empty() { - t = t.push( - Tree::atom("deps").extend( - self - .dependencies - .iter() - .map(|dependency| dependency.lexeme()), - ), - ); + let mut dependencies = Tree::atom("deps"); + + for dependency in &self.dependencies { + let mut d = Tree::atom(dependency.recipe.lexeme()); + + for argument in &dependency.arguments { + d.push_mut(argument.tree()); + } + + dependencies.push_mut(d); + } + + t.push_mut(dependencies); } if !self.body.is_empty() { diff --git a/src/parser.rs b/src/parser.rs index 5744048b..712f33cb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -217,7 +217,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } } - /// Accept a token of kind `Identifier` and parse into an `Name` + /// Accept a token of kind `Identifier` and parse into a `Name` fn accept_name(&mut self) -> CompilationResult<'src, Option>> { if self.next_is(Identifier) { Ok(Some(self.parse_name()?)) @@ -226,6 +226,28 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } } + /// Accept a dependency + fn accept_dependency(&mut self) -> CompilationResult<'src, Option>> { + if let Some(recipe) = self.accept_name()? { + Ok(Some(UnresolvedDependency { + arguments: Vec::new(), + recipe, + })) + } else if self.accepted(ParenL)? { + let recipe = self.parse_name()?; + + let mut arguments = Vec::new(); + + while !self.accepted(ParenR)? { + arguments.push(self.parse_expression()?); + } + + Ok(Some(UnresolvedDependency { recipe, arguments })) + } else { + Ok(None) + } + } + /// Accept and return `true` if next token is of kind `kind` fn accepted(&mut self, kind: TokenKind) -> CompilationResult<'src, bool> { Ok(self.accept(kind)?.is_some()) @@ -470,7 +492,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { &mut self, doc: Option<&'src str>, quiet: bool, - ) -> CompilationResult<'src, Recipe<'src, Name<'src>>> { + ) -> CompilationResult<'src, UnresolvedRecipe<'src>> { let name = self.parse_name()?; let mut positional = Vec::new(); @@ -521,7 +543,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { let mut dependencies = Vec::new(); - while let Some(dependency) = self.accept_name()? { + while let Some(dependency) = self.accept_dependency()? { dependencies.push(dependency); } @@ -934,6 +956,30 @@ mod tests { tree: (justfile (recipe foo (deps bar baz))), } + test! { + name: recipe_dependency_parenthesis, + text: "foo: (bar)", + tree: (justfile (recipe foo (deps bar))), + } + + test! { + name: recipe_dependency_argument_string, + text: "foo: (bar 'baz')", + tree: (justfile (recipe foo (deps (bar "baz")))), + } + + test! { + name: recipe_dependency_argument_identifier, + text: "foo: (bar baz)", + tree: (justfile (recipe foo (deps (bar baz)))), + } + + test! { + name: recipe_dependency_argument_concatination, + text: "foo: (bar 'a' + 'b' 'c' + 'd')", + tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))), + } + test! { name: recipe_line_single, text: "foo:\n bar", diff --git a/src/recipe.rs b/src/recipe.rs index 7a872e35..612e7b89 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -67,8 +67,8 @@ impl<'src, D> Recipe<'src, D> { pub(crate) fn run<'run>( &self, context: &RecipeContext<'src, 'run>, - arguments: &[&'src str], dotenv: &BTreeMap, + scope: Scope<'src, 'run>, ) -> RunResult<'src, ()> { let config = &context.config; @@ -82,17 +82,7 @@ impl<'src, D> Recipe<'src, D> { ); } - let scope = Evaluator::evaluate_parameters( - context.config, - dotenv, - &self.parameters, - arguments, - &context.scope, - context.settings, - context.working_directory, - )?; - - let mut evaluator = Evaluator::line_evaluator( + let mut evaluator = Evaluator::recipe_evaluator( context.config, dotenv, &scope, @@ -300,25 +290,6 @@ impl<'src, D> Recipe<'src, D> { } } -impl<'src> Recipe<'src, Name<'src>> { - pub(crate) fn resolve(self, resolved: Vec>) -> Recipe<'src> { - assert_eq!(self.dependencies.len(), resolved.len()); - for (name, resolved) in self.dependencies.iter().zip(&resolved) { - assert_eq!(name.lexeme(), resolved.0.name.lexeme()); - } - Recipe { - dependencies: resolved, - doc: self.doc, - body: self.body, - name: self.name, - parameters: self.parameters, - private: self.private, - quiet: self.quiet, - shebang: self.shebang, - } - } -} - impl<'src, D> Keyed<'src> for Recipe<'src, D> { fn key(&self) -> &'src str { self.name.lexeme() @@ -342,7 +313,7 @@ impl<'src> Display for Recipe<'src> { } write!(f, ":")?; for dependency in &self.dependencies { - write!(f, " {}", dependency.0.name())?; + write!(f, " {}", dependency)?; } for (i, line) in self.body.iter().enumerate() { diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index 91fb9fc4..57ac7199 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -3,15 +3,15 @@ use crate::common::*; use CompilationErrorKind::*; pub(crate) struct RecipeResolver<'src: 'run, 'run> { - unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>, + unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>, resolved_recipes: Table<'src, Rc>>, assignments: &'run Table<'src, Assignment<'src>>, } impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { pub(crate) fn resolve_recipes( - unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>, - assignments: &'run Table<'src, Assignment<'src>>, + unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>, + assignments: &Table<'src, Assignment<'src>>, ) -> CompilationResult<'src, Table<'src, Rc>>> { let mut resolver = RecipeResolver { resolved_recipes: empty(), @@ -32,6 +32,14 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { } } + for dependency in &recipe.dependencies { + for argument in &dependency.arguments { + for variable in argument.variables() { + resolver.resolve_variable(&variable, &recipe.parameters)?; + } + } + } + for line in &recipe.body { for fragment in &line.fragments { if let Fragment::Interpolation { expression, .. } = fragment { @@ -65,7 +73,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { fn resolve_recipe( &mut self, stack: &mut Vec<&'src str>, - recipe: Recipe<'src, Name<'src>>, + recipe: UnresolvedRecipe<'src>, ) -> CompilationResult<'src, Rc>> { if let Some(resolved) = self.resolved_recipes.get(recipe.name()) { return Ok(resolved.clone()); @@ -73,53 +81,39 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { stack.push(recipe.name()); - let mut dependencies: Vec = Vec::new(); + let mut dependencies: Vec> = Vec::new(); for dependency in &recipe.dependencies { - let name = dependency.lexeme(); + let name = dependency.recipe.lexeme(); if let Some(resolved) = self.resolved_recipes.get(name) { // dependency already resolved - if !resolved.parameters.is_empty() { - return Err(dependency.error(DependencyHasParameters { - recipe: recipe.name(), - dependency: name, - })); - } - - dependencies.push(Dependency(resolved.clone())); + dependencies.push(resolved.clone()); } else if stack.contains(&name) { let first = stack[0]; stack.push(first); return Err( - dependency.error(CircularRecipeDependency { + dependency.recipe.error(CircularRecipeDependency { recipe: recipe.name(), circle: stack .iter() - .skip_while(|name| **name != dependency.lexeme()) + .skip_while(|name| **name != dependency.recipe.lexeme()) .cloned() .collect(), }), ); } else if let Some(unresolved) = self.unresolved_recipes.remove(name) { // resolve unresolved dependency - if !unresolved.parameters.is_empty() { - return Err(dependency.error(DependencyHasParameters { - recipe: recipe.name(), - dependency: name, - })); - } - - dependencies.push(Dependency(self.resolve_recipe(stack, unresolved)?)); + dependencies.push(self.resolve_recipe(stack, unresolved)?); } else { // dependency is unknown - return Err(dependency.error(UnknownDependency { + return Err(dependency.recipe.error(UnknownDependency { recipe: recipe.name(), unknown: name, })); } } - let resolved = Rc::new(recipe.resolve(dependencies)); + let resolved = Rc::new(recipe.resolve(dependencies)?); self.resolved_recipes.insert(resolved.clone()); stack.pop(); Ok(resolved) @@ -189,4 +183,14 @@ mod tests { width: 3, kind: UndefinedVariable{variable: "foo"}, } + + analysis_error! { + name: unknown_variable_in_dependency_argument, + input: "bar x:\nfoo: (bar baz)", + offset: 17, + line: 1, + column: 10, + width: 3, + kind: UndefinedVariable{variable: "baz"}, + } } diff --git a/src/testing.rs b/src/testing.rs index 59abc6ed..42fc6d72 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,5 +1,7 @@ use crate::common::*; +use pretty_assertions::assert_eq; + pub(crate) fn compile(text: &str) -> Justfile { match Compiler::compile(text) { Ok(justfile) => justfile, diff --git a/src/unresolved_dependency.rs b/src/unresolved_dependency.rs new file mode 100644 index 00000000..7e975ee2 --- /dev/null +++ b/src/unresolved_dependency.rs @@ -0,0 +1,7 @@ +use crate::common::*; + +#[derive(PartialEq, Debug)] +pub(crate) struct UnresolvedDependency<'src> { + pub(crate) recipe: Name<'src>, + pub(crate) arguments: Vec>, +} diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs new file mode 100644 index 00000000..09266860 --- /dev/null +++ b/src/unresolved_recipe.rs @@ -0,0 +1,49 @@ +use crate::common::*; + +pub(crate) type UnresolvedRecipe<'src> = Recipe<'src, UnresolvedDependency<'src>>; + +impl<'src> UnresolvedRecipe<'src> { + pub(crate) fn resolve( + self, + resolved: Vec>>, + ) -> CompilationResult<'src, Recipe<'src>> { + assert_eq!(self.dependencies.len(), resolved.len()); + for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) { + assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme()); + if !resolved + .argument_range() + .contains(&unresolved.arguments.len()) + { + return Err(unresolved.recipe.error( + CompilationErrorKind::DependencyArgumentCountMismatch { + dependency: unresolved.recipe.lexeme(), + found: unresolved.arguments.len(), + min: resolved.min_arguments(), + max: resolved.max_arguments(), + }, + )); + } + } + + let dependencies = self + .dependencies + .into_iter() + .zip(resolved) + .map(|(unresolved, resolved)| Dependency { + recipe: resolved, + arguments: unresolved.arguments, + }) + .collect(); + + Ok(Recipe { + doc: self.doc, + body: self.body, + name: self.name, + parameters: self.parameters, + private: self.private, + quiet: self.quiet, + shebang: self.shebang, + dependencies, + }) + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 41c696d1..d3942ca4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1441,19 +1441,53 @@ bar:"#, } test! { - name: dependency_takes_arguments, - justfile: "b: a\na FOO:", + name: dependency_takes_arguments_exact, + justfile: " + a FOO: + b: a + ", args: ("b"), stdout: "", - stderr: "error: Recipe `b` depends on `a` which requires arguments. \ - Dependencies may not require arguments + stderr: "error: Dependency `a` got 0 arguments but takes 1 argument | -1 | b: a +2 | b: a | ^ ", status: EXIT_FAILURE, } +test! { + name: dependency_takes_arguments_at_least, + justfile: " + a FOO LUZ='hello': + b: a + ", + args: ("b"), + stdout: "", + stderr: "error: Dependency `a` got 0 arguments but takes at least 1 argument + | +2 | b: a + | ^ +", + status: EXIT_FAILURE, +} + +test! { + name: dependency_takes_arguments_at_most, + justfile: " + a FOO LUZ='hello': + b: (a '0' '1' '2') + ", + args: ("b"), + stdout: "", + stderr: "error: Dependency `a` got 3 arguments but takes at most 2 arguments + | +2 | b: (a '0' '1' '2') + | ^ +", + status: EXIT_FAILURE, +} + test! { name: duplicate_parameter, justfile: "a foo foo:", @@ -1467,19 +1501,6 @@ test! { status: EXIT_FAILURE, } -test! { - name: duplicate_dependency, - justfile: "b:\na: b b", - args: ("a"), - stdout: "", - stderr: "error: Recipe `a` has duplicate dependency `b` - | -2 | a: b b - | ^ -", - status: EXIT_FAILURE, -} - test! { name: duplicate_recipe, justfile: "b:\nb:", @@ -2261,3 +2282,141 @@ test! { stderr: "echo bar\necho foo\n", shell: false, } + +test! { + name: dependency_argument_string, + justfile: " + release: (build 'foo') (build 'bar') + + build target: + echo 'Building {{target}}...' + ", + args: (), + stdout: "Building foo...\nBuilding bar...\n", + stderr: "echo 'Building foo...'\necho 'Building bar...'\n", + shell: false, +} + +test! { + name: dependency_argument_parameter, + justfile: " + default: (release '1.0') + + release version: (build 'foo' version) (build 'bar' version) + + build target version: + echo 'Building {{target}}@{{version}}...' + ", + args: (), + stdout: "Building foo@1.0...\nBuilding bar@1.0...\n", + stderr: "echo 'Building foo@1.0...'\necho 'Building bar@1.0...'\n", + shell: false, +} + +test! { + name: dependency_argument_function, + justfile: " + foo: (bar env_var_or_default('x', 'y')) + + bar arg: + echo {{arg}} + ", + args: (), + stdout: "y\n", + stderr: "echo y\n", + shell: false, +} + +test! { + name: dependency_argument_backtick, + justfile: " + export X := 'X' + + foo: (bar `echo $X`) + + bar arg: + echo {{arg}} + echo $X + ", + args: (), + stdout: "X\nX\n", + stderr: "echo X\necho $X\n", + shell: false, +} + +test! { + name: dependency_argument_assignment, + justfile: " + v := '1.0' + + default: (release v) + + release version: + echo Release {{version}}... + ", + args: (), + stdout: "Release 1.0...\n", + stderr: "echo Release 1.0...\n", + shell: false, +} + +test! { + name: dependency_argument_variadic, + justfile: " + foo: (bar 'A' 'B' 'C') + + bar +args: + echo {{args}} + ", + args: (), + stdout: "A B C\n", + stderr: "echo A B C\n", + shell: false, +} + +test! { + name: duplicate_dependency_no_args, + justfile: " + foo: bar bar bar bar + + bar: + echo BAR + ", + args: (), + stdout: "BAR\n", + stderr: "echo BAR\n", + shell: false, +} + +test! { + name: duplicate_dependency_argument, + justfile: " + foo: (bar 'BAR') (bar `echo BAR`) + + bar bar: + echo {{bar}} + ", + args: (), + stdout: "BAR\n", + stderr: "echo BAR\n", + shell: false, +} + +test! { + name: parameter_cross_reference_error, + justfile: " + foo: + + bar a b=a: + ", + args: (), + stdout: "", + stderr: " + error: Variable `a` not defined + | + 3 | bar a b=a: + | ^ + ", + status: EXIT_FAILURE, + shell: false, +}