Skip to content

fix(php): emit indirect_call for first-class callables (#15) - #27

Merged
filipechagas merged 2 commits into
v8from
fix/php-fcc-indirect-call-15
Aug 5, 2026
Merged

fix(php): emit indirect_call for first-class callables (#15)#27
filipechagas merged 2 commits into
v8from
fix/php-fcc-indirect-call-15

Conversation

@filipechagas

Copy link
Copy Markdown
Collaborator

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 emits indirect_call instead of calls. The syntax creates a Closure without invoking; every other language's equivalent (C# method group, Java ::, TS bare reference) emits no calls edge, and the repo's stated relation for named-but-not-invoked is indirect_call.

  • Capture: php_fcc marker stamped when the arguments node is exactly one variadic_placeholder (probe-verified on tree-sitter-php 0.24.1; the spread form method(...$args) is distinguishable and stays calls).
  • Resolution: unchanged targets, refusals, and confidence — only the relation differs. Direct calls win precedence over a first-class-callable reference to the same (caller, method) pair, source-order independent.
  • d97e37d merges post-PHP: union- and intersection-typed receivers still mint a same-file bare-name edge (user story 11) #9 v8; the new interaction (a union-typed receiver's first-class callable now refuses entirely, matching its ordinary call) was probe-verified.

Test evidence

🤖 Generated with Claude Code

filipechagas and others added 2 commits August 5, 2026 19:56
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PHP: first-class callable $obj->method(...) emits a calls edge; candidate indirect_call

1 participant