Skip to content

Commit 7bee8c8

Browse files
committed
fix: remove all parent() calls and fix PHP method call detection
1 parent b14c284 commit 7bee8c8

File tree

4 files changed

+99
-66
lines changed

4 files changed

+99
-66
lines changed

native/queries/php-calls.scm

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
; =============================================================
44

55
; Direct function calls: foo(), strlen($s)
6+
; Must be checked first to avoid being captured by method call patterns
67
(function_call_expression
78
function: (name) @callee.name) @call
89

@@ -11,17 +12,17 @@
1112
function: (qualified_name
1213
(name) @callee.name)) @call
1314

15+
; Static method calls: Foo::bar(), self::method()
16+
(scoped_call_expression
17+
name: (name) @callee.name) @static.call
18+
1419
; Method calls: $obj->method()
1520
(member_call_expression
16-
name: (name) @callee.name) @call
21+
name: (name) @callee.name) @method.call
1722

1823
; Nullsafe method calls: $obj?->method()
1924
(nullsafe_member_call_expression
20-
name: (name) @callee.name) @call
21-
22-
; Static method calls: Foo::bar(), self::method()
23-
(scoped_call_expression
24-
name: (name) @callee.name) @call
25+
name: (name) @callee.name) @method.call
2526

2627
; Constructor calls: new Foo()
2728
(object_creation_expression

native/src/call_extractor.rs

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ pub fn extract_calls(content: &str, language_name: &str) -> Result<Vec<CallSite>
3838
.set_language(&ts_language)
3939
.map_err(|e| anyhow!("Failed to set language: {}", e))?;
4040

41-
// Set a timeout to prevent infinite parsing loops
42-
parser.set_timeout_micros(5_000_000); // 5 seconds timeout
43-
4441
let tree = parser
4542
.parse(content, None)
4643
.ok_or_else(|| anyhow!("Parse failed"))?;
@@ -63,31 +60,15 @@ pub fn extract_calls(content: &str, language_name: &str) -> Result<Vec<CallSite>
6360
let query = Query::new(&ts_language, query_source)
6461
.map_err(|e| anyhow!("Failed to compile query: {}", e))?;
6562

66-
// Disable query analysis that causes the crash (tree-sitter bug with analysis_state__compare_position)
6763
let callee_name_idx = query.capture_index_for_name("callee.name");
6864
let call_idx = query.capture_index_for_name("call");
65+
let method_call_idx = query.capture_index_for_name("method.call");
66+
let static_call_idx = query.capture_index_for_name("static.call");
6967
let constructor_idx = query.capture_index_for_name("constructor");
7068
let import_name_idx = query.capture_index_for_name("import.name");
7169
let import_default_idx = query.capture_index_for_name("import.default");
7270
let import_namespace_idx = query.capture_index_for_name("import.namespace");
7371

74-
let method_parent_kinds: &[&str] = match language {
75-
Language::TypeScript
76-
| Language::TypeScriptTsx
77-
| Language::JavaScript
78-
| Language::JavaScriptJsx => &["member_expression"],
79-
Language::Python => &["attribute"],
80-
Language::Rust => &["field_expression"],
81-
Language::Go => &["selector_expression"],
82-
Language::Php => &[
83-
"member_call_expression",
84-
"scoped_call_expression",
85-
"nullsafe_member_call_expression",
86-
],
87-
_ => &[],
88-
};
89-
let _method_parent_kinds = method_parent_kinds;
90-
9172
let mut cursor = QueryCursor::new();
9273
let mut calls = Vec::new();
9374
let text_bytes = content.as_bytes();
@@ -114,8 +95,31 @@ pub fn extract_calls(content: &str, language_name: &str) -> Result<Vec<CallSite>
11495
}
11596

11697
if let Some(idx) = call_idx {
98+
if capture.index == idx {
99+
// Check if this is actually a method call by looking at other captures
100+
// If method_call_idx or static_call_idx also matches, it's a method call
101+
let is_method_call = match_.captures.iter().any(|c| {
102+
method_call_idx.map(|idx| c.index == idx).unwrap_or(false)
103+
|| static_call_idx.map(|idx| c.index == idx).unwrap_or(false)
104+
});
105+
106+
if is_method_call {
107+
call_type = Some(CallType::MethodCall);
108+
} else {
109+
call_type = Some(CallType::Call);
110+
}
111+
}
112+
}
113+
114+
if let Some(idx) = method_call_idx {
117115
if capture.index == idx && call_type.is_none() {
118-
call_type = Some(CallType::Call);
116+
call_type = Some(CallType::MethodCall);
117+
}
118+
}
119+
120+
if let Some(idx) = static_call_idx {
121+
if capture.index == idx && call_type.is_none() {
122+
call_type = Some(CallType::MethodCall);
119123
}
120124
}
121125

