Allow arbitrary expressions as default arguments (#400)

This commit is contained in:
Casey Rodarmor 2019-04-11 23:58:08 -07:00 committed by GitHub
parent 12f9428695
commit fe0a6c252c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 505 additions and 170 deletions

View File

@ -63,6 +63,7 @@ value : NAME '(' arguments? ')'
| RAW_STRING | RAW_STRING
| BACKTICK | BACKTICK
| NAME | NAME
| '(' expression ')'
arguments : expression ',' arguments arguments : expression ',' arguments
| expression ','? | expression ','?
@ -70,8 +71,7 @@ arguments : expression ',' arguments
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body? recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
parameter : NAME parameter : NAME
| NAME '=' STRING | NAME '=' value
| NAME '=' RAW_STRING
dependencies : NAME+ dependencies : NAME+

View File

@ -480,7 +480,9 @@ cd my-awesome-project && make
Parameters may have default values: Parameters may have default values:
```make ```make
test target tests='all': default = 'all'
test target tests=default:
@echo 'Testing {{target}}:{{tests}}...' @echo 'Testing {{target}}:{{tests}}...'
./test --tests {{tests}} {{target}} ./test --tests {{tests}} {{target}}
``` ```
@ -501,6 +503,15 @@ Testing server:unit...
./test --tests unit server ./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: The last parameter of a recipe may be variadic, indicated with a `+` before the argument name:
```make ```make

View File

@ -1,5 +1,6 @@
use crate::common::*; use crate::common::*;
#[derive(Debug)]
pub struct Alias<'a> { pub struct Alias<'a> {
pub name: &'a str, pub name: &'a str,
pub target: &'a str, pub target: &'a str,

View File

@ -83,7 +83,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
Ok(()) Ok(())
} }
fn evaluate_expression( pub fn evaluate_expression(
&mut self, &mut self,
expression: &Expression<'a>, expression: &Expression<'a>,
arguments: &BTreeMap<&str, Cow<str>>, arguments: &BTreeMap<&str, Cow<str>>,
@ -120,7 +120,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
}; };
evaluate_function(token, name, &context, &call_arguments) 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 } => { Expression::Backtick { raw, ref token } => {
if self.dry_run { if self.dry_run {
Ok(format!("`{}`", raw)) Ok(format!("`{}`", raw))
@ -131,6 +131,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
Expression::Concatination { ref lhs, ref rhs } => { Expression::Concatination { ref lhs, ref rhs } => {
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?) Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
} }
Expression::Group { ref expression } => self.evaluate_expression(&expression, arguments),
} }
} }

View File

