2017-11-16 23:30:08 -08:00
|
|
|
use common::*;
|
|
|
|
|
|
|
|
use itertools;
|
2017-11-17 17:28:06 -08:00
|
|
|
use TokenKind::*;
|
|
|
|
use CompilationErrorKind::*;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
pub struct Parser<'a> {
|
|
|
|
text: &'a str,
|
|
|
|
tokens: itertools::PutBack<vec::IntoIter<Token<'a>>>,
|
|
|
|
recipes: Map<&'a str, Recipe<'a>>,
|
|
|
|
assignments: Map<&'a str, Expression<'a>>,
|
|
|
|
assignment_tokens: Map<&'a str, Token<'a>>,
|
|
|
|
exports: Set<&'a str>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Parser<'a> {
|
2017-11-18 03:36:02 -08:00
|
|
|
pub fn parse(text: &'a str) -> CompilationResult<'a, Justfile> {
|
|
|
|
let tokens = Scanner::scan(text)?;
|
|
|
|
let parser = Parser::new(text, tokens);
|
|
|
|
parser.justfile()
|
|
|
|
}
|
|
|
|
|
2017-11-16 23:30:08 -08:00
|
|
|
pub fn new(text: &'a str, tokens: Vec<Token<'a>>) -> Parser<'a> {
|
|
|
|
Parser {
|
|
|
|
text: text,
|
|
|
|
tokens: itertools::put_back(tokens),
|
|
|
|
recipes: empty(),
|
|
|
|
assignments: empty(),
|
|
|
|
assignment_tokens: empty(),
|
|
|
|
exports: empty(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn peek(&mut self, kind: TokenKind) -> bool {
|
|
|
|
let next = self.tokens.next().unwrap();
|
|
|
|
let result = next.kind == kind;
|
|
|
|
self.tokens.put_back(next);
|
|
|
|
result
|
|
|
|
}
|
|
|
|
|
|
|
|
fn accept(&mut self, kind: TokenKind) -> Option<Token<'a>> {
|
|
|
|
if self.peek(kind) {
|
|
|
|
self.tokens.next()
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn accept_any(&mut self, kinds: &[TokenKind]) -> Option<Token<'a>> {
|
|
|
|
for kind in kinds {
|
|
|
|
if self.peek(*kind) {
|
|
|
|
return self.tokens.next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn accepted(&mut self, kind: TokenKind) -> bool {
|
|
|
|
self.accept(kind).is_some()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn expect(&mut self, kind: TokenKind) -> Option<Token<'a>> {
|
|
|
|
if self.peek(kind) {
|
|
|
|
self.tokens.next();
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
self.tokens.next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn expect_eol(&mut self) -> Option<Token<'a>> {
|
|
|
|
self.accepted(Comment);
|
|
|
|
if self.peek(Eol) {
|
|
|
|
self.accept(Eol);
|
|
|
|
None
|
|
|
|
} else if self.peek(Eof) {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
self.tokens.next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenKind]) -> CompilationError<'a> {
|
2017-11-17 17:28:06 -08:00
|
|
|
found.error(UnexpectedToken {
|
2017-11-16 23:30:08 -08:00
|
|
|
expected: expected.to_vec(),
|
|
|
|
found: found.kind,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn recipe(
|
|
|
|
&mut self,
|
2017-11-17 17:28:06 -08:00
|
|
|
name: &Token<'a>,
|
2017-11-16 23:30:08 -08:00
|
|
|
doc: Option<Token<'a>>,
|
|
|
|
quiet: bool,
|
2017-11-17 17:28:06 -08:00
|
|
|
) -> CompilationResult<'a, ()> {
|
2017-11-16 23:30:08 -08:00
|
|
|
if let Some(recipe) = self.recipes.get(name.lexeme) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(name.error(DuplicateRecipe {
|
2017-11-16 23:30:08 -08:00
|
|
|
recipe: recipe.name,
|
|
|
|
first: recipe.line_number
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut parsed_parameter_with_default = false;
|
|
|
|
let mut parsed_variadic_parameter = false;
|
|
|
|
let mut parameters: Vec<Parameter> = vec![];
|
|
|
|
loop {
|
|
|
|
let plus = self.accept(Plus);
|
|
|
|
|
|
|
|
let parameter = match self.accept(Name) {
|
|
|
|
Some(parameter) => parameter,
|
|
|
|
None => if let Some(plus) = plus {
|
|
|
|
return Err(self.unexpected_token(&plus, &[Name]));
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let variadic = plus.is_some();
|
|
|
|
|
|
|
|
if parsed_variadic_parameter {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(parameter.error(ParameterFollowsVariadicParameter {
|
2017-11-16 23:30:08 -08:00
|
|
|
parameter: parameter.lexeme,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(parameter.error(DuplicateParameter {
|
2017-11-16 23:30:08 -08:00
|
|
|
recipe: name.lexeme, parameter: parameter.lexeme
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
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]));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
default = None
|
|
|
|
}
|
|
|
|
|
|
|
|
if parsed_parameter_with_default && default.is_none() {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(parameter.error(RequiredParameterFollowsDefaultParameter{
|
2017-11-16 23:30:08 -08:00
|
|
|
parameter: parameter.lexeme,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
parsed_parameter_with_default |= default.is_some();
|
|
|
|
parsed_variadic_parameter = variadic;
|
|
|
|
|
|
|
|
parameters.push(Parameter {
|
|
|
|
default: default,
|
|
|
|
name: parameter.lexeme,
|
|
|
|
token: parameter,
|
|
|
|
variadic: variadic,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(token) = self.expect(Colon) {
|
|
|
|
// if we haven't accepted any parameters, an equals
|
|
|
|
// would have been fine as part of an assignment
|
|
|
|
if parameters.is_empty() {
|
|
|
|
return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals]));
|
|
|
|
} else {
|
|
|
|
return Err(self.unexpected_token(&token, &[Name, Plus, Colon]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut dependencies = vec![];
|
|
|
|
let mut dependency_tokens = vec![];
|
|
|
|
while let Some(dependency) = self.accept(Name) {
|
|
|
|
if dependencies.contains(&dependency.lexeme) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(dependency.error(DuplicateDependency {
|
2017-11-16 23:30:08 -08:00
|
|
|
recipe: name.lexeme,
|
|
|
|
dependency: dependency.lexeme
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
dependencies.push(dependency.lexeme);
|
|
|
|
dependency_tokens.push(dependency);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(token) = self.expect_eol() {
|
|
|
|
return Err(self.unexpected_token(&token, &[Name, Eol, Eof]));
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut lines: Vec<Vec<Fragment>> = vec![];
|
|
|
|
let mut shebang = false;
|
|
|
|
|
|
|
|
if self.accepted(Indent) {
|
|
|
|
while !self.accepted(Dedent) {
|
|
|
|
if self.accepted(Eol) {
|
|
|
|
lines.push(vec![]);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if let Some(token) = self.expect(Line) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(token.error(Internal{
|
2017-11-16 23:30:08 -08:00
|
|
|
message: format!("Expected a line but got {}", token.kind)
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
let mut fragments = vec![];
|
|
|
|
|
|
|
|
while !(self.accepted(Eol) || self.peek(Dedent)) {
|
|
|
|
if let Some(token) = self.accept(Text) {
|
|
|
|
if fragments.is_empty() {
|
|
|
|
if lines.is_empty() {
|
|
|
|
if token.lexeme.starts_with("#!") {
|
|
|
|
shebang = true;
|
|
|
|
}
|
|
|
|
} else if !shebang
|
|
|
|
&& !lines.last().and_then(|line| line.last())
|
|
|
|
.map(Fragment::continuation).unwrap_or(false)
|
|
|
|
&& (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t')) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(token.error(ExtraLeadingWhitespace));
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
fragments.push(Fragment::Text{text: token});
|
|
|
|
} else if let Some(token) = self.expect(InterpolationStart) {
|
|
|
|
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
|
|
|
|
} else {
|
|
|
|
fragments.push(Fragment::Expression{
|
|
|
|
expression: self.expression(true)?
|
|
|
|
});
|
|
|
|
if let Some(token) = self.expect(InterpolationEnd) {
|
|
|
|
return Err(self.unexpected_token(&token, &[InterpolationEnd]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lines.push(fragments);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
self.recipes.insert(name.lexeme, Recipe {
|
|
|
|
line_number: name.line,
|
|
|
|
name: name.lexeme,
|
|
|
|
doc: doc.map(|t| t.lexeme[1..].trim()),
|
|
|
|
dependencies: dependencies,
|
|
|
|
dependency_tokens: dependency_tokens,
|
|
|
|
parameters: parameters,
|
|
|
|
private: &name.lexeme[0..1] == "_",
|
|
|
|
lines: lines,
|
|
|
|
shebang: shebang,
|
|
|
|
quiet: quiet,
|
|
|
|
});
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
fn expression(&mut self, interpolation: bool) -> CompilationResult<'a, Expression<'a>> {
|
2017-11-16 23:30:08 -08:00
|
|
|
let first = self.tokens.next().unwrap();
|
|
|
|
let lhs = match first.kind {
|
|
|
|
Name => Expression::Variable {name: first.lexeme, token: first},
|
|
|
|
Backtick => Expression::Backtick {
|
|
|
|
raw: &first.lexeme[1..first.lexeme.len()-1],
|
|
|
|
token: first
|
|
|
|
},
|
|
|
|
RawString | StringToken => {
|
|
|
|
Expression::String{cooked_string: CookedString::new(&first)?}
|
|
|
|
}
|
|
|
|
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
|
|
|
};
|
|
|
|
|
|
|
|
if self.accepted(Plus) {
|
|
|
|
let rhs = self.expression(interpolation)?;
|
|
|
|
Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)})
|
|
|
|
} else if interpolation && self.peek(InterpolationEnd) {
|
|
|
|
Ok(lhs)
|
|
|
|
} else if let Some(token) = self.expect_eol() {
|
|
|
|
if interpolation {
|
|
|
|
return Err(self.unexpected_token(&token, &[Plus, Eol, InterpolationEnd]))
|
|
|
|
} else {
|
|
|
|
Err(self.unexpected_token(&token, &[Plus, Eol]))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Ok(lhs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> {
|
2017-11-16 23:30:08 -08:00
|
|
|
if self.assignments.contains_key(name.lexeme) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(name.error(DuplicateVariable {variable: name.lexeme}));
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
if export {
|
|
|
|
self.exports.insert(name.lexeme);
|
|
|
|
}
|
|
|
|
let expression = self.expression(false)?;
|
|
|
|
self.assignments.insert(name.lexeme, expression);
|
|
|
|
self.assignment_tokens.insert(name.lexeme, name);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
pub fn justfile(mut self) -> CompilationResult<'a, Justfile<'a>> {
|
2017-11-16 23:30:08 -08:00
|
|
|
let mut doc = None;
|
|
|
|
loop {
|
|
|
|
match self.tokens.next() {
|
|
|
|
Some(token) => match token.kind {
|
|
|
|
Eof => break,
|
|
|
|
Eol => {
|
|
|
|
doc = None;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Comment => {
|
|
|
|
if let Some(token) = self.expect_eol() {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(token.error(Internal {
|
2017-11-16 23:30:08 -08:00
|
|
|
message: format!("found comment followed by {}", token.kind),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
doc = Some(token);
|
|
|
|
}
|
|
|
|
At => if let Some(name) = self.accept(Name) {
|
2017-11-17 17:28:06 -08:00
|
|
|
self.recipe(&name, doc, true)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
doc = None;
|
|
|
|
} else {
|
|
|
|
let unexpected = &self.tokens.next().unwrap();
|
|
|
|
return Err(self.unexpected_token(unexpected, &[Name]));
|
|
|
|
},
|
|
|
|
Name => if token.lexeme == "export" {
|
|
|
|
let next = self.tokens.next().unwrap();
|
|
|
|
if next.kind == Name && self.accepted(Equals) {
|
|
|
|
self.assignment(next, true)?;
|
|
|
|
doc = None;
|
|
|
|
} else {
|
|
|
|
self.tokens.put_back(next);
|
2017-11-17 17:28:06 -08:00
|
|
|
self.recipe(&token, doc, false)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
doc = None;
|
|
|
|
}
|
|
|
|
} else if self.accepted(Equals) {
|
|
|
|
self.assignment(token, false)?;
|
|
|
|
doc = None;
|
|
|
|
} else {
|
2017-11-17 17:28:06 -08:00
|
|
|
self.recipe(&token, doc, false)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
doc = None;
|
|
|
|
},
|
|
|
|
_ => return Err(self.unexpected_token(&token, &[Name, At])),
|
|
|
|
},
|
|
|
|
None => return Err(CompilationError {
|
|
|
|
text: self.text,
|
|
|
|
index: 0,
|
|
|
|
line: 0,
|
|
|
|
column: 0,
|
|
|
|
width: None,
|
2017-11-17 17:28:06 -08:00
|
|
|
kind: Internal {
|
2017-11-16 23:30:08 -08:00
|
|
|
message: "unexpected end of token stream".to_string()
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(token) = self.tokens.next() {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(token.error(Internal {
|
2017-11-16 23:30:08 -08:00
|
|
|
message: format!("unexpected token remaining after parsing completed: {:?}", token.kind)
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:18:04 -08:00
|
|
|
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
for recipe in self.recipes.values() {
|
|
|
|
for parameter in &recipe.parameters {
|
|
|
|
if self.assignments.contains_key(parameter.token.lexeme) {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(parameter.token.error(ParameterShadowsVariable {
|
2017-11-16 23:30:08 -08:00
|
|
|
parameter: parameter.token.lexeme
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for dependency in &recipe.dependency_tokens {
|
|
|
|
if !self.recipes[dependency.lexeme].parameters.is_empty() {
|
2017-11-17 17:28:06 -08:00
|
|
|
return Err(dependency.error(DependencyHasParameters {
|
2017-11-16 23:30:08 -08:00
|
|
|
recipe: recipe.name,
|
|
|
|
dependency: dependency.lexeme,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:18:04 -08:00
|
|
|
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
Ok(Justfile {
|
|
|
|
recipes: self.recipes,
|
|
|
|
assignments: self.assignments,
|
|
|
|
exports: self.exports,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
use brev;
|
|
|
|
use testing::parse_success;
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
macro_rules! summary_test {
|
|
|
|
($name:ident, $input:expr, $expected:expr $(,)*) => {
|
|
|
|
#[test]
|
|
|
|
fn $name() {
|
|
|
|
let input = $input;
|
|
|
|
let expected = $expected;
|
|
|
|
let justfile = parse_success(input);
|
|
|
|
let actual = format!("{:#}", justfile);
|
|
|
|
if actual != expected {
|
|
|
|
println!("got:\n\"{}\"\n", actual);
|
|
|
|
println!("\texpected:\n\"{}\"", expected);
|
|
|
|
assert_eq!(actual, expected);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_empty,
|
2017-11-17 17:28:06 -08:00
|
|
|
"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
# hello
|
|
|
|
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
",
|
|
|
|
"",
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_string_default,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
foo a="b\t":
|
|
|
|
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
"#,
|
|
|
|
r#"foo a='b\t':"#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_variadic,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
foo +a:
|
|
|
|
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
"#,
|
|
|
|
r#"foo +a:"#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_variadic_string_default,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
foo +a="Hello":
|
|
|
|
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
"#,
|
|
|
|
r#"foo +a='Hello':"#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_raw_string_default,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
foo a='b\t':
|
|
|
|
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
"#,
|
|
|
|
r#"foo a='b\\t':"#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_export,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"
|
2017-11-16 23:30:08 -08:00
|
|
|
export a = "hello"
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
"#,
|
|
|
|
r#"export a = "hello""#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_complex,
|
2017-11-17 17:28:06 -08:00
|
|
|
"
|
2017-11-16 23:30:08 -08:00
|
|
|
x:
|
|
|
|
y:
|
|
|
|
z:
|
|
|
|
foo = \"xx\"
|
|
|
|
bar = foo
|
|
|
|
goodbye = \"y\"
|
|
|
|
hello a b c : x y z #hello
|
|
|
|
#! blah
|
|
|
|
#blarg
|
|
|
|
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
|
|
|
|
1
|
|
|
|
2
|
|
|
|
3
|
2017-11-17 17:28:06 -08:00
|
|
|
",
|
|
|
|
"bar = foo
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
foo = \"xx\"
|
|
|
|
|
|
|
|
goodbye = \"y\"
|
|
|
|
|
|
|
|
hello a b c: x y z
|
|
|
|
#! blah
|
|
|
|
#blarg
|
|
|
|
{{foo + bar}}abc{{goodbye + \"x\"}}xyz
|
|
|
|
1
|
|
|
|
2
|
|
|
|
3
|
|
|
|
|
|
|
|
x:
|
|
|
|
|
|
|
|
y:
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
z:"
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_shebang,
|
2017-11-17 17:28:06 -08:00
|
|
|
"
|
2017-11-16 23:30:08 -08:00
|
|
|
practicum = 'hello'
|
|
|
|
install:
|
|
|
|
\t#!/bin/sh
|
|
|
|
\tif [[ -f {{practicum}} ]]; then
|
|
|
|
\t\treturn
|
|
|
|
\tfi
|
2017-11-17 17:28:06 -08:00
|
|
|
",
|
|
|
|
"practicum = \"hello\"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
install:
|
|
|
|
#!/bin/sh
|
|
|
|
if [[ -f {{practicum}} ]]; then
|
|
|
|
\treturn
|
2017-11-17 17:28:06 -08:00
|
|
|
fi",
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_simple_shebang,
|
|
|
|
"a:\n #!\n print(1)",
|
|
|
|
"a:\n #!\n print(1)",
|
|
|
|
}
|
|
|
|
|
|
|
|
summary_test! {
|
|
|
|
parse_assignments,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"a = "0"
|
2017-11-16 23:30:08 -08:00
|
|
|
c = a + b + a + b
|
|
|
|
b = "1"
|
|
|
|
"#,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"a = "0"
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
b = "1"
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
c = a + b + a + b"#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_assignment_backticks,
|
2017-11-17 17:28:06 -08:00
|
|
|
"a = `echo hello`
|
2017-11-16 23:30:08 -08:00
|
|
|
c = a + b + a + b
|
|
|
|
b = `echo goodbye`",
|
2017-11-17 17:28:06 -08:00
|
|
|
"a = `echo hello`
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
b = `echo goodbye`
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
c = a + b + a + b",
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parse_interpolation_backticks,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"a:
|
2017-11-16 23:30:08 -08:00
|
|
|
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"a:
|
2017-11-16 23:30:08 -08:00
|
|
|
echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#,
|
2017-11-17 17:28:06 -08:00
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
eof_test,
|
2017-11-17 17:28:06 -08:00
|
|
|
"x:\ny:\nz:\na b c: x y z",
|
|
|
|
"a b c: x y z\n\nx:\n\ny:\n\nz:",
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
string_quote_escape,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"a = "hello\"""#,
|
|
|
|
r#"a = "hello\"""#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
string_escapes,
|
2017-11-17 17:28:06 -08:00
|
|
|
r#"a = "\n\t\r\"\\""#,
|
|
|
|
r#"a = "\n\t\r\"\\""#,
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 23:59:55 -08:00
|
|
|
summary_test! {
|
|
|
|
parameters,
|
2017-11-17 17:28:06 -08:00
|
|
|
"a b c:
|
|
|
|
{{b}} {{c}}",
|
|
|
|
"a b c:
|
|
|
|
{{b}} {{c}}",
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: missing_colon,
|
|
|
|
input: "a b c\nd e f",
|
|
|
|
index: 5,
|
|
|
|
line: 0,
|
|
|
|
column: 5,
|
|
|
|
width: Some(1),
|
|
|
|
kind: UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: missing_default_eol,
|
|
|
|
input: "hello arg=\n",
|
|
|
|
index: 10,
|
|
|
|
line: 0,
|
|
|
|
column: 10,
|
|
|
|
width: Some(1),
|
|
|
|
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eol},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: missing_default_eof,
|
|
|
|
input: "hello arg=",
|
|
|
|
index: 10,
|
|
|
|
line: 0,
|
|
|
|
column: 10,
|
|
|
|
width: Some(0),
|
|
|
|
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eof},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: missing_default_colon,
|
|
|
|
input: "hello arg=:",
|
|
|
|
index: 10,
|
|
|
|
line: 0,
|
|
|
|
column: 10,
|
|
|
|
width: Some(1),
|
|
|
|
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Colon},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
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},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: parameter_after_variadic,
|
|
|
|
input: "foo +a bbb:",
|
|
|
|
index: 7,
|
|
|
|
line: 0,
|
|
|
|
column: 7,
|
|
|
|
width: Some(3),
|
|
|
|
kind: ParameterFollowsVariadicParameter{parameter: "bbb"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: required_after_default,
|
|
|
|
input: "hello arg='foo' bar:",
|
|
|
|
index: 16,
|
|
|
|
line: 0,
|
|
|
|
column: 16,
|
|
|
|
width: Some(3),
|
|
|
|
kind: RequiredParameterFollowsDefaultParameter{parameter: "bar"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: missing_eol,
|
|
|
|
input: "a b c: z =",
|
|
|
|
index: 9,
|
|
|
|
line: 0,
|
|
|
|
column: 9,
|
|
|
|
width: Some(1),
|
|
|
|
kind: UnexpectedToken{expected: vec![Name, Eol, Eof], found: Equals},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: duplicate_parameter,
|
|
|
|
input: "a b b:",
|
|
|
|
index: 4,
|
|
|
|
line: 0,
|
|
|
|
column: 4,
|
|
|
|
width: Some(1),
|
|
|
|
kind: DuplicateParameter{recipe: "a", parameter: "b"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: parameter_shadows_varible,
|
|
|
|
input: "foo = \"h\"\na foo:",
|
|
|
|
index: 12,
|
|
|
|
line: 1,
|
|
|
|
column: 2,
|
|
|
|
width: Some(3),
|
|
|
|
kind: ParameterShadowsVariable{parameter: "foo"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: dependency_has_parameters,
|
|
|
|
input: "foo arg:\nb: foo",
|
|
|
|
index: 12,
|
|
|
|
line: 1,
|
|
|
|
column: 3,
|
|
|
|
width: Some(3),
|
|
|
|
kind: DependencyHasParameters{recipe: "b", dependency: "foo"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: duplicate_dependency,
|
|
|
|
input: "a b c: b c z z",
|
|
|
|
index: 13,
|
|
|
|
line: 0,
|
|
|
|
column: 13,
|
|
|
|
width: Some(1),
|
|
|
|
kind: DuplicateDependency{recipe: "a", dependency: "z"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: duplicate_recipe,
|
|
|
|
input: "a:\nb:\na:",
|
|
|
|
index: 6,
|
|
|
|
line: 2,
|
|
|
|
column: 0,
|
|
|
|
width: Some(1),
|
|
|
|
kind: DuplicateRecipe{recipe: "a", first: 0},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: duplicate_variable,
|
|
|
|
input: "a = \"0\"\na = \"0\"",
|
|
|
|
index: 8,
|
|
|
|
line: 1,
|
|
|
|
column: 0,
|
|
|
|
width: Some(1),
|
|
|
|
kind: DuplicateVariable{variable: "a"},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: extra_whitespace,
|
|
|
|
input: "a:\n blah\n blarg",
|
|
|
|
index: 10,
|
|
|
|
line: 2,
|
|
|
|
column: 1,
|
|
|
|
width: Some(6),
|
|
|
|
kind: ExtraLeadingWhitespace,
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: interpolation_outside_of_recipe,
|
|
|
|
input: "{{",
|
|
|
|
index: 0,
|
|
|
|
line: 0,
|
|
|
|
column: 0,
|
|
|
|
width: Some(2),
|
|
|
|
kind: UnexpectedToken{expected: vec![Name, At], found: InterpolationStart},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: unclosed_interpolation_delimiter,
|
|
|
|
input: "a:\n echo {{ foo",
|
|
|
|
index: 15,
|
|
|
|
line: 1,
|
|
|
|
column: 12,
|
|
|
|
width: Some(0),
|
|
|
|
kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent},
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:44:59 -08:00
|
|
|
compilation_error_test! {
|
2017-11-17 23:59:55 -08:00
|
|
|
name: plus_following_parameter,
|
|
|
|
input: "a b c+:",
|
|
|
|
index: 5,
|
|
|
|
line: 0,
|
|
|
|
column: 5,
|
|
|
|
width: Some(1),
|
|
|
|
kind: UnexpectedToken{expected: vec![Name], found: Plus},
|
2017-11-17 17:28:06 -08:00
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
#[test]
|
|
|
|
fn readme_test() {
|
|
|
|
let mut justfiles = vec![];
|
|
|
|
let mut current = None;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
for line in brev::slurp("README.asc").lines() {
|
|
|
|
if let Some(mut justfile) = current {
|
|
|
|
if line == "```" {
|
|
|
|
justfiles.push(justfile);
|
|
|
|
current = None;
|
|
|
|
} else {
|
|
|
|
justfile += line;
|
|
|
|
justfile += "\n";
|
|
|
|
current = Some(justfile);
|
|
|
|
}
|
|
|
|
} else if line == "```make" {
|
|
|
|
current = Some(String::new());
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-17 17:28:06 -08:00
|
|
|
for justfile in justfiles {
|
|
|
|
parse_success(&justfile);
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|