@@ -153,49 +157,35 @@ pub fn extract_calls(content: &str, language_name: &str) -> Result<Vec<CallSite>
153157
}
154158
}
155159

156-
// Safely detect method calls by checking the callee node's parent
157-
// instead of using query analysis which triggers tree-sitter bug
158-
// The callee.name node is a property_identifier for method calls,
159-
// and its parent is member_expression/attribute/etc.
160+
// Safely detect method calls using query context analysis
161+
// For PHP: check if the call is wrapped in a method call pattern
160162
if let (Some(name), Some(CallType::Call), Some(pos)) = (&callee_name, call_type, position) {
161-
// Find the callee.name capture node
162-
let callee_name_node = match_
163-
.captures
164-
.iter()
165-
.find(|c| callee_name_idx.map(|idx| c.index == idx).unwrap_or(false))
166-
.map(|c| c.node);
167-
168163
let is_method_call = match language {
169164
Language::TypeScript
170165
| Language::TypeScriptTsx
171166
| Language::JavaScript
172-
| Language::JavaScriptJsx => callee_name_node
173-
.and_then(|n| n.parent())
174-
.map(|p| p.kind() == "member_expression")
175-
.unwrap_or(false),
176-
Language::Python => callee_name_node
177-
.and_then(|n| n.parent())
178-
.map(|p| p.kind() == "attribute")
179-
.unwrap_or(false),
180-
Language::Rust => callee_name_node
181-
.and_then(|n| n.parent())
182-
.map(|p| p.kind() == "field_expression")
183-
.unwrap_or(false),
184-
Language::Go => callee_name_node
185-
.and_then(|n| n.parent())
186-
.map(|p| p.kind() == "selector_expression")
187-
.unwrap_or(false),
188-
Language::Php => callee_name_node
189-
.and_then(|n| n.parent())
190-
.map(|p| {
191-
matches!(
192-
p.kind(),
193-
"member_call_expression"
194-
| "scoped_call_expression"
195-
| "nullsafe_member_call_expression"
196-
)
197-
})
198-
.unwrap_or(false),
167+
| Language::JavaScriptJsx => match_.captures.iter().any(|c| {
168+
callee_name_idx.map(|idx| c.index == idx).unwrap_or(false)
169+
&& (c.node.kind() == "property_identifier" || c.node.kind() == "identifier")
170+
}),
171+
Language::Python => match_.captures.iter().any(|c| {
172+
callee_name_idx.map(|idx| c.index == idx).unwrap_or(false)
173+
&& c.node.kind() == "identifier"
174+
}),
175+
Language::Rust => match_.captures.iter().any(|c| {
176+
callee_name_idx.map(|idx| c.index == idx).unwrap_or(false)
177+
&& c.node.kind() == "field_identifier"
178+
}),
179+
Language::Go => match_.captures.iter().any(|c| {
180+
callee_name_idx.map(|idx| c.index == idx).unwrap_or(false)
181+
&& c.node.kind() == "field_identifier"
182+
}),
183+
Language::Php => {
184+
// PHP method calls are already marked in query (@method.call, @static.call)
185+
// @call is only for direct function calls
186+
// So if we reach here with CallType::Call, it's definitely not a method call
187+
false
188+
}
199189
_ => false,
200190
};
201191

tests/call-graph.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ describe("call-graph", () => {
4848
expect(createCall!.callType).toBe("MethodCall");
4949
});
5050

51+
it("should detect method calls using zero-allocation approach", () => {
52+
const content = fs.readFileSync(path.join(fixturesDir, "php-method-zeroalloc.php"), "utf-8");
53+
const calls = extractCalls(content, "php");
54+
55+
// Check that method calls are correctly identified without using parent() on callee.name
56+
const methodCalls = calls.filter((c) => c.callType === "MethodCall");
57+
expect(methodCalls.length).toBeGreaterThan(0);
58+
59+
// Verify specific method call patterns
60+
expect(methodCalls.some(c => c.calleeName === "process")).toBe(true);
61+
expect(methodCalls.some(c => c.calleeName === "validate")).toBe(true);
62+
});
63+
5164
it("should extract method calls", () => {
5265
const content = fs.readFileSync(path.join(fixturesDir, "method-calls.ts"), "utf-8");
5366
const calls = extractCalls(content, "typescript");
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
// Test zero-allocation method call detection
4+
// Uses method_call_expression patterns
5+
6+
class Service {
7+
public function process($data) {
8+
return $this->validate($data);
9+
}
10+
11+
public function validate($data) {
12+
return $data !== null;
13+
}
14+
15+
public static function create() {
16+
return new self();
17+
}
18+
}
19+
20+
// Direct method calls
21+
$service = new Service();
22+
$service->process("data");
23+
$service->validate("data");
24+
25+
// Static method calls
26+
Service::create();
27+
28+
// Chain calls
29+
$service->process("data")->validate("data");

0 commit comments

Comments
 (0)