Add conditional expressions (#714)
Add conditional expressions of the form: foo := if lhs == rhs { then } else { otherwise } `lhs`, `rhs`, `then`, and `otherwise` are all arbitrary expressions, and can recursively include other conditionals. Conditionals short-circuit, so the branch not taken isn't evaluated. It is also possible to test for inequality with `==`.
This commit is contained in:
parent
3643a0dff0
commit
19f7ad09a7
37
Cargo.lock
generated
37
Cargo.lock
generated
@ -156,6 +156,15 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.17"
|
||||
@ -192,6 +201,7 @@ dependencies = [
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"snafu",
|
||||
"strum",
|
||||
"target",
|
||||
"tempfile",
|
||||
"test-utilities",
|
||||
@ -390,6 +400,27 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.19.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b89a286a7e3b5720b9a477b23253bc50debac207c8d21505f8e70b36792f11b5"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.19.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e61bb0be289045cb80bfce000512e32d09f8337e54c186725da381377ad1f8d5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.44"
|
||||
@ -475,6 +506,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
|
@ -30,6 +30,10 @@ unicode-width = "0.1.0"
|
||||
version = "3.1.1"
|
||||
features = ["termination"]
|
||||
|
||||
[dependencies.strum]
|
||||
version = "0.19.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dev-dependencies]
|
||||
executable-path = "1.0.0"
|
||||
pretty_assertions = "0.6.0"
|
||||
|
@ -57,9 +57,13 @@ export : 'export' assignment
|
||||
|
||||
setting : 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
|
||||
|
||||
expression : value '+' expression
|
||||
expression : 'if' condition '{' expression '}' else '{' expression '}'
|
||||
| value '+' expression
|
||||
| value
|
||||
|
||||
condition : expression '==' expression
|
||||
| expression '!=' expression
|
||||
|
||||
value : NAME '(' sequence? ')'
|
||||
| STRING
|
||||
| RAW_STRING
|
||||
|
48
README.adoc
48
README.adoc
@ -545,6 +545,54 @@ serve:
|
||||
./serve {{localhost}} 8080
|
||||
```
|
||||
|
||||
=== Conditional Expressions
|
||||
|
||||
`if`/`else` expressions evaluate different branches depending on if two expressions evaluate to the same value:
|
||||
|
||||
```make
|
||||
foo := if "2" == "2" { "Good!" } else { "1984" }
|
||||
|
||||
bar:
|
||||
@echo "{{foo}}"
|
||||
```
|
||||
|
||||
```sh
|
||||
$ just bar
|
||||
Good!
|
||||
```
|
||||
|
||||
It is also possible to test for inequality:
|
||||
|
||||
```make
|
||||
foo := if "hello" != "goodbye" { "xyz" } else { "abc" }
|
||||
|
||||
bar:
|
||||
@echo {{foo}}
|
||||
```
|
||||
|
||||
```sh
|
||||
$ just bar
|
||||
abc
|
||||
```
|
||||
|
||||
Conditional expressions short-circuit, which means they only evaluate one of
|
||||
their branches. This can be used to make sure that backtick expressions don't
|
||||
run when they shouldn't.
|
||||
|
||||
```make
|
||||
foo := if env_var("RELEASE") == "true" { `get-something-from-release-database` } else { "dummy-value" }
|
||||
```
|
||||
|
||||
Conditionals can be used inside of recipes:
|
||||
|
||||
```make
|
||||
bar foo:
|
||||
echo {{ if foo == "bar" { "hello" } else { "goodbye" } }}
|
||||
```
|
||||
|
||||
Note the space after the final `}`! Without the space, the interpolation will
|
||||
be prematurely closed.
|
||||
|
||||
=== Setting Variables from the Command Line
|
||||
|
||||
Variables can be overridden from the command line.
|
||||
|
2
justfile
2
justfile
@ -121,7 +121,7 @@ sloc:
|
||||
|
||||
@lint:
|
||||
echo Checking for FIXME/TODO...
|
||||
! grep --color -En 'FIXME|TODO' src/*.rs
|
||||
! grep --color -Ein 'fixme|todo|xxx|#\[ignore\]' src/*.rs
|
||||
echo Checking for long lines...
|
||||
! grep --color -En '.{101}' src/*.rs
|
||||
|
||||
|
@ -87,6 +87,18 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
||||
self.resolve_expression(lhs)?;
|
||||
self.resolve_expression(rhs)
|
||||
},
|
||||
Expression::Conditional {
|
||||
lhs,
|
||||
rhs,
|
||||
then,
|
||||
otherwise,
|
||||
..
|
||||
} => {
|
||||
self.resolve_expression(lhs)?;
|
||||
self.resolve_expression(rhs)?;
|
||||
self.resolve_expression(then)?;
|
||||
self.resolve_expression(otherwise)
|
||||
},
|
||||
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
|
||||
Expression::Group { contents } => self.resolve_expression(contents),
|
||||
}
|
||||
|
@ -24,10 +24,11 @@ pub(crate) use edit_distance::edit_distance;
|
||||
pub(crate) use libc::EXIT_FAILURE;
|
||||
pub(crate) use log::{info, warn};
|
||||
pub(crate) use snafu::{ResultExt, Snafu};
|
||||
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
|
||||
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
// modules
|
||||
pub(crate) use crate::{config_error, keyword, setting};
|
||||
pub(crate) use crate::{config_error, setting};
|
||||
|
||||
// functions
|
||||
pub(crate) use crate::{default::default, empty::empty, load_dotenv::load_dotenv, output::output};
|
||||
@ -47,12 +48,13 @@ pub(crate) use crate::{
|
||||
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
|
||||
fragment::Fragment, function::Function, function_context::FunctionContext,
|
||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
||||
justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
|
||||
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
|
||||
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
|
||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
||||
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
|
||||
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
|
||||
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List,
|
||||
load_error::LoadError, module::Module, name::Name, output_error::OutputError,
|
||||
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
|
||||
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
|
||||
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
|
||||
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
||||
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
|
||||
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
||||
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
||||
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
||||
|
@ -128,6 +128,11 @@ impl Display for CompilationError<'_> {
|
||||
writeln!(f, "at most {} {}", max, Count("argument", max))?;
|
||||
}
|
||||
},
|
||||
ExpectedKeyword { expected, found } => writeln!(
|
||||
f,
|
||||
"Expected keyword `{}` but found identifier `{}`",
|
||||
expected, found
|
||||
)?,
|
||||
ParameterShadowsVariable { parameter } => {
|
||||
writeln!(
|
||||
f,
|
||||
@ -198,6 +203,9 @@ impl Display for CompilationError<'_> {
|
||||
UnknownSetting { setting } => {
|
||||
writeln!(f, "Unknown setting `{}`", setting)?;
|
||||
},
|
||||
UnexpectedCharacter { expected } => {
|
||||
writeln!(f, "Expected character `{}`", expected)?;
|
||||
},
|
||||
UnknownStartOfToken => {
|
||||
writeln!(f, "Unknown start of token:")?;
|
||||
},
|
||||
|
@ -39,6 +39,10 @@ pub(crate) enum CompilationErrorKind<'src> {
|
||||
setting: &'src str,
|
||||
first: usize,
|
||||
},
|
||||
ExpectedKeyword {
|
||||
expected: Keyword,
|
||||
found: &'src str,
|
||||
},
|
||||
ExtraLeadingWhitespace,
|
||||
FunctionArgumentCountMismatch {
|
||||
function: &'src str,
|
||||
@ -86,6 +90,9 @@ pub(crate) enum CompilationErrorKind<'src> {
|
||||
function: &'src str,
|
||||
},
|
||||
UnknownStartOfToken,
|
||||
UnexpectedCharacter {
|
||||
expected: char,
|
||||
},
|
||||
UnknownSetting {
|
||||
setting: &'src str,
|
||||
},
|
||||
|
@ -116,6 +116,22 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
},
|
||||
Expression::Concatination { lhs, rhs } =>
|
||||
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?),
|
||||
Expression::Conditional {
|
||||
lhs,
|
||||
rhs,
|
||||
then,
|
||||
otherwise,
|
||||
inverted,
|
||||
} => {
|
||||
let lhs = self.evaluate_expression(lhs)?;
|
||||
let rhs = self.evaluate_expression(rhs)?;
|
||||
let condition = if *inverted { lhs != rhs } else { lhs == rhs };
|
||||
if condition {
|
||||
self.evaluate_expression(then)
|
||||
} else {
|
||||
self.evaluate_expression(otherwise)
|
||||
}
|
||||
},
|
||||
Expression::Group { contents } => self.evaluate_expression(contents),
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,14 @@ pub(crate) enum Expression<'src> {
|
||||
lhs: Box<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `if lhs == rhs { then } else { otherwise }`
|
||||
Conditional {
|
||||
lhs: Box<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
then: Box<Expression<'src>>,
|
||||
otherwise: Box<Expression<'src>>,
|
||||
inverted: bool,
|
||||
},
|
||||
/// `(contents)`
|
||||
Group { contents: Box<Expression<'src>> },
|
||||
/// `"string_literal"` or `'string_literal'`
|
||||
@ -39,6 +47,21 @@ impl<'src> Display for Expression<'src> {
|
||||
match self {
|
||||
Expression::Backtick { contents, .. } => write!(f, "`{}`", contents),
|
||||
Expression::Concatination { lhs, rhs } => write!(f, "{} + {}", lhs, rhs),
|
||||
Expression::Conditional {
|
||||
lhs,
|
||||
rhs,
|
||||
then,
|
||||
otherwise,
|
||||
inverted,
|
||||
} => write!(
|
||||
f,
|
||||
"if {} {} {} {{ {} }} else {{ {} }} ",
|
||||
lhs,
|
||||
if *inverted { "!=" } else { "==" },
|
||||
rhs,
|
||||
then,
|
||||
otherwise
|
||||
),
|
||||
Expression::StringLiteral { string_literal } => write!(f, "{}", string_literal),
|
||||
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
||||
Expression::Call { thunk } => write!(f, "{}", thunk),
|
||||
|
@ -1,5 +1,28 @@
|
||||
pub(crate) const ALIAS: &str = "alias";
|
||||
pub(crate) const EXPORT: &str = "export";
|
||||
pub(crate) const SET: &str = "set";
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) const SHELL: &str = "shell";
|
||||
#[derive(Debug, Eq, PartialEq, IntoStaticStr, Display, Copy, Clone, EnumString)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
pub(crate) enum Keyword {
|
||||
Alias,
|
||||
Else,
|
||||
Export,
|
||||
If,
|
||||
Set,
|
||||
Shell,
|
||||
}
|
||||
|
||||
impl Keyword {
|
||||
pub(crate) fn from_lexeme(lexeme: &str) -> Option<Keyword> {
|
||||
lexeme.parse().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn lexeme(self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&'a str> for Keyword {
|
||||
fn eq(&self, other: &&'a str) -> bool {
|
||||
self.lexeme() == *other
|
||||
}
|
||||
}
|
||||
|
218
src/lexer.rs
218
src/lexer.rs
@ -98,6 +98,25 @@ impl<'src> Lexer<'src> {
|
||||
self.token_end.offset - self.token_start.offset
|
||||
}
|
||||
|
||||
fn accepted(&mut self, c: char) -> CompilationResult<'src, bool> {
|
||||
if self.next_is(c) {
|
||||
self.advance()?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn presume(&mut self, c: char) -> CompilationResult<'src, ()> {
|
||||
if !self.next_is(c) {
|
||||
return Err(self.internal_error(format!("Lexer presumed character `{}`", c)));
|
||||
}
|
||||
|
||||
self.advance()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Is next character c?
|
||||
fn next_is(&self, c: char) -> bool {
|
||||
self.next == Some(c)
|
||||
@ -430,17 +449,18 @@ impl<'src> Lexer<'src> {
|
||||
/// Lex token beginning with `start` outside of a recipe body
|
||||
fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
|
||||
match start {
|
||||
'!' => self.lex_bang(),
|
||||
'*' => self.lex_single(Asterisk),
|
||||
'@' => self.lex_single(At),
|
||||
'[' => self.lex_single(BracketL),
|
||||
']' => self.lex_single(BracketR),
|
||||
'=' => self.lex_single(Equals),
|
||||
'=' => self.lex_choice('=', EqualsEquals, Equals),
|
||||
',' => self.lex_single(Comma),
|
||||
':' => self.lex_colon(),
|
||||
'(' => self.lex_single(ParenL),
|
||||
')' => self.lex_single(ParenR),
|
||||
'{' => self.lex_brace_l(),
|
||||
'}' => self.lex_brace_r(),
|
||||
'{' => self.lex_single(BraceL),
|
||||
'}' => self.lex_single(BraceR),
|
||||
'+' => self.lex_single(Plus),
|
||||
'\n' => self.lex_single(Eol),
|
||||
'\r' => self.lex_cr_lf(),
|
||||
@ -449,13 +469,11 @@ impl<'src> Lexer<'src> {
|
||||
' ' | '\t' => self.lex_whitespace(),
|
||||
'\'' => self.lex_raw_string(),
|
||||
'"' => self.lex_cooked_string(),
|
||||
_ =>
|
||||
if Self::is_identifier_start(start) {
|
||||
self.lex_identifier()
|
||||
} else {
|
||||
self.advance()?;
|
||||
Err(self.error(UnknownStartOfToken))
|
||||
},
|
||||
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
||||
_ => {
|
||||
self.advance()?;
|
||||
Err(self.error(UnknownStartOfToken))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,7 +483,6 @@ impl<'src> Lexer<'src> {
|
||||
interpolation_start: Token<'src>,
|
||||
start: char,
|
||||
) -> CompilationResult<'src, ()> {
|
||||
// Check for end of interpolation
|
||||
if self.rest_starts_with("}}") {
|
||||
// end current interpolation
|
||||
self.interpolation_start = None;
|
||||
@ -537,14 +554,14 @@ impl<'src> Lexer<'src> {
|
||||
self.recipe_body = false;
|
||||
}
|
||||
|
||||
/// Lex a single character token
|
||||
/// Lex a single-character token
|
||||
fn lex_single(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
||||
self.advance()?;
|
||||
self.token(kind);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lex a double character token
|
||||
/// Lex a double-character token
|
||||
fn lex_double(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
||||
self.advance()?;
|
||||
self.advance()?;
|
||||
@ -552,12 +569,48 @@ impl<'src> Lexer<'src> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lex a token starting with ':'
|
||||
fn lex_colon(&mut self) -> CompilationResult<'src, ()> {
|
||||
/// Lex a double-character token of kind `then` if the second character of
|
||||
/// that token would be `second`, otherwise lex a single-character token of
|
||||
/// kind `otherwise`
|
||||
fn lex_choice(
|
||||
&mut self,
|
||||
second: char,
|
||||
then: TokenKind,
|
||||
otherwise: TokenKind,
|
||||
) -> CompilationResult<'src, ()> {
|
||||
self.advance()?;
|
||||
|
||||
if self.next_is('=') {
|
||||
if self.accepted(second)? {
|
||||
self.token(then);
|
||||
} else {
|
||||
self.token(otherwise);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lex a token starting with '!'
|
||||
fn lex_bang(&mut self) -> CompilationResult<'src, ()> {
|
||||
self.presume('!')?;
|
||||
|
||||
if self.accepted('=')? {
|
||||
self.token(BangEquals);
|
||||
Ok(())
|
||||
} else {
|
||||
// Emit an unspecified token to consume the current character,
|
||||
self.token(Unspecified);
|
||||
// …and advance past another character,
|
||||
self.advance()?;
|
||||
// …so that the error we produce highlights the unexpected character.
|
||||
Err(self.error(UnexpectedCharacter { expected: '=' }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Lex a token starting with ':'
|
||||
fn lex_colon(&mut self) -> CompilationResult<'src, ()> {
|
||||
self.presume(':')?;
|
||||
|
||||
if self.accepted('=')? {
|
||||
self.token(ColonEquals);
|
||||
} else {
|
||||
self.token(Colon);
|
||||
@ -567,43 +620,21 @@ impl<'src> Lexer<'src> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lex a token starting with '{'
|
||||
fn lex_brace_l(&mut self) -> CompilationResult<'src, ()> {
|
||||
if !self.rest_starts_with("{{") {
|
||||
self.advance()?;
|
||||
|
||||
return Err(self.error(UnknownStartOfToken));
|
||||
}
|
||||
|
||||
self.lex_double(InterpolationStart)
|
||||
}
|
||||
|
||||
/// Lex a token starting with '}'
|
||||
fn lex_brace_r(&mut self) -> CompilationResult<'src, ()> {
|
||||
if !self.rest_starts_with("}}") {
|
||||
self.advance()?;
|
||||
|
||||
return Err(self.error(UnknownStartOfToken));
|
||||
}
|
||||
|
||||
self.lex_double(InterpolationEnd)
|
||||
}
|
||||
|
||||
/// Lex a carriage return and line feed
|
||||
fn lex_cr_lf(&mut self) -> CompilationResult<'src, ()> {
|
||||
if !self.rest_starts_with("\r\n") {
|
||||
// advance over \r
|
||||
self.advance()?;
|
||||
self.presume('\r')?;
|
||||
|
||||
if !self.accepted('\n')? {
|
||||
return Err(self.error(UnpairedCarriageReturn));
|
||||
}
|
||||
|
||||
self.lex_double(Eol)
|
||||
self.token(Eol);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
fn lex_identifier(&mut self) -> CompilationResult<'src, ()> {
|
||||
// advance over initial character
|
||||
self.advance()?;
|
||||
|
||||
while let Some(c) = self.next {
|
||||
@ -621,8 +652,7 @@ impl<'src> Lexer<'src> {
|
||||
|
||||
/// Lex comment: #[^\r\n]
|
||||
fn lex_comment(&mut self) -> CompilationResult<'src, ()> {
|
||||
// advance over #
|
||||
self.advance()?;
|
||||
self.presume('#')?;
|
||||
|
||||
while !self.at_eol_or_eof() {
|
||||
self.advance()?;
|
||||
@ -665,8 +695,7 @@ impl<'src> Lexer<'src> {
|
||||
|
||||
/// Lex raw string: '[^']*'
|
||||
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> {
|
||||
// advance over opening '
|
||||
self.advance()?;
|
||||
self.presume('\'')?;
|
||||
|
||||
loop {
|
||||
match self.next {
|
||||
@ -678,8 +707,7 @@ impl<'src> Lexer<'src> {
|
||||
self.advance()?;
|
||||
}
|
||||
|
||||
// advance over closing '
|
||||
self.advance()?;
|
||||
self.presume('\'')?;
|
||||
|
||||
self.token(StringRaw);
|
||||
|
||||
@ -688,8 +716,7 @@ impl<'src> Lexer<'src> {
|
||||
|
||||
/// Lex cooked string: "[^"\n\r]*" (also processes escape sequences)
|
||||
fn lex_cooked_string(&mut self) -> CompilationResult<'src, ()> {
|
||||
// advance over opening "
|
||||
self.advance()?;
|
||||
self.presume('"')?;
|
||||
|
||||
let mut escape = false;
|
||||
|
||||
@ -803,6 +830,9 @@ mod tests {
|
||||
// Fixed lexemes
|
||||
Asterisk => "*",
|
||||
At => "@",
|
||||
BangEquals => "!=",
|
||||
BraceL => "{",
|
||||
BraceR => "}",
|
||||
BracketL => "[",
|
||||
BracketR => "]",
|
||||
Colon => ":",
|
||||
@ -810,6 +840,7 @@ mod tests {
|
||||
Comma => ",",
|
||||
Eol => "\n",
|
||||
Equals => "=",
|
||||
EqualsEquals => "==",
|
||||
Indent => " ",
|
||||
InterpolationEnd => "}}",
|
||||
InterpolationStart => "{{",
|
||||
@ -901,6 +932,48 @@ mod tests {
|
||||
tokens: (StringCooked:"\"hello\""),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: equals,
|
||||
text: "=",
|
||||
tokens: (Equals),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: equals_equals,
|
||||
text: "==",
|
||||
tokens: (EqualsEquals),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: bang_equals,
|
||||
text: "!=",
|
||||
tokens: (BangEquals),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: brace_l,
|
||||
text: "{",
|
||||
tokens: (BraceL),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: brace_r,
|
||||
text: "}",
|
||||
tokens: (BraceR),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: brace_lll,
|
||||
text: "{{{",
|
||||
tokens: (BraceL, BraceL, BraceL),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: brace_rrr,
|
||||
text: "}}}",
|
||||
tokens: (BraceR, BraceR, BraceR),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: export_concatination,
|
||||
text: "export foo = 'foo' + 'bar'",
|
||||
@ -1965,4 +2038,47 @@ mod tests {
|
||||
width: 2,
|
||||
kind: UnterminatedInterpolation,
|
||||
}
|
||||
|
||||
error! {
|
||||
name: unexpected_character_after_bang,
|
||||
input: "!{",
|
||||
offset: 1,
|
||||
line: 0,
|
||||
column: 1,
|
||||
width: 1,
|
||||
kind: UnexpectedCharacter { expected: '=' },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presume_error() {
|
||||
assert_matches!(
|
||||
Lexer::new("!").presume('-').unwrap_err(),
|
||||
CompilationError {
|
||||
token: Token {
|
||||
offset: 0,
|
||||
line: 0,
|
||||
column: 0,
|
||||
length: 0,
|
||||
src: "!",
|
||||
kind: Unspecified,
|
||||
},
|
||||
kind: Internal {
|
||||
message,
|
||||
},
|
||||
} if message == "Lexer presumed character `-`"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Lexer::new("!").presume('-').unwrap_err().to_string(),
|
||||
testing::unindent(
|
||||
"
|
||||
Internal error, this may indicate a bug in just: Lexer presumed character `-`
|
||||
\
|
||||
consider filing an issue: https://github.com/casey/just/issues/new
|
||||
|
|
||||
1 | !
|
||||
| ^"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
27
src/node.rs
27
src/node.rs
@ -28,7 +28,7 @@ impl<'src> Node<'src> for Item<'src> {
|
||||
|
||||
impl<'src> Node<'src> for Alias<'src, Name<'src>> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
Tree::atom(keyword::ALIAS)
|
||||
Tree::atom(Keyword::Alias.lexeme())
|
||||
.push(self.name.lexeme())
|
||||
.push(self.target.lexeme())
|
||||
}
|
||||
@ -37,7 +37,9 @@ impl<'src> Node<'src> for Alias<'src, Name<'src>> {
|
||||
impl<'src> Node<'src> for Assignment<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
if self.export {
|
||||
Tree::atom("assignment").push("#").push(keyword::EXPORT)
|
||||
Tree::atom("assignment")
|
||||
.push("#")
|
||||
.push(Keyword::Export.lexeme())
|
||||
} else {
|
||||
Tree::atom("assignment")
|
||||
}
|
||||
@ -50,6 +52,25 @@ impl<'src> Node<'src> for Expression<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
match self {
|
||||
Expression::Concatination { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||
Expression::Conditional {
|
||||
lhs,
|
||||
rhs,
|
||||
then,
|
||||
otherwise,
|
||||
inverted,
|
||||
} => {
|
||||
let mut tree = Tree::atom(Keyword::If.lexeme());
|
||||
tree.push_mut(lhs.tree());
|
||||
if *inverted {
|
||||
tree.push_mut("!=")
|
||||
} else {
|
||||
tree.push_mut("==")
|
||||
}
|
||||
tree.push_mut(rhs.tree());
|
||||
tree.push_mut(then.tree());
|
||||
tree.push_mut(otherwise.tree());
|
||||
tree
|
||||
},
|
||||
Expression::Call { thunk } => {
|
||||
let mut tree = Tree::atom("call");
|
||||
|
||||
@ -164,7 +185,7 @@ impl<'src> Node<'src> for Fragment<'src> {
|
||||
|
||||
impl<'src> Node<'src> for Set<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
let mut set = Tree::atom(keyword::SET);
|
||||
let mut set = Tree::atom(Keyword::Set.lexeme());
|
||||
|
||||
set.push_mut(self.name.lexeme());
|
||||
|
||||
|
163
src/parser.rs
163
src/parser.rs
@ -172,9 +172,20 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
self.expect(Eol).map(|_| ())
|
||||
}
|
||||
|
||||
fn expect_keyword(&mut self, expected: Keyword) -> CompilationResult<'src, ()> {
|
||||
let identifier = self.expect(Identifier)?;
|
||||
let found = identifier.lexeme();
|
||||
|
||||
if expected == found {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(identifier.error(CompilationErrorKind::ExpectedKeyword { expected, found }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an internal error if the next token is not of kind `Identifier`
|
||||
/// with lexeme `lexeme`.
|
||||
fn presume_name(&mut self, lexeme: &str) -> CompilationResult<'src, ()> {
|
||||
fn presume_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, ()> {
|
||||
let next = self.advance()?;
|
||||
|
||||
if next.kind != Identifier {
|
||||
@ -182,10 +193,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
"Presumed next token would have kind {}, but found {}",
|
||||
Identifier, next.kind
|
||||
))?)
|
||||
} else if next.lexeme() != lexeme {
|
||||
} else if keyword != next.lexeme() {
|
||||
Err(self.internal_error(format!(
|
||||
"Presumed next token would have lexeme \"{}\", but found \"{}\"",
|
||||
lexeme,
|
||||
keyword,
|
||||
next.lexeme(),
|
||||
))?)
|
||||
} else {
|
||||
@ -253,6 +264,17 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
}
|
||||
}
|
||||
|
||||
fn accepted_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, bool> {
|
||||
let next = self.next()?;
|
||||
|
||||
if next.kind == Identifier && next.lexeme() == keyword.lexeme() {
|
||||
self.advance()?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a dependency
|
||||
fn accept_dependency(&mut self) -> CompilationResult<'src, Option<UnresolvedDependency<'src>>> {
|
||||
if let Some(recipe) = self.accept_name()? {
|
||||
@ -297,8 +319,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
} else if self.accepted(Eof)? {
|
||||
break;
|
||||
} else if self.next_is(Identifier) {
|
||||
match next.lexeme() {
|
||||
keyword::ALIAS =>
|
||||
match Keyword::from_lexeme(next.lexeme()) {
|
||||
Some(Keyword::Alias) =>
|
||||
if self.next_are(&[Identifier, Identifier, Equals]) {
|
||||
warnings.push(Warning::DeprecatedEquals {
|
||||
equals: self.get(2)?,
|
||||
@ -309,20 +331,20 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
} else {
|
||||
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
|
||||
},
|
||||
keyword::EXPORT =>
|
||||
Some(Keyword::Export) =>
|
||||
if self.next_are(&[Identifier, Identifier, Equals]) {
|
||||
warnings.push(Warning::DeprecatedEquals {
|
||||
equals: self.get(2)?,
|
||||
});
|
||||
self.presume_name(keyword::EXPORT)?;
|
||||
self.presume_keyword(Keyword::Export)?;
|
||||
items.push(Item::Assignment(self.parse_assignment(true)?));
|
||||
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
||||
self.presume_name(keyword::EXPORT)?;
|
||||
self.presume_keyword(Keyword::Export)?;
|
||||
items.push(Item::Assignment(self.parse_assignment(true)?));
|
||||
} else {
|
||||
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
|
||||
},
|
||||
keyword::SET =>
|
||||
Some(Keyword::Set) =>
|
||||
if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
||||
items.push(Item::Set(self.parse_set()?));
|
||||
} else {
|
||||
@ -363,7 +385,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
|
||||
/// Parse an alias, e.g `alias name := target`
|
||||
fn parse_alias(&mut self) -> CompilationResult<'src, Alias<'src, Name<'src>>> {
|
||||
self.presume_name(keyword::ALIAS)?;
|
||||
self.presume_keyword(Keyword::Alias)?;
|
||||
let name = self.parse_name()?;
|
||||
self.presume_any(&[Equals, ColonEquals])?;
|
||||
let target = self.parse_name()?;
|
||||
@ -386,6 +408,40 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
|
||||
/// Parse an expression, e.g. `1 + 2`
|
||||
fn parse_expression(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
||||
if self.accepted_keyword(Keyword::If)? {
|
||||
let lhs = self.parse_expression()?;
|
||||
|
||||
let inverted = self.accepted(BangEquals)?;
|
||||
|
||||
if !inverted {
|
||||
self.expect(EqualsEquals)?;
|
||||
}
|
||||
|
||||
let rhs = self.parse_expression()?;
|
||||
|
||||
self.expect(BraceL)?;
|
||||
|
||||
let then = self.parse_expression()?;
|
||||
|
||||
self.expect(BraceR)?;
|
||||
|
||||
self.expect_keyword(Keyword::Else)?;
|
||||
|
||||
self.expect(BraceL)?;
|
||||
|
||||
let otherwise = self.parse_expression()?;
|
||||
|
||||
self.expect(BraceR)?;
|
||||
|
||||
return Ok(Expression::Conditional {
|
||||
lhs: Box::new(lhs),
|
||||
rhs: Box::new(rhs),
|
||||
then: Box::new(then),
|
||||
otherwise: Box::new(otherwise),
|
||||
inverted,
|
||||
});
|
||||
}
|
||||
|
||||
let value = self.parse_value()?;
|
||||
|
||||
if self.accepted(Plus)? {
|
||||
@ -619,37 +675,36 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
|
||||
/// Parse a setting
|
||||
fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
|
||||
self.presume_name(keyword::SET)?;
|
||||
self.presume_keyword(Keyword::Set)?;
|
||||
let name = Name::from_identifier(self.presume(Identifier)?);
|
||||
self.presume(ColonEquals)?;
|
||||
match name.lexeme() {
|
||||
keyword::SHELL => {
|
||||
self.expect(BracketL)?;
|
||||
if name.lexeme() == Keyword::Shell.lexeme() {
|
||||
self.expect(BracketL)?;
|
||||
|
||||
let command = self.parse_string_literal()?;
|
||||
let command = self.parse_string_literal()?;
|
||||
|
||||
let mut arguments = Vec::new();
|
||||
let mut arguments = Vec::new();
|
||||
|
||||
if self.accepted(Comma)? {
|
||||
while !self.next_is(BracketR) {
|
||||
arguments.push(self.parse_string_literal()?);
|
||||
if self.accepted(Comma)? {
|
||||
while !self.next_is(BracketR) {
|
||||
arguments.push(self.parse_string_literal()?);
|
||||
|
||||
if !self.accepted(Comma)? {
|
||||
break;
|
||||
}
|
||||
if !self.accepted(Comma)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.expect(BracketR)?;
|
||||
self.expect(BracketR)?;
|
||||
|
||||
Ok(Set {
|
||||
value: Setting::Shell(setting::Shell { command, arguments }),
|
||||
name,
|
||||
})
|
||||
},
|
||||
_ => Err(name.error(CompilationErrorKind::UnknownSetting {
|
||||
Ok(Set {
|
||||
value: Setting::Shell(setting::Shell { command, arguments }),
|
||||
name,
|
||||
})
|
||||
} else {
|
||||
Err(name.error(CompilationErrorKind::UnknownSetting {
|
||||
setting: name.lexeme(),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1509,6 +1564,48 @@ mod tests {
|
||||
tree: (justfile (set shell "bash" "-cu" "-l")),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional,
|
||||
text: "a := if b == c { d } else { e }",
|
||||
tree: (justfile (assignment a (if b == c d e))),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional_inverted,
|
||||
text: "a := if b != c { d } else { e }",
|
||||
tree: (justfile (assignment a (if b != c d e))),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional_concatinations,
|
||||
text: "a := if b0 + b1 == c0 + c1 { d0 + d1 } else { e0 + e1 }",
|
||||
tree: (justfile (assignment a (if (+ b0 b1) == (+ c0 c1) (+ d0 d1) (+ e0 e1)))),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional_nested_lhs,
|
||||
text: "a := if if b == c { d } else { e } == c { d } else { e }",
|
||||
tree: (justfile (assignment a (if (if b == c d e) == c d e))),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional_nested_rhs,
|
||||
text: "a := if c == if b == c { d } else { e } { d } else { e }",
|
||||
tree: (justfile (assignment a (if c == (if b == c d e) d e))),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional_nested_then,
|
||||
text: "a := if b == c { if b == c { d } else { e } } else { e }",
|
||||
tree: (justfile (assignment a (if b == c (if b == c d e) e))),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: conditional_nested_otherwise,
|
||||
text: "a := if b == c { d } else { if b == c { d } else { e } }",
|
||||
tree: (justfile (assignment a (if b == c d (if b == c d e)))),
|
||||
}
|
||||
|
||||
error! {
|
||||
name: alias_syntax_multiple_rhs,
|
||||
input: "alias foo = bar baz",
|
||||
@ -1576,15 +1673,15 @@ mod tests {
|
||||
}
|
||||
|
||||
error! {
|
||||
name: interpolation_outside_of_recipe,
|
||||
name: unexpected_brace,
|
||||
input: "{{",
|
||||
offset: 0,
|
||||
line: 0,
|
||||
column: 0,
|
||||
width: 2,
|
||||
width: 1,
|
||||
kind: UnexpectedToken {
|
||||
expected: vec![At, Comment, Eof, Eol, Identifier],
|
||||
found: InterpolationStart,
|
||||
found: BraceL,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -193,6 +193,13 @@ pub enum Expression {
|
||||
lhs: Box<Expression>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
Conditional {
|
||||
lhs: Box<Expression>,
|
||||
rhs: Box<Expression>,
|
||||
then: Box<Expression>,
|
||||
otherwise: Box<Expression>,
|
||||
inverted: bool,
|
||||
},
|
||||
String {
|
||||
text: String,
|
||||
},
|
||||
@ -228,6 +235,19 @@ impl Expression {
|
||||
lhs: Box::new(Expression::new(lhs)),
|
||||
rhs: Box::new(Expression::new(rhs)),
|
||||
},
|
||||
Conditional {
|
||||
lhs,
|
||||
rhs,
|
||||
inverted,
|
||||
then,
|
||||
otherwise,
|
||||
} => Expression::Conditional {
|
||||
lhs: Box::new(Expression::new(lhs)),
|
||||
rhs: Box::new(Expression::new(rhs)),
|
||||
then: Box::new(Expression::new(lhs)),
|
||||
otherwise: Box::new(Expression::new(rhs)),
|
||||
inverted: *inverted,
|
||||
},
|
||||
StringLiteral { string_literal } => Expression::String {
|
||||
text: string_literal.cooked.to_string(),
|
||||
},
|
||||
|
@ -113,3 +113,16 @@ macro_rules! run_error {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_matches {
|
||||
($expression:expr, $( $pattern:pat )|+ $( if $guard:expr )?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => {}
|
||||
left => panic!(
|
||||
"assertion failed: (left ~= right)\n left: `{:?}`\n right: `{}`",
|
||||
left,
|
||||
stringify!($($pattern)|+ $(if $guard)?)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ pub(crate) enum TokenKind {
|
||||
Asterisk,
|
||||
At,
|
||||
Backtick,
|
||||
BangEquals,
|
||||
BraceL,
|
||||
BraceR,
|
||||
BracketL,
|
||||
BracketR,
|
||||
Colon,
|
||||
@ -15,6 +18,7 @@ pub(crate) enum TokenKind {
|
||||
Eof,
|
||||
Eol,
|
||||
Equals,
|
||||
EqualsEquals,
|
||||
Identifier,
|
||||
Indent,
|
||||
InterpolationEnd,
|
||||
@ -36,6 +40,9 @@ impl Display for TokenKind {
|
||||
Asterisk => "'*'",
|
||||
At => "'@'",
|
||||
Backtick => "backtick",
|
||||
BangEquals => "'!='",
|
||||
BraceL => "'{'",
|
||||
BraceR => "'}'",
|
||||
BracketL => "'['",
|
||||
BracketR => "']'",
|
||||
Colon => "':'",
|
||||
@ -46,6 +53,7 @@ impl Display for TokenKind {
|
||||
Eof => "end of file",
|
||||
Eol => "end of line",
|
||||
Equals => "'='",
|
||||
EqualsEquals => "'=='",
|
||||
Identifier => "identifier",
|
||||
Indent => "indent",
|
||||
InterpolationEnd => "'}}'",
|
||||
|
12
src/tree.rs
12
src/tree.rs
@ -41,6 +41,18 @@ macro_rules! tree {
|
||||
} => {
|
||||
$crate::tree::Tree::atom("*")
|
||||
};
|
||||
|
||||
{
|
||||
==
|
||||
} => {
|
||||
$crate::tree::Tree::atom("==")
|
||||
};
|
||||
|
||||
{
|
||||
!=
|
||||
} => {
|
||||
$crate::tree::Tree::atom("!=")
|
||||
};
|
||||
}
|
||||
|
||||
/// A `Tree` is either…
|
||||
|
@ -19,6 +19,19 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
||||
| Some(Expression::StringLiteral { .. })
|
||||
| Some(Expression::Backtick { .. })
|
||||
| Some(Expression::Call { .. }) => None,
|
||||
Some(Expression::Conditional {
|
||||
lhs,
|
||||
rhs,
|
||||
then,
|
||||
otherwise,
|
||||
..
|
||||
}) => {
|
||||
self.stack.push(lhs);
|
||||
self.stack.push(rhs);
|
||||
self.stack.push(then);
|
||||
self.stack.push(otherwise);
|
||||
self.next()
|
||||
},
|
||||
Some(Expression::Variable { name, .. }) => Some(name.token()),
|
||||
Some(Expression::Concatination { lhs, rhs }) => {
|
||||
self.stack.push(lhs);
|
||||
|
158
tests/conditional.rs
Normal file
158
tests/conditional.rs
Normal file
@ -0,0 +1,158 @@
|
||||
use crate::common::*;
|
||||
|
||||
test! {
|
||||
name: then_branch_unevaluated,
|
||||
justfile: "
|
||||
foo:
|
||||
echo {{ if 'a' == 'b' { `exit 1` } else { 'otherwise' } }}
|
||||
",
|
||||
stdout: "otherwise\n",
|
||||
stderr: "echo otherwise\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: otherwise_branch_unevaluated,
|
||||
justfile: "
|
||||
foo:
|
||||
echo {{ if 'a' == 'a' { 'then' } else { `exit 1` } }}
|
||||
",
|
||||
stdout: "then\n",
|
||||
stderr: "echo then\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: otherwise_branch_unevaluated_inverted,
|
||||
justfile: "
|
||||
foo:
|
||||
echo {{ if 'a' != 'b' { 'then' } else { `exit 1` } }}
|
||||
",
|
||||
stdout: "then\n",
|
||||
stderr: "echo then\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: then_branch_unevaluated_inverted,
|
||||
justfile: "
|
||||
foo:
|
||||
echo {{ if 'a' != 'a' { `exit 1` } else { 'otherwise' } }}
|
||||
",
|
||||
stdout: "otherwise\n",
|
||||
stderr: "echo otherwise\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: complex_expressions,
|
||||
justfile: "
|
||||
foo:
|
||||
echo {{ if 'a' + 'b' == `echo ab` { 'c' + 'd' } else { 'e' + 'f' } }}
|
||||
",
|
||||
stdout: "cd\n",
|
||||
stderr: "echo cd\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: undefined_lhs,
|
||||
justfile: "
|
||||
a := if b == '' { '' } else { '' }
|
||||
|
||||
foo:
|
||||
echo {{ a }}
|
||||
",
|
||||
stdout: "",
|
||||
stderr: "
|
||||
error: Variable `b` not defined
|
||||
|
|
||||
1 | a := if b == '' { '' } else { '' }
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: undefined_rhs,
|
||||
justfile: "
|
||||
a := if '' == b { '' } else { '' }
|
||||
|
||||
foo:
|
||||
echo {{ a }}
|
||||
",
|
||||
stdout: "",
|
||||
stderr: "
|
||||
error: Variable `b` not defined
|
||||
|
|
||||
1 | a := if '' == b { '' } else { '' }
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: undefined_then,
|
||||
justfile: "
|
||||
a := if '' == '' { b } else { '' }
|
||||
|
||||
foo:
|
||||
echo {{ a }}
|
||||
",
|
||||
stdout: "",
|
||||
stderr: "
|
||||
error: Variable `b` not defined
|
||||
|
|
||||
1 | a := if '' == '' { b } else { '' }
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: undefined_otherwise,
|
||||
justfile: "
|
||||
a := if '' == '' { '' } else { b }
|
||||
|
||||
foo:
|
||||
echo {{ a }}
|
||||
",
|
||||
stdout: "",
|
||||
stderr: "
|
||||
error: Variable `b` not defined
|
||||
|
|
||||
1 | a := if '' == '' { '' } else { b }
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: unexpected_op,
|
||||
justfile: "
|
||||
a := if '' a '' { '' } else { b }
|
||||
|
||||
foo:
|
||||
echo {{ a }}
|
||||
",
|
||||
stdout: "",
|
||||
stderr: "
|
||||
error: Expected '!=', '==', or '+', but found identifier
|
||||
|
|
||||
1 | a := if '' a '' { '' } else { b }
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: dump,
|
||||
justfile: "
|
||||
a := if '' == '' { '' } else { '' }
|
||||
|
||||
foo:
|
||||
echo {{ a }}
|
||||
",
|
||||
args: ("--dump"),
|
||||
stdout: format!("
|
||||
a := if '' == '' {{ '' }} else {{ '' }}{}
|
||||
|
||||
foo:
|
||||
echo {{{{a}}}}
|
||||
", " ").as_str(),
|
||||
}
|
25
tests/error_messages.rs
Normal file
25
tests/error_messages.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::common::*;
|
||||
|
||||
test! {
|
||||
name: expected_keyword,
|
||||
justfile: "foo := if '' == '' { '' } arlo { '' }",
|
||||
stderr: "
|
||||
error: Expected keyword `else` but found identifier `arlo`
|
||||
|
|
||||
1 | foo := if '' == '' { '' } arlo { '' }
|
||||
| ^^^^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: unexpected_character,
|
||||
justfile: "!~",
|
||||
stderr: "
|
||||
error: Expected character `=`
|
||||
|
|
||||
1 | !~
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
@ -5,8 +5,10 @@ mod common;
|
||||
|
||||
mod choose;
|
||||
mod completions;
|
||||
mod conditional;
|
||||
mod dotenv;
|
||||
mod edit;
|
||||
mod error_messages;
|
||||
mod examples;
|
||||
mod init;
|
||||
mod interrupts;
|
||||
|
Loading…
Reference in New Issue
Block a user