Skip to content

PHP call extraction produces ~0 CALLS edges; _get_call_name doesn't handle PHP AST node shape #377

@stefan-willems-youngones

Description

Summary

On real-world PHP codebases, code-review-graph emits virtually no CALLS edges (e.g. 18 edges across 26,372 PHP functions in a ~6,900-file repo). As a result, all change-impact features that depend on the call graph — get_impact_radius_tool, detect_changes_tool, get_affected_flows_tool, get_bridge_nodes_tool, flow detection — return empty or near-empty results on PHP repos.

Root cause

Two independent gaps in parser.py:

  1. scoped_call_expression is missing from PHP's call-type list
    parser.py:278:
"php": ["function_call_expression", "member_call_expression"],

PHP's Foo::bar() parses as scoped_call_expression. It's never iterated on, so static calls (extremely common in PHP — self::, static::, parent::, ClassName::) never produce edges.

  1. _get_call_name has no PHP branch and the generic code doesn't match PHP's AST
    parser.py:3659 — the generic fallbacks:

    • first.type in ("identifier", "simple_identifier") → PHP uses name, not identifier.
    • member_types tuple: ("attribute", "member_expression", "field_expression", "selector_expression", "navigation_expression") → PHP's member_call_expression doesn't wrap the receiver in any of these; the receiver is a direct variable_name child and the method name is a sibling name child.
      So _get_call_name returns None for:
  • function_call_expression (first child is name)
  • member_call_expression (first child is variable_name, method lives in a top-level name child)
  • scoped_call_expression (would return None too, but never gets there)

Reproduction

<?php
namespace App;
class Foo {
    public function bar() {
        $this->baz();      // member_call_expression
        self::qux();       // scoped_call_expression
        Foo::stat();       // scoped_call_expression
        myFunc();          // function_call_expression
    }
}

Actual tree-sitter-php AST (verified with tree-sitter-php 0.24+):

function_call_expression
  ├── name "myFunc"             ← first child is `name`, NOT `identifier`
  └── arguments
member_call_expression
  ├── variable_name ($this)      ← first child; not in member_types
  ├── ->
  ├── name "baz"                 ← method name is a top-level `name` child
  └── arguments
scoped_call_expression
  ├── relative_scope (self) | name "Foo"
  ├── ::
  ├── name "qux"                 ← method name
  └── arguments

After running code-review-graph build on a PHP repo:

Nodes by kind:  Class: 6671, File: 6871, Function: 26372
Edges by kind:  CALLS: 18, CONTAINS: 33043, IMPORTS_FROM: 43324, REFERENCES: 1

The 18 CALLS edges come from JS files in the repo, not PHP.

Expected

CALLS edges should number in the tens or hundreds of thousands for a codebase this size. Flow detection and impact-radius queries should produce meaningful results.

Suggested fix sketch

Two changes in parser.py:

  1. Extend the PHP entry (~line 278):
"php": ["function_call_expression", "member_call_expression", "scoped_call_expression"],
  1. Add a PHP-specific branch early in _get_call_name (~line 3659). tree-sitter-php exposes named fields — function on function_call_expression, name on member_call_expression and scoped_call_expression — so node.child_by_field_name("name") / child_by_field_name("function") resolves the target cleanly without having to iterate children. Decode the name node's text; for scoped_call_expression you may also want to join the scope (node.child_by_field_name("scope")) and name to produce a qualified target, so _resolve_call_target can pick it up via the existing namespace_use_declaration import map.
  2. Worth verifying that _resolve_call_target correctly maps bare class names (from Foo::bar) through the PHP namespace_use_declaration import map into fully-qualified \App...\Foo::bar targets.
    Impact if fixed
    Unlocks the primary value prop of the tool (token-efficient, impact-aware code review) for PHP codebases, which are a large fraction of real-world Laravel / Symfony / WordPress users.

Environment

  • code-review-graph (latest via uvx, Python 3.14)
  • tree-sitter-php current PyPI release
  • Target: ~6,900-file PHP monorepo (Laravel-style DDD)
  • OS: macOS (Darwin 24.5)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions