use super::*;
pub(crate) fn extract_modifies_targets(expr: &SpExpr) -> Vec<String> {
let mut targets = Vec::new();
collect_modifies_targets(expr, &mut targets);
targets
}
fn collect_modifies_targets(expr: &SpExpr, targets: &mut Vec<String>) {
match &expr.node {
Expr::Ident(name) => {
targets.push(name.clone());
}
Expr::Field(receiver, field) => {
let mut path = String::new();
build_field_path(receiver, &mut path);
if !path.is_empty() {
path.push('.');
}
path.push_str(field);
targets.push(path);
}
Expr::Block(exprs) => {
for e in exprs {
collect_modifies_targets(e, targets);
}
}
Expr::List(items) => {
for item in items {
collect_modifies_targets(item, targets);
}
}
Expr::Raw(tokens) => {
for tok in tokens {
let trimmed = tok.trim();
if !trimmed.is_empty() && trimmed != "," {
targets.push(trimmed.to_string());
}
}
}
_ => {}
}
}
fn build_field_path(expr: &SpExpr, path: &mut String) {
match &expr.node {
Expr::Ident(name) => {
path.push_str(name);
}
Expr::Field(receiver, field) => {
build_field_path(receiver, path);
path.push('.');
path.push_str(field);
}
_ => {}
}
}
pub(crate) fn collect_old_references(expr: &SpExpr) -> Vec<String> {
struct OldRefCollector(Vec<String>);
impl ExprVisitor for OldRefCollector {
fn visit_old(&mut self, inner: &SpExpr) {
match &inner.node {
Expr::Ident(name) => self.0.push(name.clone()),
Expr::Field(receiver, field) => {
let mut path = String::new();
build_field_path(receiver, &mut path);
if !path.is_empty() {
path.push('.');
}
path.push_str(field);
self.0.push(path);
}
_ => {}
}
self.visit_expr(inner);
}
}
let mut c = OldRefCollector(Vec::new());
c.visit_expr(expr);
c.0
}
pub(crate) fn collect_frame_equality_vars(expr: &SpExpr) -> std::collections::HashSet<String> {
struct FrameEqCollector(std::collections::HashSet<String>);
impl ExprVisitor for FrameEqCollector {
fn visit_binop(&mut self, lhs: &SpExpr, op: &BinOp, rhs: &SpExpr) {
if *op == BinOp::Eq {
if let (Expr::Ident(name), Expr::Old(inner)) = (&lhs.node, &rhs.node)
&& let Expr::Ident(old_name) = &inner.node
&& name == old_name
{
self.0.insert(name.clone());
}
if let (Expr::Old(inner), Expr::Ident(name)) = (&lhs.node, &rhs.node)
&& let Expr::Ident(old_name) = &inner.node
&& name == old_name
{
self.0.insert(name.clone());
}
}
self.visit_expr(lhs);
self.visit_expr(rhs);
}
}
let mut c = FrameEqCollector(std::collections::HashSet::new());
c.visit_expr(expr);
c.0
}
pub(crate) fn collect_ident_references(expr: &SpExpr) -> Vec<String> {
struct IdentRefCollector(Vec<String>);
impl ExprVisitor for IdentRefCollector {
fn visit_old(&mut self, _inner: &SpExpr) {
}
fn visit_ident(&mut self, name: &str) {
if name != "true" && name != "false" && name != "result" && name != "self" {
self.0.push(name.to_string());
}
}
fn visit_field(&mut self, base: &SpExpr, field: &str) {
let mut path = String::new();
build_field_path(base, &mut path);
if !path.is_empty() {
path.push('.');
}
path.push_str(field);
self.0.push(path);
self.visit_expr(base);
}
}
let mut c = IdentRefCollector(Vec::new());
c.visit_expr(expr);
c.0
}
#[cfg(test)]
mod tests {
use super::*;
use assura_parser::ast::Spanned;
fn ident(s: &str) -> SpExpr {
Spanned::no_span(Expr::Ident(s.to_string()))
}
#[test]
fn extract_modifies_single_ident() {
let expr = ident("x");
let targets = extract_modifies_targets(&expr);
assert_eq!(targets, vec!["x"]);
}
#[test]
fn extract_modifies_block_of_idents() {
let expr = Spanned::no_span(Expr::Block(vec![ident("a"), ident("b"), ident("c")]));
let targets = extract_modifies_targets(&expr);
assert_eq!(targets, vec!["a", "b", "c"]);
}
#[test]
fn extract_modifies_field_path() {
let expr = Spanned::no_span(Expr::Field(Box::new(ident("obj")), "field".into()));
let targets = extract_modifies_targets(&expr);
assert_eq!(targets, vec!["obj.field"]);
}
#[test]
fn collect_old_references_ident() {
let expr = Spanned::no_span(Expr::Old(Box::new(ident("x"))));
let refs = collect_old_references(&expr);
assert!(refs.contains(&"x".to_string()));
}
#[test]
fn collect_old_references_field() {
let inner = Spanned::no_span(Expr::Field(Box::new(ident("obj")), "val".into()));
let expr = Spanned::no_span(Expr::Old(Box::new(inner)));
let refs = collect_old_references(&expr);
assert!(refs.contains(&"obj.val".to_string()));
}
#[test]
fn collect_frame_equality_vars_eq_old() {
let expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(ident("x")),
op: BinOp::Eq,
rhs: Box::new(Spanned::no_span(Expr::Old(Box::new(ident("x"))))),
});
let vars = collect_frame_equality_vars(&expr);
assert!(vars.contains("x"), "x == old(x) should be detected");
}
#[test]
fn collect_frame_equality_vars_old_eq() {
let expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(Spanned::no_span(Expr::Old(Box::new(ident("y"))))),
op: BinOp::Eq,
rhs: Box::new(ident("y")),
});
let vars = collect_frame_equality_vars(&expr);
assert!(vars.contains("y"), "old(y) == y should be detected");
}
#[test]
fn collect_frame_equality_vars_gt_not_frame() {
let expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(ident("x")),
op: BinOp::Gt,
rhs: Box::new(Spanned::no_span(Expr::Old(Box::new(ident("x"))))),
});
let vars = collect_frame_equality_vars(&expr);
assert!(vars.is_empty(), "x > old(x) is not a frame equality");
}
#[test]
fn collect_frame_equality_vars_mismatched_names() {
let expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(ident("x")),
op: BinOp::Eq,
rhs: Box::new(Spanned::no_span(Expr::Old(Box::new(ident("y"))))),
});
let vars = collect_frame_equality_vars(&expr);
assert!(vars.is_empty(), "x == old(y) is not a frame equality");
}
#[test]
fn collect_frame_equality_nested_in_and() {
let eq_expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(ident("x")),
op: BinOp::Eq,
rhs: Box::new(Spanned::no_span(Expr::Old(Box::new(ident("x"))))),
});
let gt_expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(ident("z")),
op: BinOp::Gt,
rhs: Box::new(Spanned::no_span(Expr::Literal(Literal::Int("0".into())))),
});
let expr = Spanned::no_span(Expr::BinOp {
lhs: Box::new(eq_expr),
op: BinOp::And,
rhs: Box::new(gt_expr),
});
let vars = collect_frame_equality_vars(&expr);
assert!(
vars.contains("x"),
"x == old(x) inside && should be detected"
);
}
#[test]
fn collect_ident_references_skips_builtins() {
let expr = Spanned::no_span(Expr::Block(vec![
ident("x"),
ident("result"),
ident("true"),
ident("self"),
ident("y"),
]));
let refs = collect_ident_references(&expr);
assert!(refs.contains(&"x".to_string()));
assert!(refs.contains(&"y".to_string()));
assert!(!refs.contains(&"result".to_string()));
assert!(!refs.contains(&"true".to_string()));
assert!(!refs.contains(&"self".to_string()));
}
}