From 3a08fa4aa1f98fdaaab637b956da211bdd5e7bec Mon Sep 17 00:00:00 2001 From: gadievron Date: Fri, 10 Jul 2026 10:11:54 +0300 Subject: [PATCH] fix(parsers/php): resolve same-class methods that shadow a PHP builtin name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _resolve_member_call ($this->) and _resolve_scoped_call (self::/static::/ parent::/Class::) ran `if self._is_builtin(method_name): return None` BEFORE the same-class method lookup, so a call to a method named like a PHP builtin (count/next/key — the Countable/Iterator/Serializable interface methods) was dropped from the call graph even though the class defines it. The missing edge survives into call_graph.json with no downstream rescue. Remove the two premature guards so $this->count()/self::count() route to the class-method resolver, which returns an id only when the method exists in the class index (real builtin calls with no matching own method still resolve to None below). The builtin filter is intentionally kept for bare func() and string callbacks, where an unqualified name binds the global PHP builtin. Mirrors the shadow-aware resolution already shipped in parsers/ruby. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../parsers/php/call_graph_builder.py | 6 --- .../parsers/php/test_call_graph_builder.py | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/libs/openant-core/parsers/php/call_graph_builder.py b/libs/openant-core/parsers/php/call_graph_builder.py index 566e79c1..633a015c 100644 --- a/libs/openant-core/parsers/php/call_graph_builder.py +++ b/libs/openant-core/parsers/php/call_graph_builder.py @@ -384,9 +384,6 @@ def _resolve_member_call(self, node, source: bytes, caller_file: str, if not method_name: return None - if self._is_builtin(method_name): - return None - # $this->method() - same class if receiver == '$this' and caller_class: return self._resolve_self_call(method_name, caller_file, caller_class) @@ -416,9 +413,6 @@ def _resolve_scoped_call(self, node, source: bytes, caller_file: str, if not method_name or not scope: return None - if self._is_builtin(method_name): - return None - # self::method() or static::method() - same class if scope in ('self', 'static') and caller_class: return self._resolve_self_call(method_name, caller_file, caller_class) diff --git a/libs/openant-core/tests/parsers/php/test_call_graph_builder.py b/libs/openant-core/tests/parsers/php/test_call_graph_builder.py index f0839fa6..b42fb3a1 100644 --- a/libs/openant-core/tests/parsers/php/test_call_graph_builder.py +++ b/libs/openant-core/tests/parsers/php/test_call_graph_builder.py @@ -75,3 +75,52 @@ def test_bare_call_resolves_within_same_namespace(): assert b.call_graph.get("consumer.php:caller") == ["utils.php:helper"], ( f"same-namespace bare call must still resolve: {b.call_graph}" ) + + +def test_this_call_to_builtin_named_same_class_method_resolves(): + """$this->count() must edge to the same-class count() method (B2). + + A same-class method named like a PHP builtin (count/next/key) was dropped by a + premature _is_builtin() short-circuit in _resolve_member_call before it could + route to _resolve_self_call. Driven through the real builder on real PHP source. + """ + funcs = { + "bag.php:Bag::count": { + "name": "count", "file_path": "bag.php", "class_name": "Bag", + "namespace_name": None, + "code": "function count() { return 1; }", + }, + "bag.php:Bag::total": { + "name": "total", "file_path": "bag.php", "class_name": "Bag", + "namespace_name": None, + "code": "function total() { return $this->count() * 2; }", + }, + } + b = _build(funcs) + assert b.call_graph.get("bag.php:Bag::total") == ["bag.php:Bag::count"], ( + f"$this->count() must resolve to same-class count(): {b.call_graph}" + ) + + +def test_scoped_call_to_builtin_named_same_class_method_resolves(): + """self::count() must edge to the same-class count() method (B2). + + Same bug in _resolve_scoped_call: the premature _is_builtin() short-circuit + dropped self::/static::/Class:: calls to a builtin-named own method. + """ + funcs = { + "bag.php:Bag::count": { + "name": "count", "file_path": "bag.php", "class_name": "Bag", + "namespace_name": None, + "code": "function count() { return 1; }", + }, + "bag.php:Bag::total": { + "name": "total", "file_path": "bag.php", "class_name": "Bag", + "namespace_name": None, + "code": "function total() { return self::count() * 2; }", + }, + } + b = _build(funcs) + assert b.call_graph.get("bag.php:Bag::total") == ["bag.php:Bag::count"], ( + f"self::count() must resolve to same-class count(): {b.call_graph}" + )