@ -56,7 +56,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
} }
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> { fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
match *expression { match expression {
Expression::Variable { name, ref token } => { Expression::Variable { name, ref token } => {
if self.evaluated.contains(name) { if self.evaluated.contains(name) {
return Ok(()); return Ok(());
@ -83,6 +83,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
self.resolve_expression(rhs)?; self.resolve_expression(rhs)?;
} }
Expression::String { .. } | Expression::Backtick { .. } => {} Expression::String { .. } | Expression::Backtick { .. } => {}
Expression::Group { expression } => self.resolve_expression(expression)?,
} }
Ok(()) Ok(())
} }

View File

@ -3,7 +3,7 @@ use crate::common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct CookedString<'a> { pub struct CookedString<'a> {
pub raw: &'a str, pub raw: &'a str,
pub cooked: String, pub cooked: Cow<'a, str>,
} }
impl<'a> CookedString<'a> { impl<'a> CookedString<'a> {
@ -12,7 +12,7 @@ impl<'a> CookedString<'a> {
if let TokenKind::RawString = token.kind { if let TokenKind::RawString = token.kind {
Ok(CookedString { Ok(CookedString {
cooked: raw.to_string(), cooked: Cow::Borrowed(raw),
raw, raw,
}) })
} else if let TokenKind::StringToken = token.kind { } else if let TokenKind::StringToken = token.kind {
@ -41,7 +41,10 @@ impl<'a> CookedString<'a> {
} }
cooked.push(c); cooked.push(c);
} }
Ok(CookedString { raw, cooked }) Ok(CookedString {
raw,
cooked: Cow::Owned(cooked),
})
} else { } else {
Err(token.error(CompilationErrorKind::Internal { Err(token.error(CompilationErrorKind::Internal {
message: "cook_string() called on non-string token".to_string(), 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),
}
}
}

View File

@ -22,6 +22,9 @@ pub enum Expression<'a> {
name: &'a str, name: &'a str,
token: Token<'a>, token: Token<'a>,
}, },
Group {
expression: Box<Expression<'a>>,
},
} }
impl<'a> Expression<'a> { impl<'a> Expression<'a> {
@ -39,7 +42,7 @@ impl<'a> Display for Expression<'a> {
match *self { match *self {
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?, Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
Expression::Concatination { ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?, 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::Variable { name, .. } => write!(f, "{}", name)?,
Expression::Call { Expression::Call {
name, name,
@ -56,6 +59,7 @@ impl<'a> Display for Expression<'a> {
} }
write!(f, ")")?; write!(f, ")")?;
} }
Expression::Group { ref expression } => write!(f, "({})", expression)?,
} }
Ok(()) Ok(())
} }
@ -71,15 +75,19 @@ impl<'a> Iterator for Variables<'a> {
fn next(&mut self) -> Option<&'a Token<'a>> { fn next(&mut self) -> Option<&'a Token<'a>> {
match self.stack.pop() { match self.stack.pop() {
None None
| Some(&Expression::String { .. }) | Some(Expression::String { .. })
| Some(&Expression::Backtick { .. }) | Some(Expression::Backtick { .. })
| Some(&Expression::Call { .. }) => None, | Some(Expression::Call { .. }) => None,
Some(&Expression::Variable { ref token, .. }) => Some(token), Some(Expression::Variable { token, .. }) => Some(token),
Some(&Expression::Concatination { ref lhs, ref rhs }) => { Some(Expression::Concatination { lhs, rhs }) => {
self.stack.push(lhs); self.stack.push(lhs);
self.stack.push(rhs); self.stack.push(rhs);
self.next() 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> { fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() { match self.stack.pop() {
None None
| Some(&Expression::String { .. }) | Some(Expression::String { .. })
| Some(&Expression::Backtick { .. }) | Some(Expression::Backtick { .. })
| Some(&Expression::Variable { .. }) => None, | Some(Expression::Variable { .. }) => None,
Some(&Expression::Call { Some(Expression::Call {
ref token, token, arguments, ..
ref arguments,
..
}) => Some((token, arguments.len())), }) => Some((token, arguments.len())),
Some(&Expression::Concatination { ref lhs, ref rhs }) => { Some(Expression::Concatination { lhs, rhs }) => {
self.stack.push(lhs); self.stack.push(lhs);
self.stack.push(rhs); self.stack.push(rhs);
self.next() self.next()
} }
Some(Expression::Group { expression }) => {
self.stack.push(expression);
self.next()
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
use crate::common::*; use crate::common::*;
#[derive(Debug)]
pub struct Justfile<'a> { pub struct Justfile<'a> {
pub recipes: BTreeMap<&'a str, Recipe<'a>>, pub recipes: BTreeMap<&'a str, Recipe<'a>>,
pub assignments: BTreeMap<&'a str, Expression<'a>>, pub assignments: BTreeMap<&'a str, Expression<'a>>,

View File

@ -617,6 +617,12 @@ c: b
"#$#$.", "#$#$.",
} }
summary_test! {
multiple_recipes,
"a:\n foo\nb:",
"N:$>^_$<N:.",
}
error_test! { error_test! {
name: tokenize_space_then_tab, name: tokenize_space_then_tab,
input: "a: input: "a:

View File

@ -2,7 +2,7 @@ use crate::common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Parameter<'a> { pub struct Parameter<'a> {
pub default: Option<String>, pub default: Option<Expression<'a>>,
pub name: &'a str, pub name: &'a str,
pub token: Token<'a>, pub token: Token<'a>,
pub variadic: bool, pub variadic: bool,
@ -16,11 +16,7 @@ impl<'a> Display for Parameter<'a> {
} }
write!(f, "{}", color.parameter().paint(self.name))?; write!(f, "{}", color.parameter().paint(self.name))?;
if let Some(ref default) = self.default { if let Some(ref default) = self.default {
let escaped = default write!(f, "={}", color.string().paint(&default.to_string()))?;
.chars()
.flat_map(char::escape_default)
.collect::<String>();;
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
} }
Ok(()) Ok(())
} }

View File

@ -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 { fn accepted(&mut self, kind: TokenKind) -> bool {
self.accept(kind).is_some() self.accept(kind).is_some()
} }
@ -137,12 +128,7 @@ impl<'a> Parser<'a> {
let default; let default;
if self.accepted(Equals) { if self.accepted(Equals) {
if let Some(string) = self.accept_any(&[StringToken, RawString]) { default = Some(self.value()?);
default = Some(CookedString::new(&string)?.cooked);
} else {
let unexpected = self.tokens.next().unwrap();
return Err(self.unexpected_token(&unexpected, &[StringToken, RawString]));
}
} else { } else {
default = None 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( self.recipes.insert(
name.lexeme, name.lexeme,
Recipe { Recipe {
@ -262,9 +252,10 @@ impl<'a> Parser<'a> {
Ok(()) Ok(())
} }
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> { fn value(&mut self) -> CompilationResult<'a, Expression<'a>> {
let first = self.tokens.next().unwrap(); let first = self.tokens.next().unwrap();
let lhs = match first.kind {
match first.kind {
Name => { Name => {
if self.peek(ParenL) { if self.peek(ParenL) {
if let Some(token) = self.expect(ParenL) { if let Some(token) = self.expect(ParenL) {
@ -274,30 +265,46 @@ impl<'a> Parser<'a> {
if let Some(token) = self.expect(ParenR) { if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR])); return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
} }
Expression::Call { Ok(Expression::Call {
name: first.lexeme, name: first.lexeme,
token: first, token: first,
arguments, arguments,
} })
} else { } else {
Expression::Variable { Ok(Expression::Variable {
name: first.lexeme, name: first.lexeme,
token: first, token: first,
} })
} }
} }
Backtick => Expression::Backtick { Backtick => Ok(Expression::Backtick {
raw: &first.lexeme[1..first.lexeme.len() - 1], raw: &first.lexeme[1..first.lexeme.len() - 1],
token: first, token: first,
}, }),
RawString | StringToken => Expression::String { RawString | StringToken => Ok(Expression::String {
cooked_string: CookedString::new(&first)?, 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) { if self.accepted(Plus) {
let rhs = self.expression()?; let rhs = self.expression()?;
Ok(Expression::Concatination { Ok(Expression::Concatination {
lhs: Box::new(lhs), lhs: Box::new(lhs),
rhs: Box::new(rhs), 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)?; RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
for recipe in self.recipes.values() { for recipe in self.recipes.values() {
@ -486,8 +495,6 @@ impl<'a> Parser<'a> {
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?; AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
Ok(Justfile { Ok(Justfile {
recipes: self.recipes, recipes: self.recipes,
assignments: self.assignments, assignments: self.assignments,
@ -513,9 +520,17 @@ mod test {
let actual = format!("{:#}", justfile); let actual = format!("{:#}", justfile);
if actual != expected { if actual != expected {
println!("got:\n\"{}\"\n", actual); println!("got:\n\"{}\"\n", actual);
println!("\texpected:\n\"{}\"", expected); println!("expected:\n\"{}\"", expected);
assert_eq!(actual, 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! { summary_test! {
@ -561,7 +587,7 @@ foo +a="Hello":
"#, "#,
r#"foo +a='Hello':"#, r#"foo +a="Hello":"#,
} }
summary_test! { summary_test! {
@ -572,7 +598,7 @@ foo a='b\t':
"#, "#,
r#"foo a='b\\t':"#, r#"foo a='b\t':"#,
} }
summary_test! { summary_test! {
@ -671,7 +697,7 @@ install:
\t\treturn \t\treturn
\tfi \tfi
", ",
"practicum = \"hello\" "practicum = 'hello'
install: install:
#!/bin/sh #!/bin/sh
@ -765,10 +791,76 @@ x = env_var('foo',)
a: 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"))}}"#,
r#"x = env_var("foo") r#"x = env_var('foo')
a: 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! { compilation_error_test! {
@ -848,7 +940,7 @@ a:
line: 0, line: 0,
column: 10, column: 10,
width: Some(1), width: Some(1),
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eol}, kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eol},
} }
compilation_error_test! { compilation_error_test! {
@ -858,27 +950,7 @@ a:
line: 0, line: 0,
column: 10, column: 10,
width: Some(0), width: Some(0),
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eof}, kind: UnexpectedToken{expected: vec![Name, StringToken], 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},
} }
compilation_error_test! { compilation_error_test! {
@ -1065,4 +1137,33 @@ a:
parse_success(&justfile); 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);
}
} }

View File

@ -86,11 +86,24 @@ impl<'a> Recipe<'a> {
let mut argument_map = BTreeMap::new(); 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; let mut rest = arguments;
for parameter in &self.parameters { for parameter in &self.parameters {
let value = if rest.is_empty() { let value = if rest.is_empty() {
match parameter.default { match parameter.default {
Some(ref default) => Cow::Borrowed(default.as_str()), Some(ref default) => Cow::Owned(evaluator.evaluate_expression(default, &empty())?),
None => { None => {
return Err(RuntimeError::Internal { return Err(RuntimeError::Internal {
message: "missing parameter without default".to_string(), message: "missing parameter without default".to_string(),
@ -109,19 +122,6 @@ impl<'a> Recipe<'a> {
argument_map.insert(parameter.name, value); 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 { if self.shebang {
let mut evaluated_lines = vec![]; let mut evaluated_lines = vec![];
for line in &self.lines { for line in &self.lines {

View File

@ -2,11 +2,23 @@ use crate::common::*;
use CompilationErrorKind::*; 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> { pub struct RecipeResolver<'a: 'b, 'b> {
stack: Vec<&'a str>, stack: Vec<&'a str>,
seen: BTreeSet<&'a str>, seen: BTreeSet<&'a str>,
resolved: BTreeSet<&'a str>, resolved: BTreeSet<&'a str>,
recipes: &'b BTreeMap<&'a str, Recipe<'a>>, recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
text: &'a str,
} }
impl<'a, 'b> RecipeResolver<'a, 'b> { impl<'a, 'b> RecipeResolver<'a, 'b> {
@ -19,6 +31,8 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
seen: empty(), seen: empty(),
stack: empty(), stack: empty(),
resolved: empty(), resolved: empty(),
assignments,
text,
recipes, recipes,
}; };
@ -27,51 +41,26 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
resolver.seen = empty(); 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 recipe in recipes.values() {
for parameter in &recipe.parameters {
if let Some(expression) = &parameter.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 line in &recipe.lines {
for fragment in line { for fragment in line {
if let Fragment::Expression { ref expression, .. } = *fragment { if let Fragment::Expression { ref expression, .. } = *fragment {
for (function, argc) in expression.functions() { for (function, argc) in expression.functions() {
if let Err(error) = resolve_function(function, argc) { resolver.resolve_function(function, argc)?;
return Err(CompilationError {
index: error.index,
line: error.line,
column: error.column,
width: error.width,
kind: UnknownFunction {
function: &text[error.index..error.index + error.width.unwrap()],
},
text,
});
}
} }
for variable in expression.variables() { for variable in expression.variables() {
let name = variable.lexeme; resolver.resolve_variable(variable, &recipe.parameters)?;
let undefined = !assignments.contains_key(name)
&& !recipe.parameters.iter().any(|p| p.name == name);
if undefined {
let error = variable.error(UndefinedVariable { variable: name });
return Err(CompilationError {
index: error.index,
line: error.line,
column: error.column,
width: error.width,
kind: UndefinedVariable {
variable: &text[error.index..error.index + error.width.unwrap()],
},
text,
});
}
} }
} }
} }
@ -81,6 +70,44 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
Ok(()) 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: &self.text[error.index..error.index + error.width.unwrap()],
},
text: self.text,
})
}
fn resolve_variable(
&self,
variable: &Token,
parameters: &[Parameter],
) -> CompilationResult<'a, ()> {
let name = variable.lexeme;
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 {
index: error.index,
line: error.line,
column: error.column,
width: error.width,
kind: UndefinedVariable {
variable: &self.text[error.index..error.index + error.width.unwrap()],
},
text: self.text,
});
}
Ok(())
}
fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> { fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> {
if self.resolved.contains(recipe.name) { if self.resolved.contains(recipe.name) {
return Ok(()); return Ok(());
@ -186,4 +213,24 @@ mod test {
width: Some(3), width: Some(3),
kind: UnknownFunction{function: "bar"}, 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"},
}
} }

View File

@ -18,7 +18,7 @@ use std::{
path::Path, 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> { pub fn summary(path: impl AsRef<Path>) -> Result<Result<Summary, String>, io::Error> {
let path = path.as_ref(); let path = path.as_ref();
@ -46,7 +46,7 @@ impl Summary {
for alias in justfile.aliases.values() { for alias in justfile.aliases.values() {
aliases aliases
.entry(alias.target) .entry(alias.target)
.or_insert(Vec::new()) .or_insert_with(Vec::new)
.push(alias.name.to_string()); .push(alias.name.to_string());
} }
@ -57,7 +57,7 @@ impl Summary {
.map(|(name, recipe)| { .map(|(name, recipe)| {
( (
name.to_string(), name.to_string(),
Recipe::new(recipe, aliases.remove(name).unwrap_or(Vec::new())), Recipe::new(recipe, aliases.remove(name).unwrap_or_default()),
) )
}) })
.collect(), .collect(),
@ -83,6 +83,7 @@ pub struct Recipe {
pub private: bool, pub private: bool,
pub quiet: bool, pub quiet: bool,
pub shebang: bool, pub shebang: bool,
pub parameters: Vec<Parameter>,
} }
impl Recipe { impl Recipe {
@ -93,11 +94,31 @@ impl Recipe {
quiet: recipe.quiet, quiet: recipe.quiet,
dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(), dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(),
lines: recipe.lines.into_iter().map(Line::new).collect(), lines: recipe.lines.into_iter().map(Line::new).collect(),
parameters: recipe.parameters.into_iter().map(Parameter::new).collect(),
aliases, 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)] #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub struct Line { pub struct Line {
pub fragments: Vec<Fragment>, pub fragments: Vec<Fragment>,
@ -184,11 +205,12 @@ impl Expression {
rhs: Box::new(Expression::new(*rhs)), rhs: Box::new(Expression::new(*rhs)),
}, },
String { cooked_string } => Expression::String { String { cooked_string } => Expression::String {
text: cooked_string.cooked, text: cooked_string.cooked.to_string(),
}, },
Variable { name, .. } => Expression::Variable { Variable { name, .. } => Expression::Variable {
name: name.to_owned(), name: name.to_owned(),
}, },
Group { expression } => Expression::new(*expression),
} }
} }
} }

View File

@ -1,8 +1,6 @@
use executable_path::executable_path; use executable_path::executable_path;
use libc::{EXIT_FAILURE, EXIT_SUCCESS}; use libc::{EXIT_FAILURE, EXIT_SUCCESS};
use std::env; use std::{env, fs, process, str};
use std::process;
use std::str;
use tempdir::TempDir; use tempdir::TempDir;
/// Instantiate integration tests for a given test case using /// Instantiate integration tests for a given test case using
@ -93,6 +91,46 @@ fn integration_test(
if failure { if failure {
panic!("test failed"); 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! { integration_test! {
@ -1115,10 +1153,10 @@ a Z="\t z":
_private-recipe: _private-recipe:
"#, "#,
args: ("--list"), args: ("--list"),
stdout: r"Available recipes: stdout: r#"Available recipes:
a Z='\t z' a Z="\t z"
hello a b='B\t' c='C' # this does a thing hello a b='B ' c='C' # this does a thing
", "#,
stderr: "", stderr: "",
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
@ -1138,10 +1176,10 @@ a Z="\t z":
_private-recipe: _private-recipe:
"#, "#,
args: ("--list"), args: ("--list"),
stdout: r"Available recipes: stdout: r#"Available recipes:
a Z='\t z' # something else a Z="\t z" # something else
hello a b='B\t' c='C' # this does a thing hello a b='B ' c='C' # this does a thing
", "#,
stderr: "", stderr: "",
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
@ -1165,11 +1203,11 @@ this-recipe-is-very-very-very-important Z="\t z":
_private-recipe: _private-recipe:
"#, "#,
args: ("--list"), args: ("--list"),
stdout: r"Available recipes: stdout: r#"Available recipes:
hello a b='B\t' c='C' # this does a thing hello a b='B ' c='C' # this does a thing
this-recipe-is-very-very-very-important Z='\t z' # something else this-recipe-is-very-very-very-important Z="\t z" # something else
x a b='B\t' c='C' # this does another thing x a b='B ' c='C' # this does another thing
", "#,
stderr: "", stderr: "",
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
@ -1386,8 +1424,6 @@ b
c c
", ",
stderr: "", stderr: "",
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
@ -1809,8 +1845,8 @@ a B C +D='hello':
args: ("--color", "always", "--list"), args: ("--color", "always", "--list"),
stdout: "Available recipes:\n a \ stdout: "Available recipes:\n a \
\u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\ \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}[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", \u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
stderr: "", stderr: "",
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
@ -1924,3 +1960,94 @@ X = "\'"
"#, "#,
status: EXIT_FAILURE, 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,
}

View File

@ -1,7 +1,6 @@
use executable_path::executable_path; use executable_path::executable_path;
use std::{ use std::{
process::Command, process::Command,
thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tempdir::TempDir; use tempdir::TempDir;
@ -33,7 +32,7 @@ fn interrupt_test(justfile: &str) {
.spawn() .spawn()
.expect("just invocation failed"); .expect("just invocation failed");
thread::sleep(Duration::new(1, 0)); while start.elapsed() < Duration::from_millis(500) {}
kill(child.id()); kill(child.id());
@ -41,11 +40,11 @@ fn interrupt_test(justfile: &str) {
let elapsed = start.elapsed(); let elapsed = start.elapsed();
if elapsed > Duration::new(4, 0) { if elapsed > Duration::from_secs(2) {
panic!("process returned too late: {:?}", elapsed); panic!("process returned too late: {:?}", elapsed);
} }
if elapsed < Duration::new(1, 0) { if elapsed < Duration::from_millis(100) {
panic!("process returned too early : {:?}", elapsed); panic!("process returned too early : {:?}", elapsed);
} }
@ -59,7 +58,7 @@ fn interrupt_shebang() {
" "
default: default:
#!/usr/bin/env sh #!/usr/bin/env sh
sleep 2 sleep 1
", ",
); );
} }
@ -70,7 +69,7 @@ fn interrupt_line() {
interrupt_test( interrupt_test(
" "
default: default:
@sleep 2 @sleep 1
", ",
); );
} }
@ -80,7 +79,7 @@ default:
fn interrupt_backtick() { fn interrupt_backtick() {
interrupt_test( interrupt_test(
" "
foo = `sleep 2` foo = `sleep 1`
default: default:
@echo hello @echo hello