fix(php): emit indirect_call for first-class callables (#15) - #27
Merged
Conversation
PHP 8.1 `$obj->method(...)` creates a Closure — it names the method without invoking it — but the 8.1 grammar reuses `member_call_expression` for it, so the shared `node.type in config.call_types` gate saw an ordinary call and the PHP branch never inspected the `arguments` field. The edge landed as `calls`, claiming control flow transfers at that line. Maintainer decision on #15 (option 2): re-tag as `indirect_call`, the relation this repo already uses for "named but not invoked". No sibling resolver emits `calls` for the equivalent syntax — C# method groups, Java method references and TS bare member references are never captured at all — so PHP was the outlier, and deleting the edge would lose a real dependency that suppression cannot express. Detection is stamped at capture as `fcc` on the raw-call fact, keyed on the argument list being exactly the `...` placeholder: probe-verified on the pinned tree-sitter-php 0.24.1, `m(...)` parses as `arguments: (arguments (variadic_placeholder))` — one named child of that type — while `m()`, `m(1)` and the spread `m(...$args)` do not. `_resolve_php_member_calls` reads the marker and flips only the relation: receiver typing, the single-definition and interface/enum/trait refusals, and the confidence ladder are unchanged. The in-file path (`$this->m(...)` binding to a method in the same file) re-tags too, at unchanged EXTRACTED confidence. Ordinary invocations keep `calls`, and a caller that both invokes and references the same method keeps the `calls` edge regardless of source order — the fcc dedup uses its own pair set, and the cross-file pass sorts direct calls ahead of references. Static (`Helper::fmt(...)`) and plain-function (`strlen(...)`) first-class callables are out of scope: neither resolves to a method target today. Tests: 7 new cases through the public extract() seam — plain, nullsafe and `$this` forms (each with a same-named decoy asserted to get no edge), plus regression guards for the ordinary member call, the ordinary `$this` call, the `...$args` spread, and direct-call precedence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in #9 (union/intersection receiver refusal, PHP 8.2 DNF types). Conflict in CHANGELOG.md only: both sides edited the "Known open items" line of the unreleased 0.9.34 section — #9 removed its own clause, this branch removed the #15 clause. Resolved by keeping the two #9 Fix bullets and this branch's first-class-callable behavior-change bullet, with the open-items line now naming only #10. graphify/extractors/engine.py auto-merged: #9 changed how _php_receiver_type is derived (a PRESENT key mapped to None marks a multi-class annotation and now defers), while #15 adds the `php_fcc` capture and the indirect_call emission just below it. The interaction is the intended one — a union-typed receiver's first-class callable defers like its ordinary call does, and the cross-file resolver then refuses it for want of a receiver type, so `$multi->m(...)` emits no edge at all. Full suite: 4041 passed, 36 skipped (4034 on v8 + this branch's 7). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15 (maintainer decision recorded in the issue comments: option 2, re-tag as
indirect_call).What
A PHP 8.1 first-class callable (
$obj->method(...), plus nullsafe$obj?->method(...)and$this->method(...)) now emitsindirect_callinstead ofcalls. The syntax creates aClosurewithout invoking; every other language's equivalent (C# method group, Java::, TS bare reference) emits nocallsedge, and the repo's stated relation for named-but-not-invoked isindirect_call.php_fccmarker stamped when the arguments node is exactly onevariadic_placeholder(probe-verified on tree-sitter-php 0.24.1; the spread formmethod(...$args)is distinguishable and stayscalls).d97e37dmerges post-PHP: union- and intersection-typed receivers still mint a same-file bare-name edge (user story 11) #9v8; the new interaction (a union-typed receiver's first-class callable now refuses entirely, matching its ordinary call) was probe-verified.Test evidence
$this); 4 regression guards passed as required.🤖 Generated with Claude Code