Allow passing arguments to dependencies (#555)
Allow recipes that take parameters to be used as dependencies.
This commit is contained in:
parent
2d3134a91c
commit
0931fa8dbf
@ -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
|
||||
|
||||
|
14
README.adoc
14
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
|
||||
|
@ -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:",
|
||||
|
@ -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
|
||||
|
@ -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!(
|
||||
|
@ -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,
|
||||
|
@ -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, ")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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>),
|
||||
}
|
||||
|
@ -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, 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(())
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
23
src/node.rs
23
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() {
|
||||
|
@ -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",
|
||||
|
@ -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() {
|
||||
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
7
src/unresolved_dependency.rs
Normal file
7
src/unresolved_dependency.rs
Normal file
@ -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>>,
|
||||
}
|
49
src/unresolved_recipe.rs
Normal file
49
src/unresolved_recipe.rs
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
@ -1441,14 +1441,48 @@ 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,
|
||||
@ -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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user