just/src/parser.rs

1192 lines
26 KiB
Rust
Raw Normal View History

use crate::common::*;
2017-11-16 23:30:08 -08:00
2017-11-17 17:28:06 -08:00
use CompilationErrorKind::*;
use TokenKind::*;
2017-11-16 23:30:08 -08:00
pub(crate) struct Parser<'a> {
text: &'a str,
tokens: itertools::PutBackN<vec::IntoIter<Token<'a>>>,
recipes: BTreeMap<&'a str, Recipe<'a>>,
assignments: BTreeMap<&'a str, Expression<'a>>,
assignment_tokens: BTreeMap<&'a str, Token<'a>>,
exports: BTreeSet<&'a str>,
aliases: BTreeMap<&'a str, Alias<'a>>,
alias_tokens: BTreeMap<&'a str, Token<'a>>,
warnings: Vec<Warning<'a>>,
2017-11-16 23:30:08 -08:00
}
impl<'a> Parser<'a> {
pub(crate) fn parse(text: &'a str) -> CompilationResult<'a, Justfile> {
let mut tokens = Lexer::lex(text)?;
tokens.retain(|token| token.kind != Whitespace);
2017-11-18 03:36:02 -08:00
let parser = Parser::new(text, tokens);
parser.justfile()
}
pub(crate) fn new(text: &'a str, tokens: Vec<Token<'a>>) -> Parser<'a> {
2017-11-16 23:30:08 -08:00
Parser {
tokens: itertools::put_back_n(tokens),
recipes: empty(),
assignments: empty(),
2017-11-16 23:30:08 -08:00
assignment_tokens: empty(),
exports: empty(),
aliases: empty(),
alias_tokens: empty(),
warnings: Vec::new(),
2018-03-05 13:21:35 -08:00
text,
2017-11-16 23:30:08 -08:00
}
}
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 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,
2017-11-16 23:30:08 -08:00
})
}
fn recipe(
&mut self,
name: &Token<'a>,
doc: Option<Token<'a>>,
2017-11-16 23:30:08 -08:00
quiet: bool,
2017-11-17 17:28:06 -08:00
) -> CompilationResult<'a, ()> {
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,
2017-11-16 23:30:08 -08:00
}));
}
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;
}
}
2017-11-16 23:30:08 -08:00
};
let variadic = plus.is_some();
if parsed_variadic_parameter {
2017-11-17 17:28:06 -08:00
return Err(parameter.error(ParameterFollowsVariadicParameter {
parameter: parameter.lexeme(),
2017-11-16 23:30:08 -08:00
}));
}
if parameters.iter().any(|p| p.name == parameter.lexeme()) {
2017-11-17 17:28:06 -08:00
return Err(parameter.error(DuplicateParameter {
recipe: name.lexeme(),
parameter: parameter.lexeme(),
2017-11-16 23:30:08 -08:00
}));
}
let default;
if self.accepted(Equals) {
default = Some(self.value()?);
2017-11-16 23:30:08 -08:00
} else {
default = None
}
if parsed_parameter_with_default && default.is_none() {
return Err(parameter.error(RequiredParameterFollowsDefaultParameter {
parameter: parameter.lexeme(),
2017-11-16 23:30:08 -08:00
}));
}
parsed_parameter_with_default |= default.is_some();
parsed_variadic_parameter = variadic;
parameters.push(Parameter {
name: parameter.lexeme(),
token: parameter,
2018-03-05 13:21:35 -08:00
default,
variadic,
2017-11-16 23:30:08 -08:00
});
}
if let Some(token) = self.expect(Colon) {
// if we haven't accepted any parameters, a :=
2017-11-16 23:30:08 -08:00
// would have been fine as part of an assignment
if parameters.is_empty() {
return Err(self.unexpected_token(&token, &[Name, Plus, Colon, ColonEquals]));
2017-11-16 23:30:08 -08:00
} 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 {
recipe: name.lexeme(),
dependency: dependency.lexeme(),
2017-11-16 23:30:08 -08:00
}));
}
dependencies.push(dependency.lexeme());
2017-11-16 23:30:08 -08:00
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) {
return Err(token.error(Internal {
message: format!("Expected a line but got {}", token.kind),
}));
2017-11-16 23:30:08 -08:00
}
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("#!") {
2017-11-16 23:30:08 -08:00
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 });
2017-11-16 23:30:08 -08:00
} 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()?,
2017-11-16 23:30:08 -08:00
});
2017-11-16 23:30:08 -08:00
if let Some(token) = self.expect(InterpolationEnd) {
return Err(self.unexpected_token(&token, &[Plus, InterpolationEnd]));
2017-11-16 23:30:08 -08:00
}
}
}
lines.push(fragments);
}
}
while lines.last().map(Vec::is_empty).unwrap_or(false) {
lines.pop();
}
self.recipes.insert(
name.lexeme(),
Recipe {
line_number: name.line,
name: name.lexeme(),
doc: doc.map(|t| t.lexeme()[1..].trim()),
private: &name.lexeme()[0..1] == "_",
dependencies,
dependency_tokens,
lines,
parameters,
quiet,
shebang,
},
);
2017-11-16 23:30:08 -08:00
Ok(())
}
fn value(&mut self) -> CompilationResult<'a, Expression<'a>> {
2017-11-16 23:30:08 -08:00
let first = self.tokens.next().unwrap();
match first.kind {
Name => {
if self.peek(ParenL) {
if let Some(token) = self.expect(ParenL) {
return Err(self.unexpected_token(&token, &[ParenL]));
}
let arguments = self.arguments()?;
if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[Name, StringCooked, ParenR]));
}
Ok(Expression::Call {
name: first.lexeme(),
token: first,
arguments,
})
} else {
Ok(Expression::Variable {
name: first.lexeme(),
token: first,
})
}
}
Backtick => Ok(Expression::Backtick {
raw: &first.lexeme()[1..first.lexeme().len() - 1],
token: first,
}),
StringRaw | StringCooked => Ok(Expression::String {
cooked_string: StringLiteral::new(&first)?,
}),
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, StringCooked])),
}
}
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
let lhs = self.value()?;
2017-11-16 23:30:08 -08:00
if self.accepted(Plus) {
let rhs = self.expression()?;
Ok(Expression::Concatination {
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
2017-11-16 23:30:08 -08:00
} else {
Ok(lhs)
}
}
fn arguments(&mut self) -> CompilationResult<'a, Vec<Expression<'a>>> {
let mut arguments = Vec::new();
while !self.peek(ParenR) && !self.peek(Eof) && !self.peek(Eol) && !self.peek(InterpolationEnd) {
arguments.push(self.expression()?);
if !self.accepted(Comma) {
if self.peek(ParenR) {
break;
} else {
let next = self.tokens.next().unwrap();
return Err(self.unexpected_token(&next, &[Comma, ParenR]));
}
}
}
Ok(arguments)
}
2017-11-17 17:28:06 -08:00
fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> {
if self.assignments.contains_key(name.lexeme()) {
return Err(name.error(DuplicateVariable {
variable: name.lexeme(),
}));
2017-11-16 23:30:08 -08:00
}
if export {
self.exports.insert(name.lexeme());
2017-11-16 23:30:08 -08:00
}
let expression = self.expression()?;
if let Some(token) = self.expect_eol() {
return Err(self.unexpected_token(&token, &[Plus, Eol]));
}
self.assignments.insert(name.lexeme(), expression);
self.assignment_tokens.insert(name.lexeme(), name);
2017-11-16 23:30:08 -08:00
Ok(())
}
fn alias(&mut self, name: Token<'a>) -> CompilationResult<'a, ()> {
// Make sure alias doesn't already exist
if let Some(alias) = self.aliases.get(name.lexeme()) {
return Err(name.error(DuplicateAlias {
alias: alias.name,
first: alias.line_number,
}));
}
// Make sure the next token is of kind Name and keep it
let target = if let Some(next) = self.accept(Name) {
next.lexeme()
} else {
let unexpected = self.tokens.next().unwrap();
return Err(self.unexpected_token(&unexpected, &[Name]));
};
// Make sure this is where the line or file ends without any unexpected tokens.
if let Some(token) = self.expect_eol() {
return Err(self.unexpected_token(&token, &[Eol, Eof]));
}
self.aliases.insert(
name.lexeme(),
Alias {
name: name.lexeme(),
line_number: name.line,
private: name.lexeme().starts_with('_'),
target,
},
);
self.alias_tokens.insert(name.lexeme(), name);
Ok(())
}
pub(crate) 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) {
self.recipe(&name, doc, true)?;
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.peek(Equals) {
self.warnings.push(Warning::DeprecatedEquals {
equals: self.tokens.next().unwrap(),
});
self.assignment(next, true)?;
doc = None;
} else if next.kind == Name && self.accepted(ColonEquals) {
self.assignment(next, true)?;
doc = None;
} else {
self.tokens.put_back(next);
self.recipe(&token, doc, false)?;
doc = None;
}
} else if token.lexeme() == "alias" {
let next = self.tokens.next().unwrap();
if next.kind == Name && self.peek(Equals) {
self.warnings.push(Warning::DeprecatedEquals {
equals: self.tokens.next().unwrap(),
});
self.alias(next)?;
doc = None;
} else if next.kind == Name && self.accepted(ColonEquals) {
self.alias(next)?;
doc = None;
} else {
self.tokens.put_back(next);
self.recipe(&token, doc, false)?;
doc = None;
}
} else if self.peek(Equals) {
self.warnings.push(Warning::DeprecatedEquals {
equals: self.tokens.next().unwrap(),
});
self.assignment(token, false)?;
doc = None;
} else if self.accepted(ColonEquals) {
self.assignment(token, false)?;
2017-11-16 23:30:08 -08:00
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;
}
}
2017-11-16 23:30:08 -08:00
_ => return Err(self.unexpected_token(&token, &[Name, At])),
},
None => {
return Err(CompilationError {
text: self.text,
offset: 0,
line: 0,
column: 0,
width: 0,
kind: Internal {
message: "unexpected end of token stream".to_string(),
},
});
}
2017-11-16 23:30:08 -08:00
}
}
if let Some(token) = self.tokens.next() {
2017-11-17 17:28:06 -08:00
return Err(token.error(Internal {
message: format!(
"unexpected token remaining after parsing completed: {:?}",
token.kind
),
}));
2017-11-16 23:30:08 -08:00
}
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
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 {
parameter: parameter.token.lexeme(),
2017-11-16 23:30:08 -08:00
}));
}
}
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-16 23:30:08 -08:00
}));
}
}
}
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
2017-11-16 23:30:08 -08:00
Ok(Justfile {
recipes: self.recipes,
2017-11-16 23:30:08 -08:00
assignments: self.assignments,
exports: self.exports,
aliases: self.aliases,
warnings: self.warnings,
2017-11-16 23:30:08 -08:00
})
}
}
#[cfg(test)]
mod test {
use super::*;
2019-04-19 02:17:43 -07:00
use crate::testing::parse;
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
macro_rules! parse_test {
2017-11-17 17:28:06 -08:00
($name:ident, $input:expr, $expected:expr $(,)*) => {
#[test]
fn $name() {
let input = $input;
let expected = $expected;
2019-04-19 02:17:43 -07:00
let justfile = parse(input);
2017-11-17 17:28:06 -08:00
let actual = format!("{:#}", justfile);
2019-04-19 02:17:43 -07:00
use pretty_assertions::assert_eq;
assert_eq!(actual, expected);
println!("Re-parsing...");
2019-04-19 02:17:43 -07:00
let reparsed = parse(&actual);
let redumped = format!("{:#}", reparsed);
2019-04-19 02:17:43 -07:00
assert_eq!(redumped, actual);
2017-11-17 17:28:06 -08:00
}
};
2017-11-16 23:30:08 -08:00
}
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
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
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
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":"#,
}
2019-04-19 02:17:43 -07:00
parse_test! {
parse_multiple,
r#"
a:
b:
"#,
r#"a:
b:"#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
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
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
parse_variadic_string_default,
2017-11-17 17:28:06 -08:00
r#"
2017-11-16 23:30:08 -08:00
foo +a="Hello":
"#,
r#"foo +a="Hello":"#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
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':
"#,
r#"foo a='b\t':"#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
parse_export,
2017-11-17 17:28:06 -08:00
r#"
export a := "hello"
2017-11-16 23:30:08 -08:00
"#,
r#"export a := "hello""#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
parse_alias_after_target,
r#"
foo:
echo a
alias f := foo
"#,
r#"alias f := foo
foo:
echo a"#
}
2019-04-19 02:17:43 -07:00
parse_test! {
parse_alias_before_target,
r#"
alias f := foo
foo:
echo a
"#,
r#"alias f := foo
foo:
echo a"#
}
2019-04-19 02:17:43 -07:00
parse_test! {
parse_alias_with_comment,
r#"
alias f := foo #comment
foo:
echo a
"#,
r#"alias f := foo
foo:
echo a"#
}
2019-04-19 02:17:43 -07:00
parse_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\"
2017-11-16 23:30:08 -08:00
hello a b c : x y z #hello
#! blah
#blarg
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
1
2
3
",
"bar := foo
2017-11-16 23:30:08 -08:00
foo := \"xx\"
2017-11-16 23:30:08 -08:00
goodbye := \"y\"
2017-11-16 23:30:08 -08:00
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
2019-04-19 02:17:43 -07:00
parse_test! {
parse_shebang,
2017-11-17 17:28:06 -08:00
"
practicum := 'hello'
2017-11-16 23:30:08 -08:00
install:
\t#!/bin/sh
\tif [[ -f {{practicum}} ]]; then
\t\treturn
\tfi
",
"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
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
parse_simple_shebang,
"a:\n #!\n print(1)",
"a:\n #!\n print(1)",
}
2019-04-19 02:17:43 -07:00
parse_test! {
parse_assignments,
r#"a := "0"
c := a + b + a + b
b := "1"
2017-11-16 23:30:08 -08:00
"#,
r#"a := "0"
2017-11-16 23:30:08 -08:00
b := "1"
2017-11-16 23:30:08 -08:00
c := a + b + a + b"#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
parse_assignment_backticks,
"a := `echo hello`
c := a + b + a + b
b := `echo goodbye`",
"a := `echo hello`
2017-11-16 23:30:08 -08:00
b := `echo goodbye`
2017-11-16 23:30:08 -08:00
c := a + b + a + b",
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
parse_interpolation_backticks,
2017-11-17 17:28:06 -08:00
r#"a:
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
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
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
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
string_quote_escape,
r#"a := "hello\"""#,
r#"a := "hello\"""#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_test! {
2017-11-17 23:59:55 -08:00
string_escapes,
r#"a := "\n\t\r\"\\""#,
r#"a := "\n\t\r\"\\""#,
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
parse_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
2019-04-19 02:17:43 -07:00
parse_test! {
unary_functions,
"
x := arch()
a:
{{os()}} {{os_family()}}",
"x := arch()
a:
{{os()}} {{os_family()}}",
}
2019-04-19 02:17:43 -07:00
parse_test! {
env_functions,
r#"
x := env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
r#"x := env_var('foo')
a:
{{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
}
2019-04-19 02:17:43 -07:00
parse_test! {
parameter_default_string,
r#"
f x="abc":
"#,
r#"f x="abc":"#,
}
2019-04-19 02:17:43 -07:00
parse_test! {
parameter_default_raw_string,
r#"
f x='abc':
"#,
r#"f x='abc':"#,
}
2019-04-19 02:17:43 -07:00
parse_test! {
parameter_default_backtick,
r#"
f x=`echo hello`:
"#,
r#"f x=`echo hello`:"#,
}
2019-04-19 02:17:43 -07:00
parse_test! {
parameter_default_concatination_string,
r#"
f x=(`echo hello` + "foo"):
"#,
r#"f x=(`echo hello` + "foo"):"#,
}
2019-04-19 02:17:43 -07:00
parse_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":"#,
}
2019-04-19 02:17:43 -07:00
parse_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"):"#,
}
2019-04-19 02:17:43 -07:00
parse_test! {
concatination_in_group,
"x := ('0' + '1')",
"x := ('0' + '1')",
}
2019-04-19 02:17:43 -07:00
parse_test! {
string_in_group,
"x := ('0' )",
"x := ('0')",
}
#[rustfmt::skip]
2019-04-19 02:17:43 -07:00
parse_test! {
escaped_dos_newlines,
"@spam:\r
\t{ \\\r
\t\tfiglet test; \\\r
\t\tcargo build --color always 2>&1; \\\r
\t\tcargo test --color always -- --color always 2>&1; \\\r
\t} | less\r
",
"@spam:
{ \\
\tfiglet test; \\
\tcargo build --color always 2>&1; \\
\tcargo test --color always -- --color always 2>&1; \\
} | less",
}
2019-04-19 02:17:43 -07:00
error_test! {
name: duplicate_alias,
input: "alias foo = bar\nalias foo = baz",
offset: 22,
line: 1,
column: 6,
width: 3,
kind: DuplicateAlias { alias: "foo", first: 0 },
}
2019-04-19 02:17:43 -07:00
error_test! {
name: alias_syntax_multiple_rhs,
input: "alias foo = bar baz",
offset: 16,
line: 0,
column: 16,
width: 3,
kind: UnexpectedToken { expected: vec![Eol, Eof], found: Name },
}
2019-04-19 02:17:43 -07:00
error_test! {
name: alias_syntax_no_rhs,
input: "alias foo = \n",
offset: 12,
line: 0,
column: 12,
width: 1,
kind: UnexpectedToken {expected: vec![Name], found:Eol},
}
2019-04-19 02:17:43 -07:00
error_test! {
name: unknown_alias_target,
input: "alias foo = bar\n",
offset: 6,
line: 0,
column: 6,
width: 3,
kind: UnknownAliasTarget {alias: "foo", target: "bar"},
}
2019-04-19 02:17:43 -07:00
error_test! {
name: alias_shadows_recipe_before,
input: "bar: \n echo bar\nalias foo = bar\nfoo:\n echo foo",
offset: 23,
line: 2,
column: 6,
width: 3,
kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
}
2019-04-19 02:17:43 -07:00
error_test! {
name: alias_shadows_recipe_after,
input: "foo:\n echo foo\nalias foo = bar\nbar:\n echo bar",
offset: 22,
line: 2,
column: 6,
width: 3,
kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: missing_colon,
input: "a b c\nd e f",
offset: 5,
2017-11-17 23:59:55 -08:00
line: 0,
column: 5,
width: 1,
2017-11-17 23:59:55 -08:00
kind: UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: missing_default_eol,
input: "hello arg=\n",
offset: 10,
2017-11-17 23:59:55 -08:00
line: 0,
column: 10,
width: 1,
kind: UnexpectedToken{expected: vec![Name, StringCooked], found: Eol},
2017-11-17 23:59:55 -08:00
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: missing_default_eof,
input: "hello arg=",
offset: 10,
2017-11-17 23:59:55 -08:00
line: 0,
column: 10,
width: 0,
kind: UnexpectedToken{expected: vec![Name, StringCooked], found: Eof},
2017-11-17 23:59:55 -08:00
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: parameter_after_variadic,
input: "foo +a bbb:",
offset: 7,
2017-11-17 23:59:55 -08:00
line: 0,
column: 7,
width: 3,
2017-11-17 23:59:55 -08:00
kind: ParameterFollowsVariadicParameter{parameter: "bbb"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: required_after_default,
input: "hello arg='foo' bar:",
offset: 16,
2017-11-17 23:59:55 -08:00
line: 0,
column: 16,
width: 3,
2017-11-17 23:59:55 -08:00
kind: RequiredParameterFollowsDefaultParameter{parameter: "bar"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: missing_eol,
input: "a b c: z =",
offset: 9,
2017-11-17 23:59:55 -08:00
line: 0,
column: 9,
width: 1,
2017-11-17 23:59:55 -08:00
kind: UnexpectedToken{expected: vec![Name, Eol, Eof], found: Equals},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: duplicate_parameter,
input: "a b b:",
offset: 4,
2017-11-17 23:59:55 -08:00
line: 0,
column: 4,
width: 1,
2017-11-17 23:59:55 -08:00
kind: DuplicateParameter{recipe: "a", parameter: "b"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: parameter_shadows_varible,
input: "foo = \"h\"\na foo:",
offset: 12,
2017-11-17 23:59:55 -08:00
line: 1,
column: 2,
width: 3,
2017-11-17 23:59:55 -08:00
kind: ParameterShadowsVariable{parameter: "foo"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: dependency_has_parameters,
input: "foo arg:\nb: foo",
offset: 12,
2017-11-17 23:59:55 -08:00
line: 1,
column: 3,
width: 3,
2017-11-17 23:59:55 -08:00
kind: DependencyHasParameters{recipe: "b", dependency: "foo"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: duplicate_dependency,
input: "a b c: b c z z",
offset: 13,
2017-11-17 23:59:55 -08:00
line: 0,
column: 13,
width: 1,
2017-11-17 23:59:55 -08:00
kind: DuplicateDependency{recipe: "a", dependency: "z"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: duplicate_recipe,
input: "a:\nb:\na:",
offset: 6,
2017-11-17 23:59:55 -08:00
line: 2,
column: 0,
width: 1,
2017-11-17 23:59:55 -08:00
kind: DuplicateRecipe{recipe: "a", first: 0},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: duplicate_variable,
input: "a = \"0\"\na = \"0\"",
offset: 8,
2017-11-17 23:59:55 -08:00
line: 1,
column: 0,
width: 1,
2017-11-17 23:59:55 -08:00
kind: DuplicateVariable{variable: "a"},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: extra_whitespace,
input: "a:\n blah\n blarg",
offset: 10,
2017-11-17 23:59:55 -08:00
line: 2,
column: 1,
width: 6,
2017-11-17 23:59:55 -08:00
kind: ExtraLeadingWhitespace,
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: interpolation_outside_of_recipe,
input: "{{",
offset: 0,
2017-11-17 23:59:55 -08:00
line: 0,
column: 0,
width: 2,
2017-11-17 23:59:55 -08:00
kind: UnexpectedToken{expected: vec![Name, At], found: InterpolationStart},
}
2019-04-19 02:17:43 -07:00
error_test! {
name: unclosed_parenthesis_in_expression,
input: "x = foo(",
offset: 8,
line: 0,
column: 8,
width: 0,
kind: UnexpectedToken{expected: vec![Name, StringCooked, ParenR], found: Eof},
}
2019-04-19 02:17:43 -07:00
error_test! {
name: unclosed_parenthesis_in_interpolation,
input: "a:\n echo {{foo(}}",
offset: 15,
line: 1,
column: 12,
width: 2,
kind: UnexpectedToken{expected: vec![Name, StringCooked, ParenR], found: InterpolationEnd},
}
2019-04-19 02:17:43 -07:00
error_test! {
2017-11-17 23:59:55 -08:00
name: plus_following_parameter,
input: "a b c+:",
offset: 5,
2017-11-17 23:59:55 -08:00
line: 0,
column: 5,
width: 1,
2017-11-17 23:59:55 -08:00
kind: UnexpectedToken{expected: vec![Name], found: Plus},
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
2019-04-19 02:17:43 -07:00
error_test! {
name: bad_export,
input: "export a",
offset: 8,
line: 0,
column: 8,
width: 0,
kind: UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eof},
}
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
for line in fs::read_to_string("README.adoc").unwrap().lines() {
2017-11-17 17:28:06 -08:00
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 {
2019-04-19 02:17:43 -07:00
parse(&justfile);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
#[test]
fn empty_recipe_lines() {
let text = "a:";
2019-04-19 02:17:43 -07:00
let justfile = parse(&text);
assert_eq!(justfile.recipes["a"].lines.len(), 0);
}
#[test]
fn simple_recipe_lines() {
let text = "a:\n foo";
2019-04-19 02:17:43 -07:00
let justfile = parse(&text);
assert_eq!(justfile.recipes["a"].lines.len(), 1);
}
#[test]
fn complex_recipe_lines() {
let text = "a:
foo
b:
";
2019-04-19 02:17:43 -07:00
let justfile = parse(&text);
assert_eq!(justfile.recipes["a"].lines.len(), 1);
}
2017-11-16 23:30:08 -08:00
}