Skip to content

Native PHP extractor double-extracts interface methods as a spurious bare-named definition #2054

Description

@carlos-alm

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    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