1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
use super::super::{
argument_value_node, build_call_event, call_argument_containers, call_event_value_source_names,
call_receiver_node, emit_inline_closure_param_bindings,
emit_inline_closure_param_bindings_from_yield_call, emit_invoked_lambda_param_bindings,
immediately_invoked_lambda_callee, is_closure_arg, is_comprehension_kind, walk_call_argument_expressions,
walk_lambda_body, walk_method_chain_receivers, FlowEvent, Node, SyntaxSpecialForm,
};
use super::{walk_into, LoweringContext};
pub(super) fn lower_call(node: Node<'_>, context: LoweringContext<'_>, out: &mut Vec<FlowEvent>) -> bool {
let LoweringContext {
file,
src,
handler,
class_names,
} = context;
let kind = node.kind();
if handler.is_call(kind) {
let call_event = build_call_event(node, file, src, handler, class_names);
if let Some(lambda) = immediately_invoked_lambda_callee(&node, handler) {
walk_call_argument_expressions(node, file, src, handler, class_names, out);
if let Some(event) = call_event.as_ref() {
emit_invoked_lambda_param_bindings(lambda, file, src, handler, event, out);
}
walk_lambda_body(lambda, file, src, handler, class_names, out);
return true;
}
if let Some(event) = call_event.clone() {
out.push(event);
}
let closure_source_names = call_event
.as_ref()
.map(call_event_value_source_names)
.unwrap_or_default();
// Also descend into nested calls inside arguments. For lambda
// arguments (`xs.forEach { x -> body }`, `xs.map(x => body)`,
// `[...].forEach(x => body)`), inline the closure body into
// the OUTER flow — higher-order-function calls execute the
// closure as part of the caller's behavior, so their calls
// are flow-relevant and shouldn't be lost to the usual
// is_lambda short-circuit.
// Objective-C message sends carry their arguments as direct
// children interleaved with `method:` keyword selectors, not in an
// `arguments` container — so `call_argument_containers` finds none
// and a call nested in an arg position (`[self run:[self wrap:s]]`,
// `[self sink:strlen(s)]`) never surfaced its inner call. Walk each
// non-receiver, non-method child so nested message/function calls
// emit their own Call events.
if handler.has_special_form(SyntaxSpecialForm::DirectCallArguments)
&& call_argument_containers(node, handler).is_empty()
{
let mut cur = node.walk();
if cur.goto_first_child() {
loop {
let child = cur.node();
if child.is_named()
&& !cur.field_name().is_some_and(|field| {
handler.direct_call_argument_excluded_fields.contains(&field)
})
{
walk_into(child, file, src, handler, class_names, out, false);
}
if !cur.goto_next_sibling() {
break;
}
}
}
}
let arg_containers = call_argument_containers(node, handler);
let mut walked_closures = std::collections::HashSet::new();
for container in arg_containers {
// `any(f(t) for t in xs)` / `list(g(t) for t in xs)`: python
// exposes the bare generator_expression DIRECTLY as the call's
// `arguments` field (no `argument_list` wrapper). Iterating its
// children would walk the body call and `for_in_clause`
// separately — losing the loop-variable binding so the sink's
// arg stays untainted. Walk the comprehension AS a whole so the
// COMPREHENSION_KINDS branch binds the iterator and emits the body.
if is_comprehension_kind(container.kind(), handler) {
walk_into(container, file, src, handler, class_names, out, false);
continue;
}
// Some grammars expose a single call DIRECTLY as the
// `arguments` field with no argument-list wrapper — Perl
// `sink(source())` parses as `function_call_expression`
// whose `arguments` field IS the `source()` call. Iterating
// its children would walk `source` as a bare identifier and
// drop the nested Call (so a source rule on `source()` never
// matches). Walk the container whole so build_call_event
// fires on the nested call and its own args recurse.
if handler.is_call(container.kind()) {
walk_into(container, file, src, handler, class_names, out, false);
continue;
}
let mut cursor = container.walk();
for arg in container.named_children(&mut cursor) {
// Several grammars wrap each argument in a dedicated
// node (C# `argument`, Kotlin `value_argument`, Python
// `keyword_argument`, C# `named_argument`). A closure
// hidden inside such a wrapper would otherwise recurse
// into `walk_into`, hit the lambda short-circuit, and
// vanish — while the standalone-decl pass ALSO skips it
// (it sees a call ancestor and assumes it was inlined
// here). Unwrap one level so wrapped closures inline
// exactly like direct positional ones. `pair` /
// object-literal values stay decl-owned on purpose —
// unwrapping those would create double ownership with
// the standalone-decl pass.
let closure_node = if is_closure_arg(arg.kind(), handler) {
Some(arg)
} else if handler.argument_wrapper_kinds.contains(&arg.kind()) {
let value = argument_value_node(arg, src, handler);
is_closure_arg(value.kind(), handler).then_some(value)
} else {
None
};
if let Some(closure) = closure_node {
walked_closures.insert(arg.id());
walked_closures.insert(closure.id());
emit_inline_closure_param_bindings(
closure,
file,
src,
handler,
&closure_source_names,
out,
);
// Inline the lambda body so its calls belong to
// the enclosing function. Walks via a helper that
// bypasses the is_lambda short-circuit.
walk_lambda_body(closure, file, src, handler, class_names, out);
} else {
walk_into(arg, file, src, handler, class_names, out, false);
}
}
}
// Some grammars attach the trailing-closure block as a direct
// sibling of the call (Ruby `xs.each { |x| ... }` — the `block`
// is a top-level child of the call, not inside any arguments
// container). Kotlin and Swift place the closure one level under an
// adapter-declared call suffix. Scan only those exact direct/wrapper
// roles and inline their bodies too.
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
let closures = if is_closure_arg(child.kind(), handler) {
vec![child]
} else if handler.call_argument_wrapper_kinds.contains(&child.kind()) {
let mut wrapper_cursor = child.walk();
child
.named_children(&mut wrapper_cursor)
.filter(|nested| is_closure_arg(nested.kind(), handler))
.collect()
} else {
Vec::new()
};
for closure in closures {
if handler.has_special_form(SyntaxSpecialForm::DirectDoBlockBody)
&& handler.lambda_body_kinds.contains(&closure.kind())
{
continue;
}
if !walked_closures.insert(closure.id()) {
continue;
}
if handler
.inline_closure_yield_extractor
.is_some_and(|extract| extract(node, closure, src))
{
emit_inline_closure_param_bindings_from_yield_call(
closure,
file,
src,
handler,
call_event.as_ref(),
out,
);
} else {
emit_inline_closure_param_bindings(
closure,
file,
src,
handler,
&closure_source_names,
out,
);
}
walk_lambda_body(closure, file, src, handler, class_names, out);
}
}
// Elixir-specific: control-flow constructs (`case`, `cond`,
// `if`, `with`, `try`, `receive`, `for`) are all parsed as
// `call` nodes whose body lives in a `do_block` direct child.
// Without this descent, calls inside those bodies wouldn't
// surface in the enclosing function's flow events.
if handler.has_special_form(SyntaxSpecialForm::DirectDoBlockBody) {
let do_block = {
let mut cursor = node.walk();
let body = node
.named_children(&mut cursor)
.find(|child| handler.lambda_body_kinds.contains(&child.kind()));
body
};
if let Some(do_block) = do_block {
let mut do_cursor = do_block.walk();
for child in do_block.named_children(&mut do_cursor) {
walk_into(child, file, src, handler, class_names, out, false);
}
}
}
// Method-chain receivers. Rust / Swift / Kotlin / JS / TS
// parse `a.b().c().d()` as a nested call_expression chain
// where each step's `function` field wraps the previous step
// as its receiver. Without descending into the function /
// callee field, only the outermost call emits an event and
// the inner calls' structured args are lost. Walk through
// field-access wrappers (`field_expression`,
// `member_expression`, navigation_expression, etc.) to
// reach any nested call_expression and emit a proper event
// per inner call.
let receiver_node = call_receiver_node(&node, src, handler).or_else(|| {
handler
.call_callee_field_names
.iter()
.find_map(|field| node.child_by_field_name(field))
});
if let Some(recv) = receiver_node {
walk_method_chain_receivers(recv, file, src, handler, class_names, out);
}
return true;
}
false
}