Summary
crates/codegraph-core/src/extractors/php.rs's tree walker (match_php_node) dispatches every method_declaration node to handle_method_decl unconditionally — including ones nested inside an interface_declaration body, which are also separately extracted by handle_interface_decl's own inner loop. This produces two Definitions for the same interface method: the correct dotted one (RepoInterface.save) and a spurious bare one (save, with no parent-qualified name).
The root cause: handle_method_decl calls find_php_parent_class, which searches ancestors via PHP_CLASS_KINDS:
const PHP_CLASS_KINDS: &[&str] = &["class_declaration", "trait_declaration", "enum_declaration"];
"interface_declaration" is missing from this list, so for a method inside an interface, find_php_parent_class returns None, and handle_method_decl falls back to the bare (unqualified) name instead of skipping the node entirely.
Every other native extractor with the same interface/class dual-extraction shape (java.rs, csharp.rs) has an explicit early-return guard in their handle_method_decl to skip interface methods (already emitted by handle_interface_decl):
// java.rs / csharp.rs
if let Some(parent) = node.parent() {
if let Some(grand) = parent.parent() {
if grand.kind() == "interface_declaration" { return; }
}
}
php.rs's handle_method_decl has no equivalent guard. The WASM/TS extractor (src/extractors/php.ts's handlePhpMethodDecl) already has this guard (if (node.parent?.parent?.type === 'interface_declaration') return;), so this is native-only.
Reproduction
interface RepoInterface {
public function save(string $id, int $value): bool;
}
class Repo implements RepoInterface {
public function save(string $id, int $value): bool {
if ($value < 0) { return false; }
return true;
}
}
$ codegraph complexity --file Repo.php -T --json --engine native
functions: 3 [ Repo.save, RepoInterface.save, save ] ← spurious bare "save"
$ codegraph complexity --file Repo.php -T --json --engine wasm
functions: 2 [ Repo.save, RepoInterface.save ]
Per CLAUDE.md: "Both engines must produce identical results. If they diverge, the less-accurate engine has a bug." Native is the less-accurate engine here — the spurious bare save symbol has no real referent and would pollute codegraph where/query/dead-code detection results for any PHP codebase with interfaces.
Suggested fix
Add the same interface-method guard java.rs/csharp.rs already have to php.rs's handle_method_decl, checking whether the method's grandparent is an interface_declaration.
Discovery context
Found while fixing #1922 (WASM complexity engine skips entire file when every function has a dotted name) — live-testing native vs WASM parity for PHP interface + implementing-class fixtures as part of verifying the mirrored Definition.bodyless signal added in that fix. Independent, pre-existing bug — not fixed there to keep that PR scoped to the file-level complexity gate.
Summary
crates/codegraph-core/src/extractors/php.rs's tree walker (match_php_node) dispatches everymethod_declarationnode tohandle_method_declunconditionally — including ones nested inside aninterface_declarationbody, which are also separately extracted byhandle_interface_decl's own inner loop. This produces twoDefinitions for the same interface method: the correct dotted one (RepoInterface.save) and a spurious bare one (save, with no parent-qualified name).The root cause:
handle_method_declcallsfind_php_parent_class, which searches ancestors viaPHP_CLASS_KINDS:"interface_declaration"is missing from this list, so for a method inside an interface,find_php_parent_classreturnsNone, andhandle_method_declfalls back to the bare (unqualified) name instead of skipping the node entirely.Every other native extractor with the same interface/class dual-extraction shape (
java.rs,csharp.rs) has an explicit early-return guard in theirhandle_method_declto skip interface methods (already emitted byhandle_interface_decl):php.rs'shandle_method_declhas no equivalent guard. The WASM/TS extractor (src/extractors/php.ts'shandlePhpMethodDecl) already has this guard (if (node.parent?.parent?.type === 'interface_declaration') return;), so this is native-only.Reproduction
Per CLAUDE.md: "Both engines must produce identical results. If they diverge, the less-accurate engine has a bug." Native is the less-accurate engine here — the spurious bare
savesymbol has no real referent and would pollutecodegraph where/query/dead-code detection results for any PHP codebase with interfaces.Suggested fix
Add the same interface-method guard
java.rs/csharp.rsalready have tophp.rs'shandle_method_decl, checking whether the method's grandparent is aninterface_declaration.Discovery context
Found while fixing #1922 (WASM complexity engine skips entire file when every function has a dotted name) — live-testing native vs WASM parity for PHP interface + implementing-class fixtures as part of verifying the mirrored
Definition.bodylesssignal added in that fix. Independent, pre-existing bug — not fixed there to keep that PR scoped to the file-level complexity gate.