Skip to content

fix(php): bind namespaced receivers in ::class and constructor position - #1775

Open
mwebber92 wants to merge 2 commits into
colbymchenry:mainfrom
mwebber92:fix/php-qualified-name-refs-main
Open

fix(php): bind namespaced receivers in ::class and constructor position#1775
mwebber92 wants to merge 2 commits into
colbymchenry:mainfrom
mwebber92:fix/php-qualified-name-refs-main

Conversation

@mwebber92

@mwebber92 mwebber92 commented Sep 8, 2026

Copy link
Copy Markdown

Problem

PHP writes every namespaced name as a qualified_name node. Two extractor paths accept only the simple forms, so a class referenced through a namespace produces no edge and no unresolved_refs row — there is no trace of it anywhere in the index, and impact truthfully reports the class has no consumers.

extractStaticMemberRef accepts a receiver only when its node kind is identifier | type_identifier | simple_identifier | name | scoped_type_identifier. None of those is what a namespaced receiver produces, so all of these are dropped:

use App\SoapTypes as Type;

Type\Bankverbindung::class;                 // alias
\App\SoapTypes\Bankverbindung::class;       // fully qualified
SoapTypes\Bankverbindung::class;            // namespace-relative
\App\Models\User::TABLE;                    // any ::CONST, same shape

A bare Bankverbindung::class is a name and was always handled — which makes this easy to miss.

extractInstantiation strips a . or :: qualifier but not PHP's \, so new App\SoapTypes\Bankverbindung() pushes the unresolvable literal App\SoapTypes\Bankverbindung.

The diagnostic tell is that a type hint binds while ::class on the next line does not:

public function hint(Type\Bankverbindung $x) {}                // ✅ references edge
public function make() { return Type\Bankverbindung::class; }  // ❌ nothing

Fix

walkPhpTypePosition already handles qualified_name correctly, with the comment "match on the trailing simple name — what the class node is stored as, and what a use import brings into scope." This applies the same rule in the two places that lack it.

Both walkers, in two commits. codegraph-kernel/src/php.rs carried the identical pair of defects, and since PHP is in DEFAULT_ROUTED the kernel is what actually runs — fixing only the TypeScript side would leave a default build's output unchanged. The Rust commit needs no language gating: php.rs is PHP-only and carries its own private copy of strip_generic_and_qualifier rather than sharing one.

The TypeScript changes are gated to PHP: C# also has a qualified_name node and is in STATIC_MEMBER_LANGS, so an ungated change would add unrelated C# edges, and a backslash carries no qualifier meaning in the other languages sharing the instantiation path.

kernel-php-parity needed no fixture change — it compares the two walkers against each other at runtime rather than against goldens, so it stays green once both agree. Its docstring described the old instantiation shape and is updated to match.

This makes the alias moot rather than resolvedType\Bankverbindung and \App\SoapTypes\Bankverbindung both reduce to Bankverbindung — so a same-named class in another namespace stays ambiguous here, exactly as it is for a type hint today. Resolving aliases properly is a separate change; extractPHPImports records use X\Y as Z with exportedName set to the last segment as though it were a class, so it cannot expand a namespace alias.

Evidence

Measured on real PHP codebases (Symfony-style SOAP clients and WordPress plugins), 12 indexes:

index references + instantiates edges
A 206 → 1,013
B 426 → 728
C 1,107 → 1,628
D 3,122 → 4,151
E–H (4 similar) ~4,400 → ~6,900 each
total 22,485 → 34,336 (+53%)

Node count and every other edge kind are unchanged — the change is purely additive. On one repo, impact for a class used through an alias went from 3 of 6 consumer files to 6 of 6.

Re-indexing all twelve a second time through the kernel rather than the WASM walker reproduced 34,336 exactly, every index identical — a broader parity check than the three torture fixtures.

Tests

__tests__/php-qualified-static-member-refs.test.ts — five cases: aliased, fully-qualified and namespace-relative ::class, a qualified constructor, and a negative case pinning the capitalization guard. Each block names the mutation it catches.

Verified they can fail: with the test file in place and the tree-sitter.ts changes reverted, 4 go red and the negative case stays green.

Full suite with the kernel staged, so every kernel-*-parity suite actually runs rather than skipping: 3,234 passed, 0 failed on the v1.6.0-based branch. On main it is 4,336 passed / 3 failed, and those three (object-literal-methods, ui-steps-api ×2) fail identically on clean main — pre-existing and unrelated.

Cargo release build of the kernel is ~60 s warm, ~110 s cold.

Claude Agent and others added 2 commits September 8, 2026 14:59
PHP writes every namespaced name as a `qualified_name` node. Two extractor
paths only accepted the simple forms, so a class referenced through a
namespace was invisible to the graph:

- extractStaticMemberRef accepted `identifier | type_identifier |
  simple_identifier | name | scoped_type_identifier`, none of which a
  namespaced receiver produces. `Foo\Bar::class`, `\App\Models\User::TABLE`
  and the alias form `Type\Bankverbindung::class` were dropped with no edge
  and no unresolved ref, so `impact` reported the class had no consumers.

- extractInstantiation strips a `.` or `::` qualifier but not PHP's `\`, so
  `new Foo\Bar()` pushed the unresolvable literal `Foo\Bar`.

Both now match on the trailing simple name, which is what walkPhpTypePosition
already does for type hints and what the class node is stored as. The
instantiation strip is scoped to PHP because a backslash carries no qualifier
meaning in the other languages sharing that path.

Measured on a 327-file PHP tree: `references` edges 426 -> 728, with node
count and every other edge kind unchanged. `impact` on a class used only
through an alias goes from 3 of 6 consumer files to 6 of 6.

The alias is made moot rather than resolved: `Type\Bankverbindung` and
`\App\SoapTypes\Bankverbindung` both reduce to `Bankverbindung`, so a
same-named class in another namespace stays ambiguous here, exactly as it is
for a type hint today.

codegraph-kernel/src/php.rs carries the same two defects and is unchanged, so
kernel-php-parity will diverge once a kernel binary is staged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gmQvNnfH9URudZJeJU4ck
php.rs carried both defects the TypeScript walker did, so with php in
DEFAULT_ROUTED a default build's output was unchanged by the previous commit.

- extract_static_member_ref repeated the same receiver-kind list, dropping a
  `qualified_name` receiver: `Foo\Bar::class`, `\App\Models\User::TABLE`, and
  the alias form after `use X as Type;`.
- strip_generic_and_qualifier split on `.` and `::` but not `\`, so
  `new \App\Models\User()` kept the whole qualified text.

Both edits are local to php.rs and need no language gate: the file is PHP-only
and it carries its own private copy of strip_generic_and_qualifier rather than
sharing one.

kernel-php-parity needed no fixture change — it compares the two walkers
against each other at runtime rather than against goldens, so it goes green
once both agree. Its docstring described the old instantiation shape and is
updated to match.

Verified with the kernel staged, which is also what routes php through it:
kernel-php-parity green (8 tests), and php-qualified-static-member-refs green
(5 tests) now exercising the native path rather than the wasm fallback. Full
suite 4,336 passed / 3 failed, those three (object-literal-methods,
ui-steps-api x2) failing identically on clean main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gmQvNnfH9URudZJeJU4ck
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