diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 85e3a698c..5d416a9f3 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -906,7 +906,7 @@ impl Expr { } } } - Self::FnCall(x, ..) => { + Self::FnCall(x, ..) | Self::MethodCall(x, ..) => { for e in &*x.args { if !e.walk(path, on_node) { return false; diff --git a/tests/method_call.rs b/tests/method_call.rs index 46013a919..daeff15b0 100644 --- a/tests/method_call.rs +++ b/tests/method_call.rs @@ -1,4 +1,6 @@ #![cfg(not(feature = "no_object"))] +#[cfg(feature = "internals")] +use rhai::{ASTNode, Expr}; use rhai::{Engine, EvalAltResult, INT}; #[derive(Debug, Clone, Eq, PartialEq)] @@ -133,3 +135,92 @@ fn test_method_call_typed() { EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("foo") )); } + +/// AST walk tests — verify that `walk` visits arguments inside `MethodCall` nodes. + +#[test] +#[cfg(feature = "internals")] +fn test_method_call_walk_visits_args() { + let engine = Engine::new(); + // `my_array.contains(value)` — `value` is an argument of a MethodCall node. + let ast = engine.compile("my_array.contains(value)").unwrap(); + + let mut vars: Vec = Vec::new(); + ast.walk(&mut |nodes: &[ASTNode]| { + if let Some(ASTNode::Expr(Expr::Variable(info, _, _))) = nodes.last() { + vars.push(info.1.to_string()); + } + true + }); + + assert!(vars.contains(&"my_array".to_string()), "walk should visit the receiver `my_array`"); + assert!(vars.contains(&"value".to_string()), "walk should visit the argument `value`"); +} + +#[test] +#[cfg(feature = "internals")] +fn test_method_call_walk_visits_multiple_args() { + let engine = Engine::new(); + // Three variable arguments — all must be visited. + let ast = engine.compile("obj.foo(a, b, c)").unwrap(); + + let mut vars: Vec = Vec::new(); + ast.walk(&mut |nodes: &[ASTNode]| { + if let Some(ASTNode::Expr(Expr::Variable(info, _, _))) = nodes.last() { + vars.push(info.1.to_string()); + } + true + }); + + for name in &["obj", "a", "b", "c"] { + assert!(vars.contains(&name.to_string()), "walk should visit `{name}`", name = name); + } +} + +#[test] +#[cfg(feature = "internals")] +fn test_method_call_walk_visits_nested_expr_in_arg() { + let engine = Engine::new(); + // The argument itself contains a variable (`n`) inside an expression. + let ast = engine.compile("obj.foo(n + 1)").unwrap(); + + let mut vars: Vec = Vec::new(); + ast.walk(&mut |nodes: &[ASTNode]| { + if let Some(ASTNode::Expr(Expr::Variable(info, _, _))) = nodes.last() { + vars.push(info.1.to_string()); + } + true + }); + + assert!(vars.contains(&"obj".to_string()), "walk should visit the receiver `obj`"); + assert!(vars.contains(&"n".to_string()), "walk should visit `n` nested inside the arg expression"); +} + +#[test] +#[cfg(feature = "internals")] +fn test_method_call_walk_count_visits_matches_fn_call() { + // `obj.foo(x)` (method-call syntax) and `foo(obj, x)` (free-function syntax) + // should both surface the same two variable names via `walk`. + let engine = Engine::new(); + + let count_vars = |src: &str| -> Vec { + let ast = engine.compile(src).unwrap(); + let mut vars = Vec::new(); + ast.walk(&mut |nodes: &[ASTNode]| { + if let Some(ASTNode::Expr(Expr::Variable(info, _, _))) = nodes.last() { + vars.push(info.1.to_string()); + } + true + }); + vars + }; + + let method_vars = count_vars("obj.foo(x)"); + let fn_vars = count_vars("foo(obj, x)"); + + // Both forms must surface `obj` and `x`. + for name in &["obj", "x"] { + assert!(method_vars.contains(&name.to_string()), "method syntax: walk should visit `{name}`", name = name); + assert!(fn_vars.contains(&name.to_string()), "free-fn syntax: walk should visit `{name}`", name = name); + } +}