Let recipes take default arguments (#77)
Looks like this: ```make recipe argument default-argument='default value': echo argument is {{argument}} echo default-argument is {{default-argument}} ``` Thanks @deckarep for the feature request! Fixes #49
This commit is contained in:
parent
6c5f4eea62
commit
886acf2f95
@ -48,9 +48,11 @@ expression : STRING
|
|||||||
| BACKTICK
|
| BACKTICK
|
||||||
| expression '+' expression
|
| expression '+' expression
|
||||||
|
|
||||||
recipe : NAME arguments? ':' dependencies? body?
|
recipe : NAME argument* ':' dependencies? body?
|
||||||
|
|
||||||
arguments : NAME+
|
argument : NAME
|
||||||
|
| NAME '=' STRING
|
||||||
|
| NAME '=' RAW_STRING
|
||||||
|
|
||||||
dependencies : NAME+
|
dependencies : NAME+
|
||||||
|
|
||||||
|
24
README.md
24
README.md
@ -184,6 +184,30 @@ Building my-awesome-project...
|
|||||||
cd my-awesome-project && make
|
cd my-awesome-project && make
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Parameters may have default values:
|
||||||
|
|
||||||
|
```make
|
||||||
|
test target tests='all':
|
||||||
|
@echo 'Testing {{target}}:{{tests}}...'
|
||||||
|
./test --tests {{tests}} {{target}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters with default values may be omitted:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ just test server
|
||||||
|
Testing server:all...
|
||||||
|
./test --tests all server
|
||||||
|
```
|
||||||
|
|
||||||
|
Or supplied:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ just test server unit
|
||||||
|
Testing server:unit...
|
||||||
|
./test --tests unit server
|
||||||
|
```
|
||||||
|
|
||||||
Variables can be exported to recipes as environment variables:
|
Variables can be exported to recipes as environment variables:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
|
@ -862,6 +862,7 @@ foo A B:
|
|||||||
"error: Recipe `foo` got 3 arguments but only takes 2\n",
|
"error: Recipe `foo` got 3 arguments but only takes 2\n",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn argument_mismatch_fewer() {
|
fn argument_mismatch_fewer() {
|
||||||
integration_test(
|
integration_test(
|
||||||
@ -876,6 +877,34 @@ foo A B:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn argument_mismatch_more_with_default() {
|
||||||
|
integration_test(
|
||||||
|
&["foo", "ONE", "TWO", "THREE"],
|
||||||
|
"
|
||||||
|
foo A B='B':
|
||||||
|
echo A:{{A}} B:{{B}}
|
||||||
|
",
|
||||||
|
255,
|
||||||
|
"",
|
||||||
|
"error: Recipe `foo` got 3 arguments but takes at most 2\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn argument_mismatch_fewer_with_default() {
|
||||||
|
integration_test(
|
||||||
|
&["foo", "bar"],
|
||||||
|
"
|
||||||
|
foo A B C='C':
|
||||||
|
echo A:{{A}} B:{{B}} C:{{C}}
|
||||||
|
",
|
||||||
|
255,
|
||||||
|
"",
|
||||||
|
"error: Recipe `foo` got 1 argument but takes at least 2\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_recipe() {
|
fn unknown_recipe() {
|
||||||
integration_test(
|
integration_test(
|
||||||
@ -969,3 +998,76 @@ recipe:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn required_after_default() {
|
||||||
|
integration_test(
|
||||||
|
&[],
|
||||||
|
"bar:\nhello baz arg='foo' bar:",
|
||||||
|
255,
|
||||||
|
"",
|
||||||
|
"error: non-default parameter `bar` follows default parameter
|
||||||
|
|
|
||||||
|
2 | hello baz arg='foo' bar:
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_string_default() {
|
||||||
|
integration_test(
|
||||||
|
&["hello", "ABC"],
|
||||||
|
r#"
|
||||||
|
bar:
|
||||||
|
hello baz arg="XYZ\t\" ":
|
||||||
|
echo '{{baz}}...{{arg}}'
|
||||||
|
"#,
|
||||||
|
0,
|
||||||
|
"ABC...XYZ\t\"\t\n",
|
||||||
|
"echo 'ABC...XYZ\t\"\t'\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_raw_string_default() {
|
||||||
|
integration_test(
|
||||||
|
&["hello", "ABC"],
|
||||||
|
r#"
|
||||||
|
bar:
|
||||||
|
hello baz arg='XYZ\t\" ':
|
||||||
|
echo '{{baz}}...{{arg}}'
|
||||||
|
"#,
|
||||||
|
0,
|
||||||
|
"ABC...XYZ\t\\\"\t\n",
|
||||||
|
"echo 'ABC...XYZ\\t\\\"\t'\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supply_use_default() {
|
||||||
|
integration_test(
|
||||||
|
&["hello", "0", "1"],
|
||||||
|
r#"
|
||||||
|
hello a b='B' c='C':
|
||||||
|
echo {{a}} {{b}} {{c}}
|
||||||
|
"#,
|
||||||
|
0,
|
||||||
|
"0 1 C\n",
|
||||||
|
"echo 0 1 C\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supply_defaults() {
|
||||||
|
integration_test(
|
||||||
|
&["hello", "0", "1", "2"],
|
||||||
|
r#"
|
||||||
|
hello a b='B' c='C':
|
||||||
|
echo {{a}} {{b}} {{c}}
|
||||||
|
"#,
|
||||||
|
0,
|
||||||
|
"0 1 2\n",
|
||||||
|
"echo 0 1 2\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
202
src/lib.rs
202
src/lib.rs
@ -19,6 +19,7 @@ extern crate unicode_width;
|
|||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
|
||||||
use std::{fs, fmt, process, io};
|
use std::{fs, fmt, process, io};
|
||||||
|
use std::ops::Range;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
||||||
@ -57,6 +58,10 @@ fn re(pattern: &str) -> Regex {
|
|||||||
Regex::new(pattern).unwrap()
|
Regex::new(pattern).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn contains<T: PartialOrd>(range: &Range<T>, i: T) -> bool {
|
||||||
|
i >= range.start && i < range.end
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
struct Recipe<'a> {
|
struct Recipe<'a> {
|
||||||
line_number: usize,
|
line_number: usize,
|
||||||
@ -64,11 +69,27 @@ struct Recipe<'a> {
|
|||||||
lines: Vec<Vec<Fragment<'a>>>,
|
lines: Vec<Vec<Fragment<'a>>>,
|
||||||
dependencies: Vec<&'a str>,
|
dependencies: Vec<&'a str>,
|
||||||
dependency_tokens: Vec<Token<'a>>,
|
dependency_tokens: Vec<Token<'a>>,
|
||||||
parameters: Vec<&'a str>,
|
parameters: Vec<Parameter<'a>>,
|
||||||
parameter_tokens: Vec<Token<'a>>,
|
|
||||||
shebang: bool,
|
shebang: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
struct Parameter<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
default: Option<String>,
|
||||||
|
token: Token<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Parameter<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
write!(f, "{}", self.name)?;
|
||||||
|
if let Some(ref default) = self.default {
|
||||||
|
write!(f, r#"="{}""#, default)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum Fragment<'a> {
|
enum Fragment<'a> {
|
||||||
Text{text: Token<'a>},
|
Text{text: Token<'a>},
|
||||||
@ -78,7 +99,7 @@ enum Fragment<'a> {
|
|||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum Expression<'a> {
|
enum Expression<'a> {
|
||||||
Variable{name: &'a str, token: Token<'a>},
|
Variable{name: &'a str, token: Token<'a>},
|
||||||
String{raw: &'a str, cooked: String},
|
String{cooked_string: CookedString<'a>},
|
||||||
Backtick{raw: &'a str, token: Token<'a>},
|
Backtick{raw: &'a str, token: Token<'a>},
|
||||||
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
|
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
|
||||||
}
|
}
|
||||||
@ -116,7 +137,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 {raw, .. } => write!(f, "\"{}\"", raw)?,
|
Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?,
|
||||||
Expression::Variable {name, .. } => write!(f, "{}", name)?,
|
Expression::Variable {name, .. } => write!(f, "{}", name)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -225,6 +246,12 @@ fn run_backtick<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Recipe<'a> {
|
impl<'a> Recipe<'a> {
|
||||||
|
fn argument_range(&self) -> Range<usize> {
|
||||||
|
self.parameters.iter().filter(|p| !p.default.is_some()).count()
|
||||||
|
..
|
||||||
|
self.parameters.len() + 1
|
||||||
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
&self,
|
&self,
|
||||||
arguments: &[&'a str],
|
arguments: &[&'a str],
|
||||||
@ -232,8 +259,14 @@ impl<'a> Recipe<'a> {
|
|||||||
exports: &Set<&'a str>,
|
exports: &Set<&'a str>,
|
||||||
options: &RunOptions,
|
options: &RunOptions,
|
||||||
) -> Result<(), RunError<'a>> {
|
) -> Result<(), RunError<'a>> {
|
||||||
let argument_map = arguments .iter().enumerate()
|
let argument_map = self.parameters.iter().enumerate()
|
||||||
.map(|(i, argument)| (self.parameters[i], *argument)).collect();
|
.map(|(i, parameter)| if i < arguments.len() {
|
||||||
|
(parameter.name, arguments[i])
|
||||||
|
} else if let Some(ref default) = parameter.default {
|
||||||
|
(parameter.name, default.as_str())
|
||||||
|
} else {
|
||||||
|
panic!(); // FIXME internal error
|
||||||
|
}).collect();
|
||||||
|
|
||||||
let mut evaluator = Evaluator {
|
let mut evaluator = Evaluator {
|
||||||
evaluated: Map::new(),
|
evaluated: Map::new(),
|
||||||
@ -407,7 +440,9 @@ fn resolve_recipes<'a>(
|
|||||||
if let Fragment::Expression{ref expression, ..} = *fragment {
|
if let Fragment::Expression{ref expression, ..} = *fragment {
|
||||||
for variable in expression.variables() {
|
for variable in expression.variables() {
|
||||||
let name = variable.lexeme;
|
let name = variable.lexeme;
|
||||||
if !(assignments.contains_key(name) || recipe.parameters.contains(&name)) {
|
let undefined = !assignments.contains_key(name)
|
||||||
|
&& !recipe.parameters.iter().any(|p| p.name == name);
|
||||||
|
if undefined {
|
||||||
// There's a borrow issue here that seems too difficult to solve.
|
// There's a borrow issue here that seems too difficult to solve.
|
||||||
// The error derived from the variable token has too short a lifetime,
|
// The error derived from the variable token has too short a lifetime,
|
||||||
// so we create a new error from its contents, which do live long
|
// so we create a new error from its contents, which do live long
|
||||||
@ -644,7 +679,7 @@ impl<'a, 'b> Evaluator<'a, 'b> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::String{ref cooked, ..} => cooked.clone(),
|
Expression::String{ref cooked_string} => cooked_string.cooked.clone(),
|
||||||
Expression::Backtick{raw, ref token} => {
|
Expression::Backtick{raw, ref token} => {
|
||||||
run_backtick(raw, token, &self.scope, &self.exports, self.quiet)?
|
run_backtick(raw, token, &self.scope, &self.exports, self.quiet)?
|
||||||
}
|
}
|
||||||
@ -683,6 +718,7 @@ enum ErrorKind<'a> {
|
|||||||
MixedLeadingWhitespace{whitespace: &'a str},
|
MixedLeadingWhitespace{whitespace: &'a str},
|
||||||
OuterShebang,
|
OuterShebang,
|
||||||
ParameterShadowsVariable{parameter: &'a str},
|
ParameterShadowsVariable{parameter: &'a str},
|
||||||
|
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
|
||||||
UndefinedVariable{variable: &'a str},
|
UndefinedVariable{variable: &'a str},
|
||||||
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
||||||
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
||||||
@ -729,6 +765,49 @@ fn ticks<T: Display>(ts: &[T]) -> Vec<Tick<T>> {
|
|||||||
ts.iter().map(Tick).collect()
|
ts.iter().map(Tick).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
struct CookedString<'a> {
|
||||||
|
raw: &'a str,
|
||||||
|
cooked: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cook_string<'a>(token: &Token<'a>) -> Result<CookedString<'a>, Error<'a>> {
|
||||||
|
let raw = &token.lexeme[1..token.lexeme.len()-1];
|
||||||
|
|
||||||
|
if let RawString = token.kind {
|
||||||
|
Ok(CookedString{raw: raw, cooked: raw.to_string()})
|
||||||
|
} else if let StringToken = token.kind {
|
||||||
|
let mut cooked = String::new();
|
||||||
|
let mut escape = false;
|
||||||
|
for c in raw.chars() {
|
||||||
|
if escape {
|
||||||
|
match c {
|
||||||
|
'n' => cooked.push('\n'),
|
||||||
|
'r' => cooked.push('\r'),
|
||||||
|
't' => cooked.push('\t'),
|
||||||
|
'\\' => cooked.push('\\'),
|
||||||
|
'"' => cooked.push('"'),
|
||||||
|
other => return Err(token.error(ErrorKind::InvalidEscapeSequence {
|
||||||
|
character: other,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c == '\\' {
|
||||||
|
escape = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cooked.push(c);
|
||||||
|
}
|
||||||
|
Ok(CookedString{raw: raw, cooked: cooked})
|
||||||
|
} else {
|
||||||
|
Err(token.error(ErrorKind::InternalError{
|
||||||
|
message: "cook_string() called on non-string token".to_string()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct And<'a, T: 'a + Display>(&'a [T]);
|
struct And<'a, T: 'a + Display>(&'a [T]);
|
||||||
struct Or <'a, T: 'a + Display>(&'a [T]);
|
struct Or <'a, T: 'a + Display>(&'a [T]);
|
||||||
|
|
||||||
@ -900,6 +979,9 @@ impl<'a> Display for Error<'a> {
|
|||||||
ErrorKind::ParameterShadowsVariable{parameter} => {
|
ErrorKind::ParameterShadowsVariable{parameter} => {
|
||||||
writeln!(f, "parameter `{}` shadows variable of the same name", parameter)?;
|
writeln!(f, "parameter `{}` shadows variable of the same name", parameter)?;
|
||||||
}
|
}
|
||||||
|
ErrorKind::RequiredParameterFollowsDefaultParameter{parameter} => {
|
||||||
|
writeln!(f, "non-default parameter `{}` follows default parameter", parameter)?;
|
||||||
|
}
|
||||||
ErrorKind::MixedLeadingWhitespace{whitespace} => {
|
ErrorKind::MixedLeadingWhitespace{whitespace} => {
|
||||||
writeln!(f,
|
writeln!(f,
|
||||||
"found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
"found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
||||||
@ -1011,11 +1093,13 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
|||||||
return Err(RunError::NonLeadingRecipeWithParameters{recipe: recipe.name});
|
return Err(RunError::NonLeadingRecipeWithParameters{recipe: recipe.name});
|
||||||
}
|
}
|
||||||
let rest = &arguments[1..];
|
let rest = &arguments[1..];
|
||||||
if recipe.parameters.len() != rest.len() {
|
let argument_range = recipe.argument_range();
|
||||||
|
if !contains(&argument_range, rest.len()) {
|
||||||
return Err(RunError::ArgumentCountMismatch {
|
return Err(RunError::ArgumentCountMismatch {
|
||||||
recipe: recipe.name,
|
recipe: recipe.name,
|
||||||
found: rest.len(),
|
found: rest.len(),
|
||||||
expected: recipe.parameters.len(),
|
min: argument_range.start,
|
||||||
|
max: argument_range.end - 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return self.run_recipe(recipe, rest, &scope, &mut ran, options);
|
return self.run_recipe(recipe, rest, &scope, &mut ran, options);
|
||||||
@ -1089,7 +1173,7 @@ impl<'a> Display for Justfile<'a> {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum RunError<'a> {
|
enum RunError<'a> {
|
||||||
ArgumentCountMismatch{recipe: &'a str, found: usize, expected: usize},
|
ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize},
|
||||||
Code{recipe: &'a str, code: i32},
|
Code{recipe: &'a str, code: i32},
|
||||||
InternalError{message: String},
|
InternalError{message: String},
|
||||||
IoError{recipe: &'a str, io_error: io::Error},
|
IoError{recipe: &'a str, io_error: io::Error},
|
||||||
@ -1128,10 +1212,19 @@ impl<'a> Display for RunError<'a> {
|
|||||||
write!(f, "Recipe `{}` takes arguments and so must be the first and only recipe \
|
write!(f, "Recipe `{}` takes arguments and so must be the first and only recipe \
|
||||||
specified on the command line", recipe)?;
|
specified on the command line", recipe)?;
|
||||||
},
|
},
|
||||||
RunError::ArgumentCountMismatch{recipe, found, expected} => {
|
RunError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
if min == max {
|
||||||
|
let expected = min;
|
||||||
write!(f, "Recipe `{}` got {} argument{} but {}takes {}",
|
write!(f, "Recipe `{}` got {} argument{} but {}takes {}",
|
||||||
recipe, found, maybe_s(found),
|
recipe, found, maybe_s(found),
|
||||||
if expected < found { "only " } else { "" }, expected)?;
|
if expected < found { "only " } else { "" }, expected)?;
|
||||||
|
} else if found < min {
|
||||||
|
write!(f, "Recipe `{}` got {} argument{} but takes at least {}",
|
||||||
|
recipe, found, maybe_s(found), min)?;
|
||||||
|
} else if found > max {
|
||||||
|
write!(f, "Recipe `{}` got {} argument{} but takes at most {}",
|
||||||
|
recipe, found, maybe_s(found), max)?;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
RunError::Code{recipe, code} => {
|
RunError::Code{recipe, code} => {
|
||||||
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
||||||
@ -1566,6 +1659,15 @@ 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()
|
||||||
}
|
}
|
||||||
@ -1605,16 +1707,40 @@ impl<'a> Parser<'a> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut parameters = vec![];
|
let mut parsed_parameter_with_default = false;
|
||||||
let mut parameter_tokens = vec![];
|
let mut parameters: Vec<Parameter> = vec![];
|
||||||
while let Some(parameter) = self.accept(Name) {
|
while let Some(parameter) = self.accept(Name) {
|
||||||
if parameters.contains(¶meter.lexeme) {
|
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
||||||
return Err(parameter.error(ErrorKind::DuplicateParameter {
|
return Err(parameter.error(ErrorKind::DuplicateParameter {
|
||||||
recipe: name.lexeme, parameter: parameter.lexeme
|
recipe: name.lexeme, parameter: parameter.lexeme
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
parameters.push(parameter.lexeme);
|
|
||||||
parameter_tokens.push(parameter);
|
let default;
|
||||||
|
if self.accepted(Equals) {
|
||||||
|
if let Some(string) = self.accept_any(&[StringToken, RawString]) {
|
||||||
|
default = Some(cook_string(&string)?.cooked);
|
||||||
|
} else {
|
||||||
|
let unexpected = self.tokens.next().unwrap();
|
||||||
|
return Err(self.unexpected_token(&unexpected, &[StringToken, RawString]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
default = None
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed_parameter_with_default && default.is_none() {
|
||||||
|
return Err(parameter.error(ErrorKind::RequiredParameterFollowsDefaultParameter{
|
||||||
|
parameter: parameter.lexeme,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed_parameter_with_default |= default.is_some();
|
||||||
|
|
||||||
|
parameters.push(Parameter {
|
||||||
|
name: parameter.lexeme,
|
||||||
|
default: default,
|
||||||
|
token: parameter,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(token) = self.expect(Colon) {
|
if let Some(token) = self.expect(Colon) {
|
||||||
@ -1694,7 +1820,6 @@ impl<'a> Parser<'a> {
|
|||||||
dependencies: dependencies,
|
dependencies: dependencies,
|
||||||
dependency_tokens: dependency_tokens,
|
dependency_tokens: dependency_tokens,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
parameter_tokens: parameter_tokens,
|
|
||||||
lines: lines,
|
lines: lines,
|
||||||
shebang: shebang,
|
shebang: shebang,
|
||||||
});
|
});
|
||||||
@ -1702,7 +1827,6 @@ impl<'a> Parser<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn expression(&mut self, interpolation: bool) -> Result<Expression<'a>, Error<'a>> {
|
fn expression(&mut self, interpolation: bool) -> Result<Expression<'a>, Error<'a>> {
|
||||||
let first = self.tokens.next().unwrap();
|
let first = self.tokens.next().unwrap();
|
||||||
let lhs = match first.kind {
|
let lhs = match first.kind {
|
||||||
@ -1711,36 +1835,8 @@ impl<'a> Parser<'a> {
|
|||||||
raw: &first.lexeme[1..first.lexeme.len()-1],
|
raw: &first.lexeme[1..first.lexeme.len()-1],
|
||||||
token: first
|
token: first
|
||||||
},
|
},
|
||||||
RawString => {
|
RawString | StringToken => {
|
||||||
let raw = &first.lexeme[1..first.lexeme.len() - 1];
|
Expression::String{cooked_string: cook_string(&first)?}
|
||||||
Expression::String{raw: raw, cooked: raw.to_string()}
|
|
||||||
}
|
|
||||||
StringToken => {
|
|
||||||
let raw = &first.lexeme[1..first.lexeme.len() - 1];
|
|
||||||
let mut cooked = String::new();
|
|
||||||
let mut escape = false;
|
|
||||||
for c in raw.chars() {
|
|
||||||
if escape {
|
|
||||||
match c {
|
|
||||||
'n' => cooked.push('\n'),
|
|
||||||
'r' => cooked.push('\r'),
|
|
||||||
't' => cooked.push('\t'),
|
|
||||||
'\\' => cooked.push('\\'),
|
|
||||||
'"' => cooked.push('"'),
|
|
||||||
other => return Err(first.error(ErrorKind::InvalidEscapeSequence {
|
|
||||||
character: other,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
escape = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if c == '\\' {
|
|
||||||
escape = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cooked.push(c);
|
|
||||||
}
|
|
||||||
Expression::String{raw: raw, cooked: cooked}
|
|
||||||
}
|
}
|
||||||
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
||||||
};
|
};
|
||||||
@ -1820,10 +1916,10 @@ impl<'a> Parser<'a> {
|
|||||||
resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
||||||
|
|
||||||
for recipe in self.recipes.values() {
|
for recipe in self.recipes.values() {
|
||||||
for parameter in &recipe.parameter_tokens {
|
for parameter in &recipe.parameters {
|
||||||
if self.assignments.contains_key(parameter.lexeme) {
|
if self.assignments.contains_key(parameter.token.lexeme) {
|
||||||
return Err(parameter.error(ErrorKind::ParameterShadowsVariable {
|
return Err(parameter.token.error(ErrorKind::ParameterShadowsVariable {
|
||||||
parameter: parameter.lexeme
|
parameter: parameter.token.lexeme
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
134
src/unit.rs
134
src/unit.rs
@ -255,6 +255,26 @@ fn parse_empty() {
|
|||||||
", "");
|
", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_string_default() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo a="b\t":
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo a="b ":"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_raw_string_default() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo a='b\t':
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo a="b\t":"#);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_export() {
|
fn parse_export() {
|
||||||
parse_summary(r#"
|
parse_summary(r#"
|
||||||
@ -372,6 +392,71 @@ fn missing_colon() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_eol() {
|
||||||
|
let text = "hello arg=\n";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eol},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_eof() {
|
||||||
|
let text = "hello arg=";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(0),
|
||||||
|
kind: ErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eof},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_colon() {
|
||||||
|
let text = "hello arg=:";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Colon},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_backtick() {
|
||||||
|
let text = "hello arg=`hello`";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(7),
|
||||||
|
kind: ErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn required_after_default() {
|
||||||
|
let text = "hello arg='foo' bar:";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 16,
|
||||||
|
line: 0,
|
||||||
|
column: 16,
|
||||||
|
width: Some(3),
|
||||||
|
kind: ErrorKind::RequiredParameterFollowsDefaultParameter{parameter: "bar"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_eol() {
|
fn missing_eol() {
|
||||||
let text = "a b c: z =";
|
let text = "a b c: z =";
|
||||||
@ -614,6 +699,15 @@ fn conjoin_and() {
|
|||||||
assert_eq!("1, 2, 3, and 4", super::And(&[1,2,3,4]).to_string());
|
assert_eq!("1, 2, 3, and 4", super::And(&[1,2,3,4]).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range() {
|
||||||
|
assert!(super::contains(&(0..1), 0));
|
||||||
|
assert!(super::contains(&(10..20), 15));
|
||||||
|
assert!(!super::contains(&(0..0), 0));
|
||||||
|
assert!(!super::contains(&(1..10), 0));
|
||||||
|
assert!(!super::contains(&(1..10), 10));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_recipes() {
|
fn unknown_recipes() {
|
||||||
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() {
|
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() {
|
||||||
@ -778,25 +872,53 @@ a return code:
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_args() {
|
fn missing_some_arguments() {
|
||||||
match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() {
|
match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() {
|
||||||
RunError::ArgumentCountMismatch{recipe, found, expected} => {
|
RunError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(found, 2);
|
assert_eq!(found, 2);
|
||||||
assert_eq!(expected, 3);
|
assert_eq!(min, 3);
|
||||||
|
assert_eq!(max, 3);
|
||||||
},
|
},
|
||||||
other => panic!("expected an code run error, but got: {}", other),
|
other => panic!("expected an code run error, but got: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_default() {
|
fn missing_all_arguments() {
|
||||||
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||||
.run(&["a"], &Default::default()).unwrap_err() {
|
.run(&["a"], &Default::default()).unwrap_err() {
|
||||||
RunError::ArgumentCountMismatch{recipe, found, expected} => {
|
RunError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(found, 0);
|
assert_eq!(found, 0);
|
||||||
assert_eq!(expected, 3);
|
assert_eq!(min, 3);
|
||||||
|
assert_eq!(max, 3);
|
||||||
|
},
|
||||||
|
other => panic!("expected an code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_some_defaults() {
|
||||||
|
match parse_success("a b c d='hello':").run(&["a", "b"], &Default::default()).unwrap_err() {
|
||||||
|
RunError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 1);
|
||||||
|
assert_eq!(min, 2);
|
||||||
|
assert_eq!(max, 3);
|
||||||
|
},
|
||||||
|
other => panic!("expected an code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_all_defaults() {
|
||||||
|
match parse_success("a b c='r' d='h':").run(&["a"], &Default::default()).unwrap_err() {
|
||||||
|
RunError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 0);
|
||||||
|
assert_eq!(min, 1);
|
||||||
|
assert_eq!(max, 3);
|
||||||
},
|
},
|
||||||
other => panic!("expected an code run error, but got: {}", other),
|
other => panic!("expected an code run error, but got: {}", other),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user