From fe0a6c252c4243d16d6161eb828f35a72de682e5 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 11 Apr 2019 23:58:08 -0700 Subject: [PATCH] Allow arbitrary expressions as default arguments (#400) --- GRAMMAR.md | 4 +- README.adoc | 13 ++- src/alias.rs | 1 + src/assignment_evaluator.rs | 5 +- src/assignment_resolver.rs | 3 +- src/cooked_string.rs | 18 ++- src/expression.rs | 38 ++++--- src/justfile.rs | 1 + src/lexer.rs | 6 + src/parameter.rs | 8 +- src/parser.rs | 217 ++++++++++++++++++++++++++---------- src/recipe.rs | 28 ++--- src/recipe_resolver.rs | 123 +++++++++++++------- src/summary.rs | 30 ++++- tests/integration.rs | 167 +++++++++++++++++++++++---- tests/interrupts.rs | 13 +-- 16 files changed, 505 insertions(+), 170 deletions(-) diff --git a/GRAMMAR.md b/GRAMMAR.md index be60fdd..03efab3 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -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+ diff --git a/README.adoc b/README.adoc index f123b89..38f0eb5 100644 --- a/README.adoc +++ b/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 diff --git a/src/alias.rs b/src/alias.rs index ec733da..ad4e826 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -1,5 +1,6 @@ use crate::common::*; +#[derive(Debug)] pub struct Alias<'a> { pub name: &'a str, pub target: &'a str, diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs index 2d1391f..3767b3c 100644 --- a/src/assignment_evaluator.rs +++ b/src/assignment_evaluator.rs @@ -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>, @@ -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), } } diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 8a8f0fc..51e91ad 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -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(()) } diff --git a/src/cooked_string.rs b/src/cooked_string.rs index 0f7f910..279023f 100644 --- a/src/cooked_string.rs +++ b/src/cooked_string.rs @@ -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), + } + } +} diff --git a/src/expression.rs b/src/expression.rs index 18f1511..caa844f 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -22,6 +22,9 @@ pub enum Expression<'a> { name: &'a str, token: Token<'a>, }, + Group { + expression: Box>, + }, } 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 { 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() + } } } } diff --git a/src/justfile.rs b/src/justfile.rs index ee25d11..8905e9c 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -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>>, diff --git a/src/lexer.rs b/src/lexer.rs index 01c6b22..94421e6 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -617,6 +617,12 @@ c: b "#$#$.", } + summary_test! { + multiple_recipes, + "a:\n foo\nb:", + "N:$>^_$ { - pub default: Option, + pub default: Option>, 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::();; - write!(f, r#"='{}'"#, color.string().paint(&escaped))?; + write!(f, "={}", color.string().paint(&default.to_string()))?; } Ok(()) } diff --git a/src/parser.rs b/src/parser.rs index 19b6a92..d61a9fa 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -49,15 +49,6 @@ impl<'a> Parser<'a> { } } - fn accept_any(&mut self, kinds: &[TokenKind]) -> Option> { - 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); + } } diff --git a/src/recipe.rs b/src/recipe.rs index 87383a6..0daf830 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -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 { diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index f9a5ea7..d65b5cd 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -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,51 +41,26 @@ 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 { - index: error.index, - line: error.line, - column: error.column, - width: error.width, - kind: UnknownFunction { - function: &text[error.index..error.index + error.width.unwrap()], - }, - text, - }); - } + resolver.resolve_function(function, argc)?; } for variable in expression.variables() { - let name = variable.lexeme; - 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, - }); - } + resolver.resolve_variable(variable, &recipe.parameters)?; } } } @@ -81,6 +70,44 @@ impl<'a, 'b> RecipeResolver<'a, 'b> { 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, ()> { if self.resolved.contains(recipe.name) { return 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"}, + } } diff --git a/src/summary.rs b/src/summary.rs index 263d0a4..e5756cb 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -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) -> Result, 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, } 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, +} + +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, @@ -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), } } } diff --git a/tests/integration.rs b/tests/integration.rs index aa8af3d..40812ff 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -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, +} diff --git a/tests/interrupts.rs b/tests/interrupts.rs index 33280b5..4127216 100644 --- a/tests/interrupts.rs +++ b/tests/interrupts.rs @@ -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