Repository: casey/just
Pull request #555 (2019-12-03 22:22:19)

Allow passing arguments to dependencies
I've started working on passing arguments to dependencies, see #289, a longstanding feature request.

Open questions to resolve:

## Syntax

- 1. Lisp style:

```
bar name options:
  ./build {{name}} {{options}}

foo: (bar "arg" "baz")
```

- 2. Fortran style:

```
bar name:
  ./build {{name}}

foo: bar("arg", "baz")
```

Lisp can be pretty incomprehensible, for example:

```
foo: (bar 'a' + 'b' 'c' + 'd')
```

As opposed to:

```
foo: bar('a' + 'b', 'c' + 'd')
```

However, I think in some sense lisp style is more natural, since recipe invocation on the command line doesn't require parenthesis or commas. At the moment I'm leaning towards lisp style.

## Repeat dependency invocations

Just, like Make, will only run a recipe once per invocation, even if it appears multiple times in a dependency tree. For example, given the following, `just bob` will run `bar` only once, even though `foo` and `baz` both depend on it:

```make
bar:

foo: bar

baz: bar

bob: foo baz
```

However, the situation is complicated by arguments. Consider:

```
build target:
  ./build {{target}}

release: (build "foo") (build "bar")
  ./release
```

Clear, the user intends to run `build` twice, once for "foo" and once for "bar".

I think a reasonable approach would be to run recipes once per recipe and combination of arguments. So given the following justfile, `just release` would run `build "foo"`, `build "bar"` and `build "baz"`:

```
build target:
  ./build {{target}}

build-foo: (build "foo") (build "bar")

build-baz: (build "baz") (build "bar")

release: build-foo build-baz
```

I can't immediately think of any counter-examples where this behavior wouldn't be desirable, so barring any counter examples, this is what I'll implement.

## To Do

- [x] error on undefined variables in arguments
- [x] allow recipes with parameters in dependencies
- [x] error if wrong number of arguments are passed to dependency
- [x] test assignments can be used in arguments
- [x] test parameters can be used in arguments
-  [x] test functions can be used in arguments
-  [x] test backticks can be used in arguments
- [x] dependency memoization is correct (recipe with same args run once, different args run multiple times)
- [x] update readme

clarfon: I also think the lisp style is the way to go.
runeimp: I good with either but I'd say the "lowest common denominator" might be the Fortran style. Most languages I've dealt with handle functions in the same manner that you have Fortran listed. Were as the LISP style is similar to calling functions in a subshell. So if your coming from anything other than LISP (and Scheme?) development or shell scripting the Fortran style will be more natural.

Patch:
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<Recipe<'src>>);
+pub(crate) struct Dependency<'src> {
+  pub(crate) recipe: Rc<Recipe<'src>>,
+  pub(crate) arguments: Vec<Expression<'src>>,
+}
+
+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<String, String>,
     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<String, String>,
-    ran: &mut BTreeSet<&'src str>,
+    ran: &mut BTreeSet<Vec<String>>,
   ) -> 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::<Vec<&str>>();
+        self.run_recipe(context, recipe, &arguments, dotenv, ran)?;
+      }
+    }
+
+    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());
     }
-    recipe.run(context, arguments, dotenv)?;
-    ran.insert(recipe.name());
+
+    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<Name<'src>>> {
     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<UnresolvedDependency<'src>>> {
+    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<String, String>,
+    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<Dependency<'src>>) -> 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<Recipe<'src>>>,
   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<Recipe<'src>>>> {
     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<Recipe<'src>>> {
     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<Dependency> = Vec::new();
+    let mut dependencies: Vec<Rc<Recipe>> = 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/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<Expression<'src>>,
+}
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<Rc<Recipe<'src>>>,
+  ) -> 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,
+    })
+  }
+}


Test patch:
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/tests/integration.rs b/tests/integration.rs
index 41c696d1..d3942ca4 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -1441,41 +1441,62 @@ 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:     duplicate_parameter,
-  justfile: "a foo foo:",
-  args:     ("a"),
+  name:     dependency_takes_arguments_at_least,
+  justfile: "
+    a FOO LUZ='hello':
+    b: a
+  ",
+  args:     ("b"),
   stdout:   "",
-  stderr:   "error: Recipe `a` has duplicate parameter `foo`
+  stderr:   "error: Dependency `a` got 0 arguments but takes at least 1 argument
   |
-1 | a foo foo:
-  |       ^^^
+2 | b: a
+  |    ^
 ",
   status:   EXIT_FAILURE,
 }
 
 test! {
-  name:     duplicate_dependency,
-  justfile: "b:\na: b b",
+  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:",
   args:     ("a"),
   stdout:   "",
-  stderr:   "error: Recipe `a` has duplicate dependency `b`
+  stderr:   "error: Recipe `a` has duplicate parameter `foo`
   |
-2 | a: b b
-  |      ^
+1 | a foo foo:
+  |       ^^^
 ",
   status:   EXIT_FAILURE,
 }
@@ -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,
+}