Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
91 changes: 91 additions & 0 deletions tests/method_call.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
use rhai::{ASTNode, Expr};
use rhai::{Engine, EvalAltResult, INT};

Check warning on line 4 in tests/method_call.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,sync,no_time,no_function,no_float,no_position,no...

unused import: `EvalAltResult`

#[derive(Debug, Clone, Eq, PartialEq)]
struct TestStruct {
Expand Down Expand Up @@ -133,3 +135,92 @@
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<String> = 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<String> = 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<String> = 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<String> {
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);
}
}
Loading