Add assert
expression (#1845)
This commit is contained in:
parent
e11684008e
commit
9aea3e679b
@ -83,6 +83,7 @@ module : 'mod' '?'? NAME string?
|
|||||||
boolean : ':=' ('true' | 'false')
|
boolean : ':=' ('true' | 'false')
|
||||||
|
|
||||||
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
||||||
|
| 'assert' '(' condition ',' expression ')'
|
||||||
| value '/' expression
|
| value '/' expression
|
||||||
| value '+' expression
|
| value '+' expression
|
||||||
| value
|
| value
|
||||||
|
@ -54,25 +54,17 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
|
|
||||||
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> {
|
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> {
|
||||||
match expression {
|
match expression {
|
||||||
Expression::Variable { name } => {
|
Expression::Assert {
|
||||||
let variable = name.lexeme();
|
condition: Condition {
|
||||||
if self.evaluated.contains(variable) {
|
lhs,
|
||||||
Ok(())
|
rhs,
|
||||||
} else if self.stack.contains(&variable) {
|
operator: _,
|
||||||
self.stack.push(variable);
|
},
|
||||||
Err(
|
error,
|
||||||
self.assignments[variable]
|
} => {
|
||||||
.name
|
self.resolve_expression(lhs)?;
|
||||||
.error(CircularVariableDependency {
|
self.resolve_expression(rhs)?;
|
||||||
variable,
|
self.resolve_expression(error)
|
||||||
circle: self.stack.clone(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else if self.assignments.contains_key(variable) {
|
|
||||||
self.resolve_assignment(variable)
|
|
||||||
} else {
|
|
||||||
Err(name.token.error(UndefinedVariable { variable }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Expression::Call { thunk } => match thunk {
|
Expression::Call { thunk } => match thunk {
|
||||||
Thunk::Nullary { .. } => Ok(()),
|
Thunk::Nullary { .. } => Ok(()),
|
||||||
@ -111,15 +103,12 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
self.resolve_expression(lhs)?;
|
self.resolve_expression(lhs)?;
|
||||||
self.resolve_expression(rhs)
|
self.resolve_expression(rhs)
|
||||||
}
|
}
|
||||||
Expression::Join { lhs, rhs } => {
|
|
||||||
if let Some(lhs) = lhs {
|
|
||||||
self.resolve_expression(lhs)?;
|
|
||||||
}
|
|
||||||
self.resolve_expression(rhs)
|
|
||||||
}
|
|
||||||
Expression::Conditional {
|
Expression::Conditional {
|
||||||
lhs,
|
condition: Condition {
|
||||||
rhs,
|
lhs,
|
||||||
|
rhs,
|
||||||
|
operator: _,
|
||||||
|
},
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
..
|
..
|
||||||
@ -129,8 +118,34 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
self.resolve_expression(then)?;
|
self.resolve_expression(then)?;
|
||||||
self.resolve_expression(otherwise)
|
self.resolve_expression(otherwise)
|
||||||
}
|
}
|
||||||
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
|
|
||||||
Expression::Group { contents } => self.resolve_expression(contents),
|
Expression::Group { contents } => self.resolve_expression(contents),
|
||||||
|
Expression::Join { lhs, rhs } => {
|
||||||
|
if let Some(lhs) = lhs {
|
||||||
|
self.resolve_expression(lhs)?;
|
||||||
|
}
|
||||||
|
self.resolve_expression(rhs)
|
||||||
|
}
|
||||||
|
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
|
||||||
|
Expression::Variable { name } => {
|
||||||
|
let variable = name.lexeme();
|
||||||
|
if self.evaluated.contains(variable) {
|
||||||
|
Ok(())
|
||||||
|
} else if self.stack.contains(&variable) {
|
||||||
|
self.stack.push(variable);
|
||||||
|
Err(
|
||||||
|
self.assignments[variable]
|
||||||
|
.name
|
||||||
|
.error(CircularVariableDependency {
|
||||||
|
variable,
|
||||||
|
circle: self.stack.clone(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if self.assignments.contains_key(variable) {
|
||||||
|
self.resolve_assignment(variable)
|
||||||
|
} else {
|
||||||
|
Err(name.token.error(UndefinedVariable { variable }))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
src/condition.rs
Normal file
27
src/condition.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub(crate) struct Condition<'src> {
|
||||||
|
pub(crate) lhs: Box<Expression<'src>>,
|
||||||
|
pub(crate) rhs: Box<Expression<'src>>,
|
||||||
|
pub(crate) operator: ConditionalOperator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> Display for Condition<'src> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
|
write!(f, "{} {} {}", self.lhs, self.operator, self.rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> Serialize for Condition<'src> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
|
seq.serialize_element(&self.operator.to_string())?;
|
||||||
|
seq.serialize_element(&self.lhs)?;
|
||||||
|
seq.serialize_element(&self.rhs)?;
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,9 @@ pub(crate) enum Error<'src> {
|
|||||||
min: usize,
|
min: usize,
|
||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
|
Assert {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
Backtick {
|
Backtick {
|
||||||
token: Token<'src>,
|
token: Token<'src>,
|
||||||
output_error: OutputError,
|
output_error: OutputError,
|
||||||
@ -256,6 +259,9 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?;
|
write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Assert { message }=> {
|
||||||
|
write!(f, "Assert failed: {message}")?;
|
||||||
|
}
|
||||||
Backtick { output_error, .. } => match output_error {
|
Backtick { output_error, .. } => match output_error {
|
||||||
OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?,
|
OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?,
|
||||||
OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?,
|
OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?,
|
||||||
|
@ -171,22 +171,11 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
|
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
|
||||||
}
|
}
|
||||||
Expression::Conditional {
|
Expression::Conditional {
|
||||||
lhs,
|
condition,
|
||||||
rhs,
|
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
operator,
|
|
||||||
} => {
|
} => {
|
||||||
let lhs_value = self.evaluate_expression(lhs)?;
|
if self.evaluate_condition(condition)? {
|
||||||
let rhs_value = self.evaluate_expression(rhs)?;
|
|
||||||
let condition = match operator {
|
|
||||||
ConditionalOperator::Equality => lhs_value == rhs_value,
|
|
||||||
ConditionalOperator::Inequality => lhs_value != rhs_value,
|
|
||||||
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
|
|
||||||
.map_err(|source| Error::RegexCompile { source })?
|
|
||||||
.is_match(&lhs_value),
|
|
||||||
};
|
|
||||||
if condition {
|
|
||||||
self.evaluate_expression(then)
|
self.evaluate_expression(then)
|
||||||
} else {
|
} else {
|
||||||
self.evaluate_expression(otherwise)
|
self.evaluate_expression(otherwise)
|
||||||
@ -198,9 +187,31 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
lhs: Some(lhs),
|
lhs: Some(lhs),
|
||||||
rhs,
|
rhs,
|
||||||
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
|
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
|
||||||
|
Expression::Assert { condition, error } => {
|
||||||
|
if self.evaluate_condition(condition)? {
|
||||||
|
Ok(String::new())
|
||||||
|
} else {
|
||||||
|
Err(Error::Assert {
|
||||||
|
message: self.evaluate_expression(error)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_condition(&mut self, condition: &Condition<'src>) -> RunResult<'src, bool> {
|
||||||
|
let lhs_value = self.evaluate_expression(&condition.lhs)?;
|
||||||
|
let rhs_value = self.evaluate_expression(&condition.rhs)?;
|
||||||
|
let condition = match condition.operator {
|
||||||
|
ConditionalOperator::Equality => lhs_value == rhs_value,
|
||||||
|
ConditionalOperator::Inequality => lhs_value != rhs_value,
|
||||||
|
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
|
||||||
|
.map_err(|source| Error::RegexCompile { source })?
|
||||||
|
.is_match(&lhs_value),
|
||||||
|
};
|
||||||
|
Ok(condition)
|
||||||
|
}
|
||||||
|
|
||||||
fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> {
|
fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> {
|
||||||
let mut cmd = self.settings.shell_command(self.config);
|
let mut cmd = self.settings.shell_command(self.config);
|
||||||
|
|
||||||
|
@ -8,6 +8,11 @@ use super::*;
|
|||||||
/// The parser parses both values and expressions into `Expression`s.
|
/// The parser parses both values and expressions into `Expression`s.
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub(crate) enum Expression<'src> {
|
pub(crate) enum Expression<'src> {
|
||||||
|
/// `assert(condition, error)`
|
||||||
|
Assert {
|
||||||
|
condition: Condition<'src>,
|
||||||
|
error: Box<Expression<'src>>,
|
||||||
|
},
|
||||||
/// `contents`
|
/// `contents`
|
||||||
Backtick {
|
Backtick {
|
||||||
contents: String,
|
contents: String,
|
||||||
@ -20,13 +25,11 @@ pub(crate) enum Expression<'src> {
|
|||||||
lhs: Box<Expression<'src>>,
|
lhs: Box<Expression<'src>>,
|
||||||
rhs: Box<Expression<'src>>,
|
rhs: Box<Expression<'src>>,
|
||||||
},
|
},
|
||||||
/// `if lhs == rhs { then } else { otherwise }`
|
/// `if condition { then } else { otherwise }`
|
||||||
Conditional {
|
Conditional {
|
||||||
lhs: Box<Expression<'src>>,
|
condition: Condition<'src>,
|
||||||
rhs: Box<Expression<'src>>,
|
|
||||||
then: Box<Expression<'src>>,
|
then: Box<Expression<'src>>,
|
||||||
otherwise: Box<Expression<'src>>,
|
otherwise: Box<Expression<'src>>,
|
||||||
operator: ConditionalOperator,
|
|
||||||
},
|
},
|
||||||
/// `(contents)`
|
/// `(contents)`
|
||||||
Group { contents: Box<Expression<'src>> },
|
Group { contents: Box<Expression<'src>> },
|
||||||
@ -50,6 +53,7 @@ impl<'src> Expression<'src> {
|
|||||||
impl<'src> Display for Expression<'src> {
|
impl<'src> Display for Expression<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
match self {
|
match self {
|
||||||
|
Expression::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
||||||
Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
||||||
Expression::Join { lhs: None, rhs } => write!(f, "/ {rhs}"),
|
Expression::Join { lhs: None, rhs } => write!(f, "/ {rhs}"),
|
||||||
Expression::Join {
|
Expression::Join {
|
||||||
@ -58,15 +62,10 @@ impl<'src> Display for Expression<'src> {
|
|||||||
} => write!(f, "{lhs} / {rhs}"),
|
} => write!(f, "{lhs} / {rhs}"),
|
||||||
Expression::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"),
|
Expression::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"),
|
||||||
Expression::Conditional {
|
Expression::Conditional {
|
||||||
lhs,
|
condition,
|
||||||
rhs,
|
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
operator,
|
} => write!(f, "if {condition} {{ {then} }} else {{ {otherwise} }}"),
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"if {lhs} {operator} {rhs} {{ {then} }} else {{ {otherwise} }}"
|
|
||||||
),
|
|
||||||
Expression::StringLiteral { string_literal } => write!(f, "{string_literal}"),
|
Expression::StringLiteral { string_literal } => write!(f, "{string_literal}"),
|
||||||
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
||||||
Expression::Call { thunk } => write!(f, "{thunk}"),
|
Expression::Call { thunk } => write!(f, "{thunk}"),
|
||||||
@ -81,6 +80,13 @@ impl<'src> Serialize for Expression<'src> {
|
|||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
|
Self::Assert { condition, error } => {
|
||||||
|
let mut seq: <S as Serializer>::SerializeSeq = serializer.serialize_seq(None)?;
|
||||||
|
seq.serialize_element("assert")?;
|
||||||
|
seq.serialize_element(condition)?;
|
||||||
|
seq.serialize_element(error)?;
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
Self::Backtick { contents, .. } => {
|
Self::Backtick { contents, .. } => {
|
||||||
let mut seq = serializer.serialize_seq(None)?;
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
seq.serialize_element("evaluate")?;
|
seq.serialize_element("evaluate")?;
|
||||||
@ -103,17 +109,13 @@ impl<'src> Serialize for Expression<'src> {
|
|||||||
seq.end()
|
seq.end()
|
||||||
}
|
}
|
||||||
Self::Conditional {
|
Self::Conditional {
|
||||||
lhs,
|
condition,
|
||||||
rhs,
|
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
operator,
|
|
||||||
} => {
|
} => {
|
||||||
let mut seq = serializer.serialize_seq(None)?;
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
seq.serialize_element("if")?;
|
seq.serialize_element("if")?;
|
||||||
seq.serialize_element(&operator.to_string())?;
|
seq.serialize_element(condition)?;
|
||||||
seq.serialize_element(lhs)?;
|
|
||||||
seq.serialize_element(rhs)?;
|
|
||||||
seq.serialize_element(then)?;
|
seq.serialize_element(then)?;
|
||||||
seq.serialize_element(otherwise)?;
|
seq.serialize_element(otherwise)?;
|
||||||
seq.end()
|
seq.end()
|
||||||
|
@ -6,6 +6,7 @@ pub(crate) enum Keyword {
|
|||||||
Alias,
|
Alias,
|
||||||
AllowDuplicateRecipes,
|
AllowDuplicateRecipes,
|
||||||
AllowDuplicateVariables,
|
AllowDuplicateVariables,
|
||||||
|
Assert,
|
||||||
DotenvFilename,
|
DotenvFilename,
|
||||||
DotenvLoad,
|
DotenvLoad,
|
||||||
DotenvPath,
|
DotenvPath,
|
||||||
|
20
src/lib.rs
20
src/lib.rs
@ -19,15 +19,16 @@ pub(crate) use {
|
|||||||
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
||||||
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
||||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
||||||
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
|
condition::Condition, conditional_operator::ConditionalOperator, config::Config,
|
||||||
count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat,
|
config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
|
||||||
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression,
|
dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator,
|
||||||
fragment::Fragment, function::Function, function_context::FunctionContext,
|
expression::Expression, fragment::Fragment, function::Function,
|
||||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
function_context::FunctionContext, interrupt_guard::InterruptGuard,
|
||||||
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
|
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed,
|
||||||
load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
|
keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv,
|
||||||
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
|
loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal, output::output,
|
||||||
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position,
|
output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser,
|
||||||
|
platform::Platform, platform_interface::PlatformInterface, position::Position,
|
||||||
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
|
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
|
||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
|
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
|
||||||
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
||||||
@ -124,6 +125,7 @@ mod compile_error;
|
|||||||
mod compile_error_kind;
|
mod compile_error_kind;
|
||||||
mod compiler;
|
mod compiler;
|
||||||
mod completions;
|
mod completions;
|
||||||
|
mod condition;
|
||||||
mod conditional_operator;
|
mod conditional_operator;
|
||||||
mod config;
|
mod config;
|
||||||
mod config_error;
|
mod config_error;
|
||||||
|
12
src/node.rs
12
src/node.rs
@ -83,13 +83,19 @@ impl<'src> Node<'src> for Assignment<'src> {
|
|||||||
impl<'src> Node<'src> for Expression<'src> {
|
impl<'src> Node<'src> for Expression<'src> {
|
||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
match self {
|
match self {
|
||||||
|
Expression::Assert {
|
||||||
|
condition: Condition { lhs, rhs, operator },
|
||||||
|
error,
|
||||||
|
} => Tree::atom(Keyword::Assert.lexeme())
|
||||||
|
.push(lhs.tree())
|
||||||
|
.push(operator.to_string())
|
||||||
|
.push(rhs.tree())
|
||||||
|
.push(error.tree()),
|
||||||
Expression::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
Expression::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||||
Expression::Conditional {
|
Expression::Conditional {
|
||||||
lhs,
|
condition: Condition { lhs, rhs, operator },
|
||||||
rhs,
|
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
operator,
|
|
||||||
} => {
|
} => {
|
||||||
let mut tree = Tree::atom(Keyword::If.lexeme());
|
let mut tree = Tree::atom(Keyword::If.lexeme());
|
||||||
tree.push_mut(lhs.tree());
|
tree.push_mut(lhs.tree());
|
||||||
|
@ -504,18 +504,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
|
|
||||||
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
|
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
|
||||||
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
let lhs = self.parse_expression()?;
|
let condition = self.parse_condition()?;
|
||||||
|
|
||||||
let operator = if self.accepted(BangEquals)? {
|
|
||||||
ConditionalOperator::Inequality
|
|
||||||
} else if self.accepted(EqualsTilde)? {
|
|
||||||
ConditionalOperator::RegexMatch
|
|
||||||
} else {
|
|
||||||
self.expect(EqualsEquals)?;
|
|
||||||
ConditionalOperator::Equality
|
|
||||||
};
|
|
||||||
|
|
||||||
let rhs = self.parse_expression()?;
|
|
||||||
|
|
||||||
self.expect(BraceL)?;
|
self.expect(BraceL)?;
|
||||||
|
|
||||||
@ -535,10 +524,26 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Expression::Conditional {
|
Ok(Expression::Conditional {
|
||||||
lhs: Box::new(lhs),
|
condition,
|
||||||
rhs: Box::new(rhs),
|
|
||||||
then: Box::new(then),
|
then: Box::new(then),
|
||||||
otherwise: Box::new(otherwise),
|
otherwise: Box::new(otherwise),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_condition(&mut self) -> CompileResult<'src, Condition<'src>> {
|
||||||
|
let lhs = self.parse_expression()?;
|
||||||
|
let operator = if self.accepted(BangEquals)? {
|
||||||
|
ConditionalOperator::Inequality
|
||||||
|
} else if self.accepted(EqualsTilde)? {
|
||||||
|
ConditionalOperator::RegexMatch
|
||||||
|
} else {
|
||||||
|
self.expect(EqualsEquals)?;
|
||||||
|
ConditionalOperator::Equality
|
||||||
|
};
|
||||||
|
let rhs = self.parse_expression()?;
|
||||||
|
Ok(Condition {
|
||||||
|
lhs: Box::new(lhs),
|
||||||
|
rhs: Box::new(rhs),
|
||||||
operator,
|
operator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -564,18 +569,26 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
if contents.starts_with("#!") {
|
if contents.starts_with("#!") {
|
||||||
return Err(next.error(CompileErrorKind::BacktickShebang));
|
return Err(next.error(CompileErrorKind::BacktickShebang));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Expression::Backtick { contents, token })
|
Ok(Expression::Backtick { contents, token })
|
||||||
} else if self.next_is(Identifier) {
|
} else if self.next_is(Identifier) {
|
||||||
let name = self.parse_name()?;
|
if self.accepted_keyword(Keyword::Assert)? {
|
||||||
|
self.expect(ParenL)?;
|
||||||
if self.next_is(ParenL) {
|
let condition = self.parse_condition()?;
|
||||||
let arguments = self.parse_sequence()?;
|
self.expect(Comma)?;
|
||||||
Ok(Expression::Call {
|
let error = Box::new(self.parse_expression()?);
|
||||||
thunk: Thunk::resolve(name, arguments)?,
|
self.expect(ParenR)?;
|
||||||
})
|
Ok(Expression::Assert { condition, error })
|
||||||
} else {
|
} else {
|
||||||
Ok(Expression::Variable { name })
|
let name = self.parse_name()?;
|
||||||
|
|
||||||
|
if self.next_is(ParenL) {
|
||||||
|
let arguments = self.parse_sequence()?;
|
||||||
|
Ok(Expression::Call {
|
||||||
|
thunk: Thunk::resolve(name, arguments)?,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Expression::Variable { name })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if self.next_is(ParenL) {
|
} else if self.next_is(ParenL) {
|
||||||
self.presume(ParenL)?;
|
self.presume(ParenL)?;
|
||||||
@ -2103,6 +2116,18 @@ mod tests {
|
|||||||
tree: (justfile (mod ? foo "some/file/path.txt")),
|
tree: (justfile (mod ? foo "some/file/path.txt")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: assert,
|
||||||
|
text: "a := assert(foo == \"bar\", \"error\")",
|
||||||
|
tree: (justfile (assignment a (assert foo == "bar" "error"))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: assert_conditional_condition,
|
||||||
|
text: "foo := assert(if a != b { c } else { d } == \"abc\", \"error\")",
|
||||||
|
tree: (justfile (assignment foo (assert (if a != b c d) == "abc" "error"))),
|
||||||
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: alias_syntax_multiple_rhs,
|
name: alias_syntax_multiple_rhs,
|
||||||
input: "alias foo := bar baz",
|
input: "alias foo := bar baz",
|
||||||
|
@ -19,9 +19,9 @@ use {
|
|||||||
|
|
||||||
mod full {
|
mod full {
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
assignment::Assignment, conditional_operator::ConditionalOperator, dependency::Dependency,
|
assignment::Assignment, condition::Condition, conditional_operator::ConditionalOperator,
|
||||||
expression::Expression, fragment::Fragment, justfile::Justfile, line::Line,
|
dependency::Dependency, expression::Expression, fragment::Fragment, justfile::Justfile,
|
||||||
parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk,
|
line::Line, parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +183,10 @@ impl Assignment {
|
|||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
pub enum Expression {
|
pub enum Expression {
|
||||||
|
Assert {
|
||||||
|
condition: Condition,
|
||||||
|
error: Box<Expression>,
|
||||||
|
},
|
||||||
Backtick {
|
Backtick {
|
||||||
command: String,
|
command: String,
|
||||||
},
|
},
|
||||||
@ -217,6 +221,17 @@ impl Expression {
|
|||||||
fn new(expression: &full::Expression) -> Expression {
|
fn new(expression: &full::Expression) -> Expression {
|
||||||
use full::Expression::*;
|
use full::Expression::*;
|
||||||
match expression {
|
match expression {
|
||||||
|
Assert {
|
||||||
|
condition: full::Condition { lhs, rhs, operator },
|
||||||
|
error,
|
||||||
|
} => Expression::Assert {
|
||||||
|
condition: Condition {
|
||||||
|
lhs: Box::new(Expression::new(lhs)),
|
||||||
|
rhs: Box::new(Expression::new(rhs)),
|
||||||
|
operator: ConditionalOperator::new(*operator),
|
||||||
|
},
|
||||||
|
error: Box::new(Expression::new(error)),
|
||||||
|
},
|
||||||
Backtick { contents, .. } => Expression::Backtick {
|
Backtick { contents, .. } => Expression::Backtick {
|
||||||
command: (*contents).clone(),
|
command: (*contents).clone(),
|
||||||
},
|
},
|
||||||
@ -284,10 +299,8 @@ impl Expression {
|
|||||||
rhs: Box::new(Expression::new(rhs)),
|
rhs: Box::new(Expression::new(rhs)),
|
||||||
},
|
},
|
||||||
Conditional {
|
Conditional {
|
||||||
lhs,
|
condition: full::Condition { lhs, rhs, operator },
|
||||||
operator,
|
|
||||||
otherwise,
|
otherwise,
|
||||||
rhs,
|
|
||||||
then,
|
then,
|
||||||
} => Expression::Conditional {
|
} => Expression::Conditional {
|
||||||
lhs: Box::new(Expression::new(lhs)),
|
lhs: Box::new(Expression::new(lhs)),
|
||||||
@ -307,6 +320,13 @@ impl Expression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
|
pub struct Condition {
|
||||||
|
lhs: Box<Expression>,
|
||||||
|
rhs: Box<Expression>,
|
||||||
|
operator: ConditionalOperator,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
pub enum ConditionalOperator {
|
pub enum ConditionalOperator {
|
||||||
Equality,
|
Equality,
|
||||||
|
@ -49,11 +49,14 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Expression::Conditional {
|
Expression::Conditional {
|
||||||
lhs,
|
condition:
|
||||||
rhs,
|
Condition {
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
operator: _,
|
||||||
|
},
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
self.stack.push(otherwise);
|
self.stack.push(otherwise);
|
||||||
self.stack.push(then);
|
self.stack.push(then);
|
||||||
@ -74,6 +77,19 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
Expression::Group { contents } => {
|
Expression::Group { contents } => {
|
||||||
self.stack.push(contents);
|
self.stack.push(contents);
|
||||||
}
|
}
|
||||||
|
Expression::Assert {
|
||||||
|
condition:
|
||||||
|
Condition {
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
operator: _,
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
} => {
|
||||||
|
self.stack.push(error);
|
||||||
|
self.stack.push(rhs);
|
||||||
|
self.stack.push(lhs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
tests/assertions.rs
Normal file
22
tests/assertions.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: assert_pass,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
{{ assert('a' == 'a', 'error message') }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: assert_fail,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
{{ assert('a' != 'a', 'error message') }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Assert failed: error message\n",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
@ -262,7 +262,7 @@ fn dependency_argument() {
|
|||||||
["concatenate", "a", "b"],
|
["concatenate", "a", "b"],
|
||||||
["evaluate", "echo"],
|
["evaluate", "echo"],
|
||||||
["variable", "x"],
|
["variable", "x"],
|
||||||
["if", "==", "a", "b", "c", "d"],
|
["if", ["==", "a", "b"], "c", "d"],
|
||||||
["call", "arch"],
|
["call", "arch"],
|
||||||
["call", "env_var", "foo"],
|
["call", "env_var", "foo"],
|
||||||
["call", "join", "a", "b"],
|
["call", "join", "a", "b"],
|
||||||
|
@ -36,6 +36,7 @@ mod allow_duplicate_recipes;
|
|||||||
mod allow_duplicate_variables;
|
mod allow_duplicate_variables;
|
||||||
mod assert_stdout;
|
mod assert_stdout;
|
||||||
mod assert_success;
|
mod assert_success;
|
||||||
|
mod assertions;
|
||||||
mod attributes;
|
mod attributes;
|
||||||
mod backticks;
|
mod backticks;
|
||||||
mod byte_order_mark;
|
mod byte_order_mark;
|
||||||
|
Loading…
Reference in New Issue
Block a user