use vantage_core::{Result, error};
use vantage_rhai::rhai::{AST, Expr, FnPtr, Stmt};
const NARROWING_VERBS: [&str; 6] = ["where", "sort", "search", "limit", "ref", "with_condition"];
pub fn reject_dropped_narrowing(ast: &AST, what: &'static str, has_value: bool) -> Result<()> {
check_block(ast.statements(), has_value, what)
}
fn check_block(statements: &[Stmt], in_value_position: bool, what: &'static str) -> Result<()> {
let last = statements.len().saturating_sub(1);
for (i, statement) in statements.iter().enumerate() {
check_statement(statement, in_value_position && i == last, what)?;
}
Ok(())
}
fn check_statement(statement: &Stmt, in_value_position: bool, what: &'static str) -> Result<()> {
match statement {
Stmt::Expr(expr) => match expr.as_ref() {
Expr::Stmt(block) => check_block(block.statements(), in_value_position, what),
expr if !in_value_position && ends_on_narrowing(expr) => Err(error!(
"this statement's table handle is discarded; chain the calls or assign it",
script = what,
line = statement.position().line().unwrap_or(0)
)
.mark_incorrect_usage()),
_ => Ok(()),
},
Stmt::Block(block) => check_block(block.statements(), in_value_position, what),
Stmt::If(flow, _) | Stmt::TryCatch(flow, _) => {
check_block(flow.body.statements(), in_value_position, what)?;
check_block(flow.branch.statements(), in_value_position, what)
}
Stmt::While(flow, _) | Stmt::Do(flow, ..) => {
check_block(flow.body.statements(), false, what)
}
Stmt::For(parts, _) => check_block(parts.2.body.statements(), false, what),
_ => Ok(()),
}
}
fn ends_on_narrowing(expr: &Expr) -> bool {
let mut last = expr;
while let Expr::Dot(chain, ..) = last {
last = &chain.rhs;
}
if std::ptr::eq(last, expr) {
return false;
}
let Expr::MethodCall(call, _) = last else {
return false;
};
match call.name.as_str() {
"sort" => {
matches!(call.args.len(), 1 | 2) && !call.args.first().is_some_and(is_function_value)
}
name => NARROWING_VERBS.contains(&name),
}
}
fn is_function_value(expr: &Expr) -> bool {
match expr {
Expr::DynamicConstant(value, _) => value.is::<FnPtr>(),
Expr::FnCall(call, _) => is_fn_constructor(call.name.as_str()),
Expr::Stmt(block) => match block.statements().last() {
Some(Stmt::FnCall(call, _)) => is_fn_constructor(call.name.as_str()),
Some(Stmt::Expr(expr)) => is_function_value(expr),
_ => false,
},
_ => false,
}
}
fn is_fn_constructor(name: &str) -> bool {
matches!(name, "Fn" | "curry")
}
#[cfg(test)]
mod tests;