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
|
sequence : expression ',' sequence
|
||||||
| expression ','?
|
| expression ','?
|
||||||
|
|
||||||
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependency* body?
|
||||||
|
|
||||||
parameter : NAME
|
parameter : NAME
|
||||||
| NAME '=' value
|
| NAME '=' value
|
||||||
|
|
||||||
dependencies : NAME+
|
dependency : NAME
|
||||||
|
| '(' NAME expression* ')
|
||||||
|
|
||||||
body : INDENT line+ DEDENT
|
body : INDENT line+ DEDENT
|
||||||
|
|
||||||
|
14
README.adoc
14
README.adoc
@ -547,9 +547,7 @@ build target:
|
|||||||
cd {{target}} && make
|
cd {{target}} && make
|
||||||
```
|
```
|
||||||
|
|
||||||
Other recipes may not depend on a recipe with parameters.
|
To pass arguments on the command line, put them after the recipe name:
|
||||||
|
|
||||||
To pass arguments, put them after the recipe name:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ just build my-awesome-project
|
$ just build my-awesome-project
|
||||||
@ -557,6 +555,16 @@ Building my-awesome-project...
|
|||||||
cd my-awesome-project && make
|
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:
|
Parameters may have default values:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
|
@ -3,7 +3,7 @@ use crate::common::*;
|
|||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
|
|
||||||
pub(crate) struct Analyzer<'src> {
|
pub(crate) struct Analyzer<'src> {
|
||||||
recipes: Table<'src, Recipe<'src, Name<'src>>>,
|
recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||||
assignments: Table<'src, Assignment<'src>>,
|
assignments: Table<'src, Assignment<'src>>,
|
||||||
aliases: Table<'src, Alias<'src, Name<'src>>>,
|
aliases: Table<'src, Alias<'src, Name<'src>>>,
|
||||||
sets: Table<'src, Set<'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()) {
|
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
|
||||||
return Err(recipe.name.token().error(DuplicateRecipe {
|
return Err(recipe.name.token().error(DuplicateRecipe {
|
||||||
recipe: original.name(),
|
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;
|
let mut continued = false;
|
||||||
for line in &recipe.body {
|
for line in &recipe.body {
|
||||||
if !recipe.shebang && !continued {
|
if !recipe.shebang && !continued {
|
||||||
@ -295,26 +284,6 @@ mod tests {
|
|||||||
kind: ParameterShadowsVariable{parameter: "foo"},
|
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! {
|
analysis_error! {
|
||||||
name: duplicate_recipe,
|
name: duplicate_recipe,
|
||||||
input: "a:\nb:\na:",
|
input: "a:\nb:\na:",
|
||||||
|
@ -62,8 +62,9 @@ pub(crate) use crate::{
|
|||||||
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
||||||
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
|
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
|
||||||
string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token,
|
string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token,
|
||||||
token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
|
token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
||||||
warning::Warning,
|
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
||||||
|
verbosity::Verbosity, warning::Warning,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
@ -89,13 +89,6 @@ impl Display for CompilationError<'_> {
|
|||||||
self.token.line.ordinal(),
|
self.token.line.ordinal(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
DuplicateDependency { recipe, dependency } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` has duplicate dependency `{}`",
|
|
||||||
recipe, dependency
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
DuplicateRecipe { recipe, first } => {
|
DuplicateRecipe { recipe, first } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
@ -114,13 +107,28 @@ impl Display for CompilationError<'_> {
|
|||||||
self.token.line.ordinal(),
|
self.token.line.ordinal(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
DependencyHasParameters { recipe, dependency } => {
|
DependencyArgumentCountMismatch {
|
||||||
writeln!(
|
dependency,
|
||||||
|
found,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
f,
|
f,
|
||||||
"Recipe `{}` depends on `{}` which requires arguments. \
|
"Dependency `{}` got {} {} but takes ",
|
||||||
Dependencies may not require arguments",
|
dependency,
|
||||||
recipe, 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 } => {
|
ParameterShadowsVariable { parameter } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
|
@ -14,18 +14,16 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
variable: &'src str,
|
variable: &'src str,
|
||||||
circle: Vec<&'src str>,
|
circle: Vec<&'src str>,
|
||||||
},
|
},
|
||||||
DependencyHasParameters {
|
DependencyArgumentCountMismatch {
|
||||||
recipe: &'src str,
|
|
||||||
dependency: &'src str,
|
dependency: &'src str,
|
||||||
|
found: usize,
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
},
|
},
|
||||||
DuplicateAlias {
|
DuplicateAlias {
|
||||||
alias: &'src str,
|
alias: &'src str,
|
||||||
first: usize,
|
first: usize,
|
||||||
},
|
},
|
||||||
DuplicateDependency {
|
|
||||||
recipe: &'src str,
|
|
||||||
dependency: &'src str,
|
|
||||||
},
|
|
||||||
DuplicateParameter {
|
DuplicateParameter {
|
||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
parameter: &'src str,
|
parameter: &'src str,
|
||||||
|
@ -1,4 +1,23 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[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)
|
Ok(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn line_evaluator(
|
pub(crate) fn recipe_evaluator(
|
||||||
config: &'run Config,
|
config: &'run Config,
|
||||||
dotenv: &'run BTreeMap<String, String>,
|
dotenv: &'run BTreeMap<String, String>,
|
||||||
scope: &'run Scope<'src, 'run>,
|
scope: &'run Scope<'src, 'run>,
|
||||||
|
@ -5,6 +5,6 @@ use crate::common::*;
|
|||||||
pub(crate) enum Item<'src> {
|
pub(crate) enum Item<'src> {
|
||||||
Alias(Alias<'src, Name<'src>>),
|
Alias(Alias<'src, Name<'src>>),
|
||||||
Assignment(Assignment<'src>),
|
Assignment(Assignment<'src>),
|
||||||
Recipe(Recipe<'src, Name<'src>>),
|
Recipe(UnresolvedRecipe<'src>),
|
||||||
Set(Set<'src>),
|
Set(Set<'src>),
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ impl<'src> Justfile<'src> {
|
|||||||
working_directory,
|
working_directory,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut ran = empty();
|
let mut ran = BTreeSet::new();
|
||||||
for (recipe, arguments) in grouped {
|
for (recipe, arguments) in grouped {
|
||||||
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
|
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
|
||||||
}
|
}
|
||||||
@ -201,17 +201,54 @@ impl<'src> Justfile<'src> {
|
|||||||
&self,
|
&self,
|
||||||
context: &'run RecipeContext<'src, 'run>,
|
context: &'run RecipeContext<'src, 'run>,
|
||||||
recipe: &Recipe<'src>,
|
recipe: &Recipe<'src>,
|
||||||
arguments: &[&'src str],
|
arguments: &[&'run str],
|
||||||
dotenv: &BTreeMap<String, String>,
|
dotenv: &BTreeMap<String, String>,
|
||||||
ran: &mut BTreeSet<&'src str>,
|
ran: &mut BTreeSet<Vec<String>>,
|
||||||
) -> RunResult<'src, ()> {
|
) -> RunResult<'src, ()> {
|
||||||
for Dependency(dependency) in &recipe.dependencies {
|
let scope = Evaluator::evaluate_parameters(
|
||||||
if !ran.contains(dependency.name()) {
|
context.config,
|
||||||
self.run_recipe(context, dependency, &[], dotenv, ran)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,8 @@ mod table;
|
|||||||
mod thunk;
|
mod thunk;
|
||||||
mod token;
|
mod token;
|
||||||
mod token_kind;
|
mod token_kind;
|
||||||
|
mod unresolved_dependency;
|
||||||
|
mod unresolved_recipe;
|
||||||
mod use_color;
|
mod use_color;
|
||||||
mod variables;
|
mod variables;
|
||||||
mod verbosity;
|
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> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
let mut t = Tree::atom("recipe");
|
let mut t = Tree::atom("recipe");
|
||||||
|
|
||||||
@ -111,14 +111,19 @@ impl<'src> Node<'src> for Recipe<'src, Name<'src>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !self.dependencies.is_empty() {
|
if !self.dependencies.is_empty() {
|
||||||
t = t.push(
|
let mut dependencies = Tree::atom("deps");
|
||||||
Tree::atom("deps").extend(
|
|
||||||
self
|
for dependency in &self.dependencies {
|
||||||
.dependencies
|
let mut d = Tree::atom(dependency.recipe.lexeme());
|
||||||
.iter()
|
|
||||||
.map(|dependency| dependency.lexeme()),
|
for argument in &dependency.arguments {
|
||||||
),
|
d.push_mut(argument.tree());
|
||||||
);
|
}
|
||||||
|
|
||||||
|
dependencies.push_mut(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.push_mut(dependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.body.is_empty() {
|
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>>> {
|
fn accept_name(&mut self) -> CompilationResult<'src, Option<Name<'src>>> {
|
||||||
if self.next_is(Identifier) {
|
if self.next_is(Identifier) {
|
||||||
Ok(Some(self.parse_name()?))
|
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`
|
/// Accept and return `true` if next token is of kind `kind`
|
||||||
fn accepted(&mut self, kind: TokenKind) -> CompilationResult<'src, bool> {
|
fn accepted(&mut self, kind: TokenKind) -> CompilationResult<'src, bool> {
|
||||||
Ok(self.accept(kind)?.is_some())
|
Ok(self.accept(kind)?.is_some())
|
||||||
@ -470,7 +492,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
doc: Option<&'src str>,
|
doc: Option<&'src str>,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
) -> CompilationResult<'src, Recipe<'src, Name<'src>>> {
|
) -> CompilationResult<'src, UnresolvedRecipe<'src>> {
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
|
|
||||||
let mut positional = Vec::new();
|
let mut positional = Vec::new();
|
||||||
@ -521,7 +543,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
let mut dependencies = Vec::new();
|
let mut dependencies = Vec::new();
|
||||||
|
|
||||||
while let Some(dependency) = self.accept_name()? {
|
while let Some(dependency) = self.accept_dependency()? {
|
||||||
dependencies.push(dependency);
|
dependencies.push(dependency);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -934,6 +956,30 @@ mod tests {
|
|||||||
tree: (justfile (recipe foo (deps bar baz))),
|
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! {
|
test! {
|
||||||
name: recipe_line_single,
|
name: recipe_line_single,
|
||||||
text: "foo:\n bar",
|
text: "foo:\n bar",
|
||||||
|
@ -67,8 +67,8 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
pub(crate) fn run<'run>(
|
pub(crate) fn run<'run>(
|
||||||
&self,
|
&self,
|
||||||
context: &RecipeContext<'src, 'run>,
|
context: &RecipeContext<'src, 'run>,
|
||||||
arguments: &[&'src str],
|
|
||||||
dotenv: &BTreeMap<String, String>,
|
dotenv: &BTreeMap<String, String>,
|
||||||
|
scope: Scope<'src, 'run>,
|
||||||
) -> RunResult<'src, ()> {
|
) -> RunResult<'src, ()> {
|
||||||
let config = &context.config;
|
let config = &context.config;
|
||||||
|
|
||||||
@ -82,17 +82,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scope = Evaluator::evaluate_parameters(
|
let mut evaluator = Evaluator::recipe_evaluator(
|
||||||
context.config,
|
|
||||||
dotenv,
|
|
||||||
&self.parameters,
|
|
||||||
arguments,
|
|
||||||
&context.scope,
|
|
||||||
context.settings,
|
|
||||||
context.working_directory,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut evaluator = Evaluator::line_evaluator(
|
|
||||||
context.config,
|
context.config,
|
||||||
dotenv,
|
dotenv,
|
||||||
&scope,
|
&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> {
|
impl<'src, D> Keyed<'src> for Recipe<'src, D> {
|
||||||
fn key(&self) -> &'src str {
|
fn key(&self) -> &'src str {
|
||||||
self.name.lexeme()
|
self.name.lexeme()
|
||||||
@ -342,7 +313,7 @@ impl<'src> Display for Recipe<'src> {
|
|||||||
}
|
}
|
||||||
write!(f, ":")?;
|
write!(f, ":")?;
|
||||||
for dependency in &self.dependencies {
|
for dependency in &self.dependencies {
|
||||||
write!(f, " {}", dependency.0.name())?;
|
write!(f, " {}", dependency)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i, line) in self.body.iter().enumerate() {
|
for (i, line) in self.body.iter().enumerate() {
|
||||||
|
@ -3,15 +3,15 @@ use crate::common::*;
|
|||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
|
|
||||||
pub(crate) struct RecipeResolver<'src: 'run, 'run> {
|
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>>>,
|
resolved_recipes: Table<'src, Rc<Recipe<'src>>>,
|
||||||
assignments: &'run Table<'src, Assignment<'src>>,
|
assignments: &'run Table<'src, Assignment<'src>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
||||||
pub(crate) fn resolve_recipes(
|
pub(crate) fn resolve_recipes(
|
||||||
unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>,
|
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||||
assignments: &'run Table<'src, Assignment<'src>>,
|
assignments: &Table<'src, Assignment<'src>>,
|
||||||
) -> CompilationResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
) -> CompilationResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
||||||
let mut resolver = RecipeResolver {
|
let mut resolver = RecipeResolver {
|
||||||
resolved_recipes: empty(),
|
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 line in &recipe.body {
|
||||||
for fragment in &line.fragments {
|
for fragment in &line.fragments {
|
||||||
if let Fragment::Interpolation { expression, .. } = fragment {
|
if let Fragment::Interpolation { expression, .. } = fragment {
|
||||||
@ -65,7 +73,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
|||||||
fn resolve_recipe(
|
fn resolve_recipe(
|
||||||
&mut self,
|
&mut self,
|
||||||
stack: &mut Vec<&'src str>,
|
stack: &mut Vec<&'src str>,
|
||||||
recipe: Recipe<'src, Name<'src>>,
|
recipe: UnresolvedRecipe<'src>,
|
||||||
) -> CompilationResult<'src, Rc<Recipe<'src>>> {
|
) -> CompilationResult<'src, Rc<Recipe<'src>>> {
|
||||||
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
|
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
|
||||||
return Ok(resolved.clone());
|
return Ok(resolved.clone());
|
||||||
@ -73,53 +81,39 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
|||||||
|
|
||||||
stack.push(recipe.name());
|
stack.push(recipe.name());
|
||||||
|
|
||||||
let mut dependencies: Vec<Dependency> = Vec::new();
|
let mut dependencies: Vec<Rc<Recipe>> = Vec::new();
|
||||||
for dependency in &recipe.dependencies {
|
for dependency in &recipe.dependencies {
|
||||||
let name = dependency.lexeme();
|
let name = dependency.recipe.lexeme();
|
||||||
|
|
||||||
if let Some(resolved) = self.resolved_recipes.get(name) {
|
if let Some(resolved) = self.resolved_recipes.get(name) {
|
||||||
// dependency already resolved
|
// dependency already resolved
|
||||||
if !resolved.parameters.is_empty() {
|
dependencies.push(resolved.clone());
|
||||||
return Err(dependency.error(DependencyHasParameters {
|
|
||||||
recipe: recipe.name(),
|
|
||||||
dependency: name,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies.push(Dependency(resolved.clone()));
|
|
||||||
} else if stack.contains(&name) {
|
} else if stack.contains(&name) {
|
||||||
let first = stack[0];
|
let first = stack[0];
|
||||||
stack.push(first);
|
stack.push(first);
|
||||||
return Err(
|
return Err(
|
||||||
dependency.error(CircularRecipeDependency {
|
dependency.recipe.error(CircularRecipeDependency {
|
||||||
recipe: recipe.name(),
|
recipe: recipe.name(),
|
||||||
circle: stack
|
circle: stack
|
||||||
.iter()
|
.iter()
|
||||||
.skip_while(|name| **name != dependency.lexeme())
|
.skip_while(|name| **name != dependency.recipe.lexeme())
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if let Some(unresolved) = self.unresolved_recipes.remove(name) {
|
} else if let Some(unresolved) = self.unresolved_recipes.remove(name) {
|
||||||
// resolve unresolved dependency
|
// resolve unresolved dependency
|
||||||
if !unresolved.parameters.is_empty() {
|
dependencies.push(self.resolve_recipe(stack, unresolved)?);
|
||||||
return Err(dependency.error(DependencyHasParameters {
|
|
||||||
recipe: recipe.name(),
|
|
||||||
dependency: name,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies.push(Dependency(self.resolve_recipe(stack, unresolved)?));
|
|
||||||
} else {
|
} else {
|
||||||
// dependency is unknown
|
// dependency is unknown
|
||||||
return Err(dependency.error(UnknownDependency {
|
return Err(dependency.recipe.error(UnknownDependency {
|
||||||
recipe: recipe.name(),
|
recipe: recipe.name(),
|
||||||
unknown: name,
|
unknown: name,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolved = Rc::new(recipe.resolve(dependencies));
|
let resolved = Rc::new(recipe.resolve(dependencies)?);
|
||||||
self.resolved_recipes.insert(resolved.clone());
|
self.resolved_recipes.insert(resolved.clone());
|
||||||
stack.pop();
|
stack.pop();
|
||||||
Ok(resolved)
|
Ok(resolved)
|
||||||
@ -189,4 +183,14 @@ mod tests {
|
|||||||
width: 3,
|
width: 3,
|
||||||
kind: UndefinedVariable{variable: "foo"},
|
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 crate::common::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
pub(crate) fn compile(text: &str) -> Justfile {
|
pub(crate) fn compile(text: &str) -> Justfile {
|
||||||
match Compiler::compile(text) {
|
match Compiler::compile(text) {
|
||||||
Ok(justfile) => justfile,
|
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! {
|
test! {
|
||||||
name: dependency_takes_arguments,
|
name: dependency_takes_arguments_exact,
|
||||||
justfile: "b: a\na FOO:",
|
justfile: "
|
||||||
|
a FOO:
|
||||||
|
b: a
|
||||||
|
",
|
||||||
args: ("b"),
|
args: ("b"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Recipe `b` depends on `a` which requires arguments. \
|
stderr: "error: Dependency `a` got 0 arguments but takes 1 argument
|
||||||
Dependencies may not require arguments
|
|
||||||
|
|
|
|
||||||
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,
|
status: EXIT_FAILURE,
|
||||||
@ -1467,19 +1501,6 @@ test! {
|
|||||||
status: EXIT_FAILURE,
|
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! {
|
test! {
|
||||||
name: duplicate_recipe,
|
name: duplicate_recipe,
|
||||||
justfile: "b:\nb:",
|
justfile: "b:\nb:",
|
||||||
@ -2261,3 +2282,141 @@ test! {
|
|||||||
stderr: "echo bar\necho foo\n",
|
stderr: "echo bar\necho foo\n",
|
||||||
shell: false,
|
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