Skip to content

PHP: member/instance method calls never resolve to calls edges — only static Class::method() works #1682

Description

@distribuidornexvia

Summary

For PHP codebases, calls edges are only ever generated for static method calls
(ClassName::method()). Instance-method calls — both constructor-injected
dependencies and inline new ClassName() — never produce a calls edge, even
though the callee class is statically knowable in both cases.

This matters a lot for the "what depends on X / what breaks if I change X" use
case, which is one of the main reasons people point an AI coding assistant at a
graphify graph instead of grepping. In a modern Laravel codebase, constructor
injection is the dominant call pattern (it's the framework's own recommended
style), so this gap silently blanks out cross-file call edges for most
non-trivial service classes.

Repro (graphify 0.8.30, tree_sitter_php)

Three real classes in the same codebase, same MCP server, same freshly-built
(current-HEAD) graph:

  1. Static call — works
    class SucursalContext {
        public static function id() { ... }
    }
    // called elsewhere as: SucursalContext::id()

get_neighbors("SucursalContext") → 25 correct inbound calls [INFERRED] edges.

Constructor-injected instance call — fails

class LeadController extends Controller {
public function __construct(protected LeadHunterService $leadHunter) {}
// ...$this->leadHunter->search(...) elsewhere in the class
}
get_neighbors("LeadHunterService") → 0 inbound calls edges, despite confirmed
real usage in LeadController.php (verified via grep).

Inline instantiation — fails

$paymentResult = (new \App\Services\MixedPaymentService())->resolve(...);
get_neighbors("MixedPaymentService") → 0 inbound calls edges, despite the
call site existing and being active code.

What I found reading the source
extract.py (PHP walk_calls branch, _PHP_CONFIG.call_types includes member_call_expression) does visit member-call nodes, but only extracts the bare method name from the name field — it never inspects the object field ($this, a property, a new X() expression), so there's no attempt to resolve which class the call belongs to.
Even if that were resolved, the cross-file resolution pass unconditionally drops any raw call flagged is_member_call, with a comment explaining this is intentional — to avoid false positives from common method names (log(), execute(), find()) colliding across unrelated classes when there's no type information to disambiguate.
So the gap is understandable as a conservative anti-false-positive choice, but
it means calls edges are effectively silent for the majority pattern of
idiomatic OOP/DI PHP code, with no signal to the caller that the edge set is
incomplete for that reason.

Suggested direction (not prescriptive — happy to be told this is out of scope)
Resolving $this->prop->method() and (new ClassName())->method() for
concretely-typed properties/constructor params looks feasible (a per-file map
of property-name → declared type from constructor-promoted params and typed
property declarations), while leaving interface-typed properties and
genuinely ambiguous cases unresolved rather than guessing. Existing
bound_to/service-container-binding edges elsewhere in the extractor might
already carry some of the type information needed for the interface case.

Even without a full fix, surfacing this as a known limitation (e.g. a
"member-call edges best-effort / static-only for PHP" note in the docs or in
graph_stats) would help — right now the graph "looks complete" (rich
inherits/method/contains edges) while silently missing what's often the
most useful edge type for impact analysis.

Environment
graphify 0.8.30 (graphifyy on PyPI)
Language: PHP 8, Laravel, tree_sitter_php
Reproduced via both get_neighbors and query_graph (MCP tools)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions