Skip to content

fix(parsers/php): repair the PHP call-graph builder (tag grammar, callbacks, new, scopes, imports)#71

Closed
gadievron wants to merge 1 commit into
masterfrom
fix/parsers-php-repair-the-php-call-graph-builder
Closed

fix(parsers/php): repair the PHP call-graph builder (tag grammar, callbacks, new, scopes, imports)#71
gadievron wants to merge 1 commit into
masterfrom
fix/parsers-php-repair-the-php-call-graph-builder

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

Several defects in parsers/php/call_graph_builder.py + function_extractor.py; before this the PHP call
graph was effectively empty (see #1).

  1. Tag grammar: the call extractor re-parsed tag-stripped function bodies with ts_php.language_php() (the
    tag-REQUIRING grammar), which treats tagless input as inert text -- so ZERO call nodes were found and
    the entire PHP call graph was empty. Use language_php_only(). Foundational: every fix below only
    produces edges because of it.

  2. Higher-order callbacks: call_user_func / call_user_func_array / array_map / array_filter / array_walk /
    array_reduce / usort / uasort / uksort / preg_replace_callback are in PHP_BUILTINS, so
    _resolve_function_call returned None before inspecting their callback argument. A CALLBACK_BUILTINS map
    now resolves the string-literal callback ('fn', 'Class::method', or ['Class','method']) at the builtin's
    callback position. The outer builtin call stays filtered; variable/closure callbacks have no static
    target.

  3. use-import node type: _extract_imports matched use_declaration, but tree-sitter-php emits
    namespace_use_declaration, so use App\Service\Foo; imports were never recorded. Match the real node
    type; handle grouped use A, B; and strip as Alias.

  4. Import-file matching: _resolve_simple_call matched import_name in file_path -- an unanchored substring
    -- so an import 'Bar' matched 'app/BarBaz/x.php'. Match the import file name (anchored: endswith the
    basename .php, or exact).

  5. new/__construct: new Foo(...) parses as object_creation_expression, which the traversal never visited,
    so constructors were untracked. Add the node type + resolve the class __construct.

  6. self/static/parent scopes: in tree-sitter-php self/static/parent are relative_scope nodes, not
    name, so _resolve_scoped_call never captured the scope and ALL self::/static::/parent:: calls were
    silently dropped. Handle relative_scope; resolve parent:: in the superclass (from the class's extends,
    namespace-stripped), with no fall-back to the caller's own class (which would mis-link an override's
    parent:: call to itself).

  7. Case-insensitive builtins: _is_builtin compared case-sensitively, but PHP function names are
    case-insensitive, so e.g. CALL_USER_FUNC was not recognized. Compare name.lower().

Scope: PHP-specific (the c/python/ruby/zig/go call-graph builders are separate implementations).

Tests: tests/test_php_call_graph.py (new; the package had no php call-graph tests) -- loaded under a
unique importlib name (call_graph_builder/function_extractor are basenames shared by every parser). Eight
behavioral tests: tagless body produces edges, callback builtins resolve, new->__construct,
parent::->superclass, parent:: with a namespaced superclass, case-insensitive builtins, anchored import
match, namespace_use_declaration import. RED 8 failed (pre-fix) -> GREEN 8 passed; ruff clean; full suite
184 passed / 63 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

…lbacks, new, scopes, imports)

Several defects in parsers/php/call_graph_builder.py + function_extractor.py; before this the PHP call
graph was effectively empty (see #1).

1. Tag grammar: the call extractor re-parsed tag-stripped function bodies with ts_php.language_php() (the
   tag-REQUIRING grammar), which treats tagless input as inert `text` -- so ZERO call nodes were found and
   the entire PHP call graph was empty. Use language_php_only(). Foundational: every fix below only
   produces edges because of it.

2. Higher-order callbacks: call_user_func / call_user_func_array / array_map / array_filter / array_walk /
   array_reduce / usort / uasort / uksort / preg_replace_callback are in PHP_BUILTINS, so
   _resolve_function_call returned None before inspecting their callback argument. A CALLBACK_BUILTINS map
   now resolves the string-literal callback ('fn', 'Class::method', or ['Class','method']) at the builtin's
   callback position. The outer builtin call stays filtered; variable/closure callbacks have no static
   target.

3. use-import node type: _extract_imports matched `use_declaration`, but tree-sitter-php emits
   `namespace_use_declaration`, so `use App\Service\Foo;` imports were never recorded. Match the real node
   type; handle grouped `use A, B;` and strip `as Alias`.

4. Import-file matching: _resolve_simple_call matched `import_name in file_path` -- an unanchored substring
   -- so an import 'Bar' matched 'app/BarBaz/x.php'. Match the import file name (anchored: endswith the
   basename `.php`, or exact).

5. new/__construct: `new Foo(...)` parses as object_creation_expression, which the traversal never visited,
   so constructors were untracked. Add the node type + resolve the class __construct.

6. self/static/parent scopes: in tree-sitter-php `self`/`static`/`parent` are `relative_scope` nodes, not
   `name`, so _resolve_scoped_call never captured the scope and ALL self::/static::/parent:: calls were
   silently dropped. Handle relative_scope; resolve parent:: in the superclass (from the class's `extends`,
   namespace-stripped), with no fall-back to the caller's own class (which would mis-link an override's
   parent:: call to itself).

7. Case-insensitive builtins: _is_builtin compared case-sensitively, but PHP function names are
   case-insensitive, so e.g. CALL_USER_FUNC was not recognized. Compare name.lower().

Scope: PHP-specific (the c/python/ruby/zig/go call-graph builders are separate implementations).

Tests: tests/test_php_call_graph.py (new; the package had no php call-graph tests) -- loaded under a
unique importlib name (call_graph_builder/function_extractor are basenames shared by every parser). Eight
behavioral tests: tagless body produces edges, callback builtins resolve, new->__construct,
parent::->superclass, parent:: with a namespaced superclass, case-insensitive builtins, anchored import
match, namespace_use_declaration import. RED 8 failed (pre-fix) -> GREEN 8 passed; ruff clean; full suite
184 passed / 63 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gadievron

Copy link
Copy Markdown
Collaborator Author

Superseded by #134 (the 2026-06-24 release), which independently re-landed this parser work — the added code and tests are verified present on origin/master (line-by-line subset check). Closing to unclog the review queue; please reopen if a specific hunk is found missing on master.

@gadievron gadievron closed this Jul 9, 2026
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.

1 participant