From 4ffe5449acb987ec76d184b89575634aa1fce6f2 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 11 May 2026 18:12:35 +0100 Subject: [PATCH] fix(php): inject FQN-keyed module-scope bindings (Codex #1497 finding 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend populatePhpNamespaceSiblings with Step 3b: for every PHP file's Module scope, inject a binding entry keyed by the fully-qualified class name (`App\Models\User`) for every class-like def in the workspace. This routes FQN-receivers like `\App\Other\User` to the exact namespace-qualified class regardless of which simple-name `User` the caller's `use` imports shadowed. Why module-scope bindingAugmentations instead of mutating def.qualifiedName: the shared QualifiedNameIndex consumes def.qualifiedName at finalize time, but PHP class defs need to remain keyed by simple name throughout the rest of the pipeline (MRO, method-dispatch-index, namespace-siblings step 3). An earlier attempt to rewrite def.qualifiedName to namespace-prefixed form cascaded into 32 unrelated test failures across receiver-binding, MRO, and heritage. The bindingAugmentations channel is purpose-built for adding post-finalize visibility without mutating shared semantic state, and `findClassBindingInScope`'s scope-chain walk already consumes it via `lookupBindingsAt` — wiring is zero-touch. Cost: O(PHP files × class-like defs) augmentation entries. Typical PHP project: hundreds × hundreds = bounded. Verified locally: - Registry-primary: 178/178 PHP tests pass (including the new FQN regression). - Legacy DAG: 174 passed, 3 existing skips, 1 failure (the FQN test, expected — legacy DAG has no namespace augmentation channel; U5 registers it in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.php). - Cross-language: 794/794 (C#, Python, TypeScript, Go, C) pass. The PHP-only augmentation does not touch shared resolution code. - tsc clean. Plan: docs/plans/2026-05-11-002-fix-php-fqn-and-overload-codex-findings-plan.md (U3) --- .../languages/php/namespace-siblings.ts | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/php/namespace-siblings.ts b/gitnexus/src/core/ingestion/languages/php/namespace-siblings.ts index 6ebfbde2a..20b99d971 100644 --- a/gitnexus/src/core/ingestion/languages/php/namespace-siblings.ts +++ b/gitnexus/src/core/ingestion/languages/php/namespace-siblings.ts @@ -117,8 +117,8 @@ const namespaceByFilePath = new Map(); /** * Read the cached PHP namespace for a given filePath. Returns `''` (global) * when the file has no namespace_definition or hasn't been processed yet. - * Callers should only consult this AFTER `populatePhpNamespaceSiblings` has - * run for the current resolution. + * Callers should only consult this AFTER either `populatePhpClassQualifiedNames` + * or `populatePhpNamespaceSiblings` has run for the current resolution. */ export function getPhpNamespaceForFile(filePath: string): string { return namespaceByFilePath.get(filePath) ?? ''; @@ -240,6 +240,38 @@ export function populatePhpNamespaceSiblings( } } + // Step 3b: Inject fully-qualified-name bindings into every PHP file's + // Module scope. PHP `\App\Models\User` (leading-backslash FQN) and + // `App\Models\User` (already-qualified relative) on a parameter or + // typed receiver must resolve to the exact namespace-qualified class + // regardless of which simple-name `User` the caller's `use` imports + // shadowed. The shared `findClassBindingInScope` scope-chain walk + // consumes these augmentations via `lookupBindingsAt`, so adding the + // qualified key on every file's module scope routes FQN-receivers to + // the right def. Codex PR #1497 review, finding 1. + // + // Cost: O(PHP files × class-like defs in the workspace) augmentation + // entries. Bounded and acceptable in practice — typical PHP projects + // have hundreds of files and classes, not tens of thousands. + for (const parsed of parsedFiles) { + const moduleScope = parsed.scopes.find((s) => s.kind === 'Module'); + if (moduleScope === undefined) continue; + const moduleScopeId = moduleScope.id; + + for (const [ns, bucket] of buckets) { + if (ns === '') continue; // global-namespace classes have no qualified form to register + for (const def of bucket.classDefs) { + const q = def.qualifiedName ?? ''; + const simpleName = q.includes('\\') ? q.slice(q.lastIndexOf('\\') + 1) : q; + if (simpleName === '') continue; + const fqn = `${ns}\\${simpleName}`; + const arr = getAugmentationBucket(augmentations, moduleScopeId, fqn); + if (arr.some((b) => b.def.nodeId === def.nodeId)) continue; + arr.push({ def, origin: 'namespace' }); + } + } + } + // Step 4: Mirror return-type bindings from same-namespace sibling files. // This enables chain-follow like `$c->greet()->save()` where `greet()` // returns `Greeting` (declared in A.php, same namespace) and `Greeting`