Allow arbitrary expressions as default arguments (#400)
This commit is contained in:
parent
12f9428695
commit
fe0a6c252c
@ -63,6 +63,7 @@ value : NAME '(' arguments? ')'
|
||||
| RAW_STRING
|
||||
| BACKTICK
|
||||
| NAME
|
||||
| '(' expression ')'
|
||||
|
||||
arguments : expression ',' arguments
|
||||
| expression ','?
|
||||
@ -70,8 +71,7 @@ arguments : expression ',' arguments
|
||||
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
||||
|
||||
parameter : NAME
|
||||
| NAME '=' STRING
|
||||
| NAME '=' RAW_STRING
|
||||
| NAME '=' value
|
||||
|
||||
dependencies : NAME+
|
||||
|
||||
|
13
README.adoc
13
README.adoc
@ -480,7 +480,9 @@ cd my-awesome-project && make
|
||||
Parameters may have default values:
|
||||
|
||||
```make
|
||||
test target tests='all':
|
||||
default = 'all'
|
||||
|
||||
test target tests=default:
|
||||
@echo 'Testing {{target}}:{{tests}}...'
|
||||
./test --tests {{tests}} {{target}}
|
||||
```
|
||||
@ -501,6 +503,15 @@ Testing server:unit...
|
||||
./test --tests unit server
|
||||
```
|
||||
|
||||
Default values may be arbitrary expressions, but concatenations must be parenthesized:
|
||||
|
||||
```make
|
||||
arch = "wasm"
|
||||
|
||||
test triple=(arch + "-unknown-unknown"):
|
||||
./test {{triple}}
|
||||
```
|
||||
|
||||
The last parameter of a recipe may be variadic, indicated with a `+` before the argument name:
|
||||
|
||||
```make
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Alias<'a> {
|
||||
pub name: &'a str,
|
||||
pub target: &'a str,
|
||||
|
@ -83,7 +83,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn evaluate_expression(
|
||||
pub fn evaluate_expression(
|
||||
&mut self,
|
||||
expression: &Expression<'a>,
|
||||
arguments: &BTreeMap<&str, Cow<str>>,
|
||||
@ -120,7 +120,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
};
|
||||
evaluate_function(token, name, &context, &call_arguments)
|
||||
}
|
||||
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.clone()),
|
||||
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.to_string()),
|
||||
Expression::Backtick { raw, ref token } => {
|
||||
if self.dry_run {
|
||||
Ok(format!("`{}`", raw))
|
||||
@ -131,6 +131,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
Expression::Concatination { ref lhs, ref rhs } => {
|
||||
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
|
||||
}
|
||||
Expression::Group { ref expression } => self.evaluate_expression(&expression, arguments),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
||||
}
|
||||
|
||||
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
|
||||
match *expression {
|
||||
match expression {
|
||||
Expression::Variable { name, ref token } => {
|
||||
if self.evaluated.contains(name) {
|
||||
return Ok(());
|
||||
@ -83,6 +83,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
||||
self.resolve_expression(rhs)?;
|
||||
}
|
||||
Expression::String { .. } | Expression::Backtick { .. } => {}
|
||||
Expression::Group { expression } => self.resolve_expression(expression)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use crate::common::*;
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct CookedString<'a> {
|
||||
pub raw: &'a str,
|
||||
pub cooked: String,
|
||||
pub cooked: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> CookedString<'a> {
|
||||
@ -12,7 +12,7 @@ impl<'a> CookedString<'a> {
|
||||
|
||||
if let TokenKind::RawString = token.kind {
|
||||
Ok(CookedString {
|
||||
cooked: raw.to_string(),
|
||||
cooked: Cow::Borrowed(raw),
|
||||
raw,
|
||||
})
|
||||
} else if let TokenKind::StringToken = token.kind {
|
||||
@ -41,7 +41,10 @@ impl<'a> CookedString<'a> {
|
||||
}
|
||||
cooked.push(c);
|
||||
}
|
||||
Ok(CookedString { raw, cooked })
|
||||
Ok(CookedString {
|
||||
raw,
|
||||
cooked: Cow::Owned(cooked),
|
||||
})
|
||||
} else {
|
||||
Err(token.error(CompilationErrorKind::Internal {
|
||||
message: "cook_string() called on non-string token".to_string(),
|
||||
@ -49,3 +52,12 @@ impl<'a> CookedString<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for CookedString<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self.cooked {
|
||||
Cow::Borrowed(raw) => write!(f, "'{}'", raw),
|
||||
Cow::Owned(_) => write!(f, "\"{}\"", self.raw),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,9 @@ pub enum Expression<'a> {
|
||||
name: &'a str,
|
||||
token: Token<'a>,
|
||||
},
|
||||
Group {
|
||||
expression: Box<Expression<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Expression<'a> {
|
||||
@ -39,7 +42,7 @@ impl<'a> Display for Expression<'a> {
|
||||
match *self {
|
||||
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
|
||||
Expression::Concatination { ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
|
||||
Expression::String { ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
|
||||
Expression::String { ref cooked_string } => write!(f, "{}", cooked_string)?,
|
||||
Expression::Variable { name, .. } => write!(f, "{}", name)?,
|
||||
Expression::Call {
|
||||
name,
|
||||
@ -56,6 +59,7 @@ impl<'a> Display for Expression<'a> {
|
||||
}
|
||||
write!(f, ")")?;
|
||||
}
|
||||
Expression::Group { ref expression } => write!(f, "({})", expression)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -71,15 +75,19 @@ impl<'a> Iterator for Variables<'a> {
|
||||
fn next(&mut self) -> Option<&'a Token<'a>> {
|
||||
match self.stack.pop() {
|
||||
None
|
||||
| Some(&Expression::String { .. })
|
||||
| Some(&Expression::Backtick { .. })
|
||||
| Some(&Expression::Call { .. }) => None,
|
||||
Some(&Expression::Variable { ref token, .. }) => Some(token),
|
||||
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
|
||||
| Some(Expression::String { .. })
|
||||
| Some(Expression::Backtick { .. })
|
||||
| Some(Expression::Call { .. }) => None,
|
||||
Some(Expression::Variable { token, .. }) => Some(token),
|
||||
Some(Expression::Concatination { lhs, rhs }) => {
|
||||
self.stack.push(lhs);
|
||||
self.stack.push(rhs);
|
||||
self.next()
|
||||
}
|
||||
Some(Expression::Group { expression }) => {
|
||||
self.stack.push(expression);
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,19 +102,21 @@ impl<'a> Iterator for Functions<'a> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
None
|
||||
| Some(&Expression::String { .. })
|
||||
| Some(&Expression::Backtick { .. })
|
||||
| Some(&Expression::Variable { .. }) => None,
|
||||
Some(&Expression::Call {
|
||||
ref token,
|
||||
ref arguments,
|
||||
..
|
||||
| Some(Expression::String { .. })
|
||||
| Some(Expression::Backtick { .. })
|
||||
| Some(Expression::Variable { .. }) => None,
|
||||
Some(Expression::Call {
|
||||
token, arguments, ..
|
||||
}) => Some((token, arguments.len())),
|
||||
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
|
||||
Some(Expression::Concatination { lhs, rhs }) => {
|
||||
self.stack.push(lhs);
|
||||
self.stack.push(rhs);
|
||||
self.next()
|
||||
}
|
||||
Some(Expression::Group { expression }) => {
|
||||
self.stack.push(expression);
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Justfile<'a> {
|
||||
pub recipes: BTreeMap<&'a str, Recipe<'a>>,
|
||||
pub assignments: BTreeMap<&'a str, Expression<'a>>,
|
||||
|
@ -617,6 +617,12 @@ c: b
|
||||
"#$#$.",
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
multiple_recipes,
|
||||
"a:\n foo\nb:",
|
||||
"N:$>^_$<N:.",
|
||||
}
|
||||
|
||||
error_test! {
|
||||
name: tokenize_space_then_tab,
|
||||
input: "a:
|
||||
|
@ -2,7 +2,7 @@ use crate::common::*;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Parameter<'a> {
|
||||
pub default: Option<String>,
|
||||
pub default: Option<Expression<'a>>,
|
||||
pub name: &'a str,
|
||||
pub token: Token<'a>,
|
||||
pub variadic: bool,
|
||||
@ -16,11 +16,7 @@ impl<'a> Display for Parameter<'a> {
|
||||
}
|
||||
write!(f, "{}", color.parameter().paint(self.name))?;
|
||||
if let Some(ref default) = self.default {
|
||||
let escaped = default
|
||||
.chars()
|
||||
.flat_map(char::escape_default)
|
||||
.collect::<String>();;
|
||||
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
|
||||
write!(f, "={}", color.string().paint(&default.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
217
src/parser.rs
217
src/parser.rs
@ -49,15 +49,6 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_any(&mut self, kinds: &[TokenKind]) -> Option<Token<'a>> {
|
||||
for kind in kinds {
|
||||
if self.peek(*kind) {
|
||||
return self.tokens.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn accepted(&mut self, kind: TokenKind) -> bool {
|
||||
self.accept(kind).is_some()
|
||||
}
|
||||
@ -137,12 +128,7 @@ impl<'a> Parser<'a> {
|
||||
|
||||
let default;
|
||||
if self.accepted(Equals) {
|
||||
if let Some(string) = self.accept_any(&[StringToken, RawString]) {
|
||||
default = Some(CookedString::new(&string)?.cooked);
|
||||
} else {
|
||||
let unexpected = self.tokens.next().unwrap();
|
||||
return Err(self.unexpected_token(&unexpected, &[StringToken, RawString]));
|
||||
}
|
||||
default = Some(self.value()?);
|
||||
} else {
|
||||
default = None
|
||||
}
|
||||
@ -243,6 +229,10 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
while lines.last().map(Vec::is_empty).unwrap_or(false) {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
self.recipes.insert(
|
||||
name.lexeme,
|
||||
Recipe {
|
||||
@ -262,9 +252,10 @@ impl<'a> Parser<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
|
||||
fn value(&mut self) -> CompilationResult<'a, Expression<'a>> {
|
||||
let first = self.tokens.next().unwrap();
|
||||
let lhs = match first.kind {
|
||||
|
||||
match first.kind {
|
||||
Name => {
|
||||
if self.peek(ParenL) {
|
||||
if let Some(token) = self.expect(ParenL) {
|
||||
@ -274,30 +265,46 @@ impl<'a> Parser<'a> {
|
||||
if let Some(token) = self.expect(ParenR) {
|
||||
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
|
||||
}
|
||||
Expression::Call {
|
||||
Ok(Expression::Call {
|
||||
name: first.lexeme,
|
||||
token: first,
|
||||
arguments,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Expression::Variable {
|
||||
Ok(Expression::Variable {
|
||||
name: first.lexeme,
|
||||
token: first,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Backtick => Expression::Backtick {
|
||||
Backtick => Ok(Expression::Backtick {
|
||||
raw: &first.lexeme[1..first.lexeme.len() - 1],
|
||||
token: first,
|
||||
},
|
||||
RawString | StringToken => Expression::String {
|
||||
}),
|
||||
RawString | StringToken => Ok(Expression::String {
|
||||
cooked_string: CookedString::new(&first)?,
|
||||
},
|
||||
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
||||
};
|
||||
}),
|
||||
ParenL => {
|
||||
let expression = self.expression()?;
|
||||
|
||||
if let Some(token) = self.expect(ParenR) {
|
||||
return Err(self.unexpected_token(&token, &[ParenR]));
|
||||
}
|
||||
|
||||
Ok(Expression::Group {
|
||||
expression: Box::new(expression),
|
||||
})
|
||||
}
|
||||
_ => Err(self.unexpected_token(&first, &[Name, StringToken])),
|
||||
}
|
||||
}
|
||||
|
||||
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
|
||||
let lhs = self.value()?;
|
||||
|
||||
if self.accepted(Plus) {
|
||||
let rhs = self.expression()?;
|
||||
|
||||
Ok(Expression::Concatination {
|
||||
lhs: Box::new(lhs),
|
||||
rhs: Box::new(rhs),
|
||||
@ -463,6 +470,8 @@ impl<'a> Parser<'a> {
|
||||
}));
|
||||
}
|
||||
|
||||
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
||||
|
||||
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
||||
|
||||
for recipe in self.recipes.values() {
|
||||
@ -486,8 +495,6 @@ impl<'a> Parser<'a> {
|
||||
|
||||
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
|
||||
|
||||
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
||||
|
||||
Ok(Justfile {
|
||||
recipes: self.recipes,
|
||||
assignments: self.assignments,
|
||||
@ -513,9 +520,17 @@ mod test {
|
||||
let actual = format!("{:#}", justfile);
|
||||
if actual != expected {
|
||||
println!("got:\n\"{}\"\n", actual);
|
||||
println!("\texpected:\n\"{}\"", expected);
|
||||
println!("expected:\n\"{}\"", expected);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
println!("Re-parsing...");
|
||||
let reparsed = parse_success(&actual);
|
||||
let redumped = format!("{:#}", reparsed);
|
||||
if redumped != actual {
|
||||
println!("reparsed:\n\"{}\"\n", redumped);
|
||||
println!("expected:\n\"{}\"", actual);
|
||||
assert_eq!(redumped, actual);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -539,7 +554,18 @@ foo a="b\t":
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo a='b\t':"#,
|
||||
r#"foo a="b\t":"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parse_multiple,
|
||||
r#"
|
||||
a:
|
||||
b:
|
||||
"#,
|
||||
r#"a:
|
||||
|
||||
b:"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
@ -561,7 +587,7 @@ foo +a="Hello":
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo +a='Hello':"#,
|
||||
r#"foo +a="Hello":"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
@ -572,7 +598,7 @@ foo a='b\t':
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo a='b\\t':"#,
|
||||
r#"foo a='b\t':"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
@ -671,7 +697,7 @@ install:
|
||||
\t\treturn
|
||||
\tfi
|
||||
",
|
||||
"practicum = \"hello\"
|
||||
"practicum = 'hello'
|
||||
|
||||
install:
|
||||
#!/bin/sh
|
||||
@ -765,10 +791,76 @@ x = env_var('foo',)
|
||||
|
||||
a:
|
||||
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
|
||||
r#"x = env_var("foo")
|
||||
r#"x = env_var('foo')
|
||||
|
||||
a:
|
||||
{{env_var_or_default("foo" + "bar", "baz")}} {{env_var(env_var("baz"))}}"#,
|
||||
{{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parameter_default_string,
|
||||
r#"
|
||||
f x="abc":
|
||||
"#,
|
||||
r#"f x="abc":"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parameter_default_raw_string,
|
||||
r#"
|
||||
f x='abc':
|
||||
"#,
|
||||
r#"f x='abc':"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parameter_default_backtick,
|
||||
r#"
|
||||
f x=`echo hello`:
|
||||
"#,
|
||||
r#"f x=`echo hello`:"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parameter_default_concatination_string,
|
||||
r#"
|
||||
f x=(`echo hello` + "foo"):
|
||||
"#,
|
||||
r#"f x=(`echo hello` + "foo"):"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parameter_default_concatination_variable,
|
||||
r#"
|
||||
x = "10"
|
||||
f y=(`echo hello` + x) +z="foo":
|
||||
"#,
|
||||
r#"x = "10"
|
||||
|
||||
f y=(`echo hello` + x) +z="foo":"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parameter_default_multiple,
|
||||
r#"
|
||||
x = "10"
|
||||
f y=(`echo hello` + x) +z=("foo" + "bar"):
|
||||
"#,
|
||||
r#"x = "10"
|
||||
|
||||
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
concatination_in_group,
|
||||
"x = ('0' + '1')",
|
||||
"x = ('0' + '1')",
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
string_in_group,
|
||||
"x = ('0' )",
|
||||
"x = ('0')",
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
@ -848,7 +940,7 @@ a:
|
||||
line: 0,
|
||||
column: 10,
|
||||
width: Some(1),
|
||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eol},
|
||||
kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eol},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
@ -858,27 +950,7 @@ a:
|
||||
line: 0,
|
||||
column: 10,
|
||||
width: Some(0),
|
||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eof},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: missing_default_colon,
|
||||
input: "hello arg=:",
|
||||
index: 10,
|
||||
line: 0,
|
||||
column: 10,
|
||||
width: Some(1),
|
||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Colon},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: missing_default_backtick,
|
||||
input: "hello arg=`hello`",
|
||||
index: 10,
|
||||
line: 0,
|
||||
column: 10,
|
||||
width: Some(7),
|
||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick},
|
||||
kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eof},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
@ -1065,4 +1137,33 @@ a:
|
||||
parse_success(&justfile);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_recipe_lines() {
|
||||
let text = "a:";
|
||||
let justfile = parse_success(&text);
|
||||
|
||||
assert_eq!(justfile.recipes["a"].lines.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_recipe_lines() {
|
||||
let text = "a:\n foo";
|
||||
let justfile = parse_success(&text);
|
||||
|
||||
assert_eq!(justfile.recipes["a"].lines.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_recipe_lines() {
|
||||
let text = "a:
|
||||
foo
|
||||
|
||||
b:
|
||||
";
|
||||
|
||||
let justfile = parse_success(&text);
|
||||
|
||||
assert_eq!(justfile.recipes["a"].lines.len(), 1);
|
||||
}
|
||||
}
|
||||
|
@ -86,11 +86,24 @@ impl<'a> Recipe<'a> {
|
||||
|
||||
let mut argument_map = BTreeMap::new();
|
||||
|
||||
let mut evaluator = AssignmentEvaluator {
|
||||
assignments: &empty(),
|
||||
dry_run: configuration.dry_run,
|
||||
evaluated: empty(),
|
||||
invocation_directory: context.invocation_directory,
|
||||
overrides: &empty(),
|
||||
quiet: configuration.quiet,
|
||||
scope: &context.scope,
|
||||
shell: configuration.shell,
|
||||
dotenv,
|
||||
exports,
|
||||
};
|
||||
|
||||
let mut rest = arguments;
|
||||
for parameter in &self.parameters {
|
||||
let value = if rest.is_empty() {
|
||||
match parameter.default {
|
||||
Some(ref default) => Cow::Borrowed(default.as_str()),
|
||||
Some(ref default) => Cow::Owned(evaluator.evaluate_expression(default, &empty())?),
|
||||
None => {
|
||||
return Err(RuntimeError::Internal {
|
||||
message: "missing parameter without default".to_string(),
|
||||
@ -109,19 +122,6 @@ impl<'a> Recipe<'a> {
|
||||
argument_map.insert(parameter.name, value);
|
||||
}
|
||||
|
||||
let mut evaluator = AssignmentEvaluator {
|
||||
assignments: &empty(),
|
||||
dry_run: configuration.dry_run,
|
||||
evaluated: empty(),
|
||||
invocation_directory: context.invocation_directory,
|
||||
overrides: &empty(),
|
||||
quiet: configuration.quiet,
|
||||
scope: &context.scope,
|
||||
shell: configuration.shell,
|
||||
dotenv,
|
||||
exports,
|
||||
};
|
||||
|
||||
if self.shebang {
|
||||
let mut evaluated_lines = vec![];
|
||||
for line in &self.lines {
|
||||
|
@ -2,11 +2,23 @@ use crate::common::*;
|
||||
|
||||
use CompilationErrorKind::*;
|
||||
|
||||
// There are borrow issues here that seems too difficult to solve.
|
||||
// The errors derived from the variable token has too short a lifetime,
|
||||
// so we create a new error from its contents, which do live long
|
||||
// enough.
|
||||
//
|
||||
// I suspect the solution here is to give recipes, pieces, and expressions
|
||||
// two lifetime parameters instead of one, with one being the lifetime
|
||||
// of the struct, and the second being the lifetime of the tokens
|
||||
// that it contains.
|
||||
|
||||
pub struct RecipeResolver<'a: 'b, 'b> {
|
||||
stack: Vec<&'a str>,
|
||||
seen: BTreeSet<&'a str>,
|
||||
resolved: BTreeSet<&'a str>,
|
||||
recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
|
||||
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
@ -19,6 +31,8 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
seen: empty(),
|
||||
stack: empty(),
|
||||
resolved: empty(),
|
||||
assignments,
|
||||
text,
|
||||
recipes,
|
||||
};
|
||||
|
||||
@ -27,38 +41,56 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
resolver.seen = empty();
|
||||
}
|
||||
|
||||
// There are borrow issues here that seems too difficult to solve.
|
||||
// The errors derived from the variable token has too short a lifetime,
|
||||
// so we create a new error from its contents, which do live long
|
||||
// enough.
|
||||
//
|
||||
// I suspect the solution here is to give recipes, pieces, and expressions
|
||||
// two lifetime parameters instead of one, with one being the lifetime
|
||||
// of the struct, and the second being the lifetime of the tokens
|
||||
// that it contains.
|
||||
|
||||
for recipe in recipes.values() {
|
||||
for parameter in &recipe.parameters {
|
||||
if let Some(expression) = ¶meter.default {
|
||||
for (function, argc) in expression.functions() {
|
||||
resolver.resolve_function(function, argc)?;
|
||||
}
|
||||
for variable in expression.variables() {
|
||||
resolver.resolve_variable(variable, &[])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for line in &recipe.lines {
|
||||
for fragment in line {
|
||||
if let Fragment::Expression { ref expression, .. } = *fragment {
|
||||
for (function, argc) in expression.functions() {
|
||||
if let Err(error) = resolve_function(function, argc) {
|
||||
return Err(CompilationError {
|
||||
resolver.resolve_function(function, argc)?;
|
||||
}
|
||||
for variable in expression.variables() {
|
||||
resolver.resolve_variable(variable, &recipe.parameters)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_function(&self, function: &Token, argc: usize) -> CompilationResult<'a, ()> {
|
||||
resolve_function(function, argc).map_err(|error| CompilationError {
|
||||
index: error.index,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
width: error.width,
|
||||
kind: UnknownFunction {
|
||||
function: &text[error.index..error.index + error.width.unwrap()],
|
||||
function: &self.text[error.index..error.index + error.width.unwrap()],
|
||||
},
|
||||
text,
|
||||
});
|
||||
text: self.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
for variable in expression.variables() {
|
||||
|
||||
fn resolve_variable(
|
||||
&self,
|
||||
variable: &Token,
|
||||
parameters: &[Parameter],
|
||||
) -> CompilationResult<'a, ()> {
|
||||
let name = variable.lexeme;
|
||||
let undefined = !assignments.contains_key(name)
|
||||
&& !recipe.parameters.iter().any(|p| p.name == name);
|
||||
let undefined =
|
||||
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name == name);
|
||||
if undefined {
|
||||
let error = variable.error(UndefinedVariable { variable: name });
|
||||
return Err(CompilationError {
|
||||
@ -67,16 +99,11 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
column: error.column,
|
||||
width: error.width,
|
||||
kind: UndefinedVariable {
|
||||
variable: &text[error.index..error.index + error.width.unwrap()],
|
||||
variable: &self.text[error.index..error.index + error.width.unwrap()],
|
||||
},
|
||||
text,
|
||||
text: self.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -186,4 +213,24 @@ mod test {
|
||||
width: Some(3),
|
||||
kind: UnknownFunction{function: "bar"},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: unknown_function_in_default,
|
||||
input: "a f=baz():",
|
||||
index: 4,
|
||||
line: 0,
|
||||
column: 4,
|
||||
width: Some(3),
|
||||
kind: UnknownFunction{function: "baz"},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: unknown_variable_in_default,
|
||||
input: "a f=foo:",
|
||||
index: 4,
|
||||
line: 0,
|
||||
column: 4,
|
||||
width: Some(3),
|
||||
kind: UndefinedVariable{variable: "foo"},
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{expression, fragment, justfile::Justfile, parser::Parser, recipe};
|
||||
use crate::{expression, fragment, justfile::Justfile, parameter, parser::Parser, recipe};
|
||||
|
||||
pub fn summary(path: impl AsRef<Path>) -> Result<Result<Summary, String>, io::Error> {
|
||||
let path = path.as_ref();
|
||||
@ -46,7 +46,7 @@ impl Summary {
|
||||
for alias in justfile.aliases.values() {
|
||||
aliases
|
||||
.entry(alias.target)
|
||||
.or_insert(Vec::new())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(alias.name.to_string());
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ impl Summary {
|
||||
.map(|(name, recipe)| {
|
||||
(
|
||||
name.to_string(),
|
||||
Recipe::new(recipe, aliases.remove(name).unwrap_or(Vec::new())),
|
||||
Recipe::new(recipe, aliases.remove(name).unwrap_or_default()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
@ -83,6 +83,7 @@ pub struct Recipe {
|
||||
pub private: bool,
|
||||
pub quiet: bool,
|
||||
pub shebang: bool,
|
||||
pub parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
@ -93,11 +94,31 @@ impl Recipe {
|
||||
quiet: recipe.quiet,
|
||||
dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(),
|
||||
lines: recipe.lines.into_iter().map(Line::new).collect(),
|
||||
parameters: recipe.parameters.into_iter().map(Parameter::new).collect(),
|
||||
aliases,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||
pub struct Parameter {
|
||||
pub variadic: bool,
|
||||
pub name: String,
|
||||
pub default: Option<Expression>,
|
||||
}
|
||||
|
||||
impl Parameter {
|
||||
fn new(parameter: parameter::Parameter) -> Parameter {
|
||||
Parameter {
|
||||
variadic: parameter.variadic,
|
||||
name: parameter.name.to_owned(),
|
||||
default: parameter
|
||||
.default
|
||||
.map(|expression| Expression::new(expression)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||
pub struct Line {
|
||||
pub fragments: Vec<Fragment>,
|
||||
@ -184,11 +205,12 @@ impl Expression {
|
||||
rhs: Box::new(Expression::new(*rhs)),
|
||||
},
|
||||
String { cooked_string } => Expression::String {
|
||||
text: cooked_string.cooked,
|
||||
text: cooked_string.cooked.to_string(),
|
||||
},
|
||||
Variable { name, .. } => Expression::Variable {
|
||||
name: name.to_owned(),
|
||||
},
|
||||
Group { expression } => Expression::new(*expression),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
use executable_path::executable_path;
|
||||
use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||
use std::env;
|
||||
use std::process;
|
||||
use std::str;
|
||||
use std::{env, fs, process, str};
|
||||
use tempdir::TempDir;
|
||||
|
||||
/// Instantiate integration tests for a given test case using
|
||||
@ -93,6 +91,46 @@ fn integration_test(
|
||||
if failure {
|
||||
panic!("test failed");
|
||||
}
|
||||
|
||||
if expected_status == EXIT_SUCCESS {
|
||||
println!("Reparsing...");
|
||||
|
||||
let output = process::Command::new(&executable_path("just"))
|
||||
.current_dir(tmp.path())
|
||||
.arg("--dump")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("dump failed: {}", output.status);
|
||||
}
|
||||
|
||||
let dumped = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let reparsed_path = tmp.path().join("reparsed.just");
|
||||
|
||||
fs::write(&reparsed_path, &dumped).unwrap();
|
||||
|
||||
let output = process::Command::new(&executable_path("just"))
|
||||
.current_dir(tmp.path())
|
||||
.arg("--justfile")
|
||||
.arg(&reparsed_path)
|
||||
.arg("--dump")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("reparse failed: {}", output.status);
|
||||
}
|
||||
|
||||
let reparsed = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
if reparsed != dumped {
|
||||
print!("expected:\n{}", reparsed);
|
||||
print!("got:\n{}", dumped);
|
||||
assert_eq!(reparsed, dumped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
@ -1115,10 +1153,10 @@ a Z="\t z":
|
||||
_private-recipe:
|
||||
"#,
|
||||
args: ("--list"),
|
||||
stdout: r"Available recipes:
|
||||
a Z='\t z'
|
||||
hello a b='B\t' c='C' # this does a thing
|
||||
",
|
||||
stdout: r#"Available recipes:
|
||||
a Z="\t z"
|
||||
hello a b='B ' c='C' # this does a thing
|
||||
"#,
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
@ -1138,10 +1176,10 @@ a Z="\t z":
|
||||
_private-recipe:
|
||||
"#,
|
||||
args: ("--list"),
|
||||
stdout: r"Available recipes:
|
||||
a Z='\t z' # something else
|
||||
hello a b='B\t' c='C' # this does a thing
|
||||
",
|
||||
stdout: r#"Available recipes:
|
||||
a Z="\t z" # something else
|
||||
hello a b='B ' c='C' # this does a thing
|
||||
"#,
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
@ -1165,11 +1203,11 @@ this-recipe-is-very-very-very-important Z="\t z":
|
||||
_private-recipe:
|
||||
"#,
|
||||
args: ("--list"),
|
||||
stdout: r"Available recipes:
|
||||
hello a b='B\t' c='C' # this does a thing
|
||||
this-recipe-is-very-very-very-important Z='\t z' # something else
|
||||
x a b='B\t' c='C' # this does another thing
|
||||
",
|
||||
stdout: r#"Available recipes:
|
||||
hello a b='B ' c='C' # this does a thing
|
||||
this-recipe-is-very-very-very-important Z="\t z" # something else
|
||||
x a b='B ' c='C' # this does another thing
|
||||
"#,
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
@ -1386,8 +1424,6 @@ b
|
||||
|
||||
|
||||
c
|
||||
|
||||
|
||||
",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
@ -1809,8 +1845,8 @@ a B C +D='hello':
|
||||
args: ("--color", "always", "--list"),
|
||||
stdout: "Available recipes:\n a \
|
||||
\u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\
|
||||
\u{1b}[0m\u{1b}[36mD\u{1b}[0m=\'\u{1b}[32mhello\u{1b}[0m\
|
||||
\' \u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
|
||||
\u{1b}[0m\u{1b}[36mD\u{1b}[0m=\u{1b}[32m'hello'\u{1b}[0m \
|
||||
\u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
@ -1924,3 +1960,94 @@ X = "\'"
|
||||
"#,
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: unknown_variable_in_default,
|
||||
justfile: "
|
||||
foo x=bar:
|
||||
",
|
||||
args: (),
|
||||
stdout: "",
|
||||
stderr: r#"error: Variable `bar` not defined
|
||||
|
|
||||
2 | foo x=bar:
|
||||
| ^^^
|
||||
"#,
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: unknown_function_in_default,
|
||||
justfile: "
|
||||
foo x=bar():
|
||||
",
|
||||
args: (),
|
||||
stdout: "",
|
||||
stderr: r#"error: Call to unknown function `bar`
|
||||
|
|
||||
2 | foo x=bar():
|
||||
| ^^^
|
||||
"#,
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: default_string,
|
||||
justfile: "
|
||||
foo x='bar':
|
||||
echo {{x}}
|
||||
",
|
||||
args: (),
|
||||
stdout: "bar\n",
|
||||
stderr: "echo bar\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: default_concatination,
|
||||
justfile: "
|
||||
foo x=(`echo foo` + 'bar'):
|
||||
echo {{x}}
|
||||
",
|
||||
args: (),
|
||||
stdout: "foobar\n",
|
||||
stderr: "echo foobar\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: default_backtick,
|
||||
justfile: "
|
||||
foo x=`echo foo`:
|
||||
echo {{x}}
|
||||
",
|
||||
args: (),
|
||||
stdout: "foo\n",
|
||||
stderr: "echo foo\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: default_variable,
|
||||
justfile: "
|
||||
y = 'foo'
|
||||
foo x=y:
|
||||
echo {{x}}
|
||||
",
|
||||
args: (),
|
||||
stdout: "foo\n",
|
||||
stderr: "echo foo\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: test_os_arch_functions_in_default,
|
||||
justfile: r#"
|
||||
foo a=arch() o=os() f=os_family():
|
||||
echo {{a}} {{o}} {{f}}
|
||||
"#,
|
||||
args: (),
|
||||
stdout: format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
|
||||
stderr: format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use executable_path::executable_path;
|
||||
use std::{
|
||||
process::Command,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tempdir::TempDir;
|
||||
@ -33,7 +32,7 @@ fn interrupt_test(justfile: &str) {
|
||||
.spawn()
|
||||
.expect("just invocation failed");
|
||||
|
||||
thread::sleep(Duration::new(1, 0));
|
||||
while start.elapsed() < Duration::from_millis(500) {}
|
||||
|
||||
kill(child.id());
|
||||
|
||||
@ -41,11 +40,11 @@ fn interrupt_test(justfile: &str) {
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
if elapsed > Duration::new(4, 0) {
|
||||
if elapsed > Duration::from_secs(2) {
|
||||
panic!("process returned too late: {:?}", elapsed);
|
||||
}
|
||||
|
||||
if elapsed < Duration::new(1, 0) {
|
||||
if elapsed < Duration::from_millis(100) {
|
||||
panic!("process returned too early : {:?}", elapsed);
|
||||
}
|
||||
|
||||
@ -59,7 +58,7 @@ fn interrupt_shebang() {
|
||||
"
|
||||
default:
|
||||
#!/usr/bin/env sh
|
||||
sleep 2
|
||||
sleep 1
|
||||
",
|
||||
);
|
||||
}
|
||||
@ -70,7 +69,7 @@ fn interrupt_line() {
|
||||
interrupt_test(
|
||||
"
|
||||
default:
|
||||
@sleep 2
|
||||
@sleep 1
|
||||
",
|
||||
);
|
||||
}
|
||||
@ -80,7 +79,7 @@ default:
|
||||
fn interrupt_backtick() {
|
||||
interrupt_test(
|
||||
"
|
||||
foo = `sleep 2`
|
||||
foo = `sleep 1`
|
||||
|
||||
default:
|
||||
@echo hello
|
||||
|
Loading…
Reference in New Issue
Block a user