From 1d69b3f5fa108c0d69b8c3aa59258a6809e0300c Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 11 May 2026 17:05:22 +0100 Subject: [PATCH] fix(php): preserve qualified form on TypeRef.rawName in normalizePhpType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop collapsing `\App\Models\User` to `User` in normalizePhpType (step 6). Canonicalize the leading backslash off and preserve the qualified path on TypeRef.rawName so downstream PHP receiver resolution can distinguish the FQN target from a same-simple-name class reachable via `use`. - Step 6 rewritten: `\App\Models\User` → `App\Models\User`, `App\Models\User` → unchanged, `User` → unchanged. - Final validation regex relaxed from /^\w+$/ to /^\w+(?:\\w+)*$/ to accept qualified PHP identifiers while still rejecting empty segments, trailing backslashes, and non-identifier characters. - Step 5 (single-arg generic strip) already passes qualified inner types through via its existing /^\w[\w\]*/ pattern — no change needed. U3 will wire the qualified-name lookup path; this commit alone is a no-op for resolution (the lookup chain still keys on simple names). PHP suite remains green at 177 passed; the only failing test is the intentional FQN regression from U1. Plan: docs/plans/2026-05-11-002-fix-php-fqn-and-overload-codex-findings-plan.md (U2) --- .../core/ingestion/languages/php/interpret.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/php/interpret.ts b/gitnexus/src/core/ingestion/languages/php/interpret.ts index 2cafbe1e3..8aa07a736 100644 --- a/gitnexus/src/core/ingestion/languages/php/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/php/interpret.ts @@ -155,8 +155,15 @@ export function interpretPhpTypeBinding(captures: CaptureMatch): ParsedTypeBindi * 3. Take first part of `&` intersection * 4. Strip array suffix `[]` * 5. Strip generic wrapper `Collection` → `User` - * 6. Strip backslash namespace qualifier `\App\Models\User` → `User` + * 6. Canonicalize leading backslash off: `\App\Models\User` → `App\Models\User` * 7. Reject PHP primitive / pseudo types + * + * The qualified form is preserved on `TypeRef.rawName` so downstream PHP + * receiver resolution can distinguish `\App\Other\User` from a same-simple-name + * `User` reachable via `use`. Without this, fully-qualified type hints collapse + * to ambiguous simple names and resolve against the caller's scope chain + * instead of the explicit target the source named (Codex PR #1497 review, + * finding 1). */ export function normalizePhpType(raw: string): string | null { // 1. Strip nullable prefix @@ -183,22 +190,28 @@ export function normalizePhpType(raw: string): string | null { if (type.endsWith('[]')) type = type.slice(0, -2).trim(); // 5. Strip single-arg generic wrapper: Collection → User + // Qualified inner types (Collection<\App\Models\User>) survive — the + // capture group preserves whatever the writer named. const genericMatch = type.match(/^\w[\w\\]*\s*<([^,<>]+)>$/); if (genericMatch) { type = genericMatch[1].trim(); } - // 6. Strip backslash namespace qualifier: \App\Models\User → User - if (type.includes('\\')) { - const segments = type.split('\\').filter(Boolean); - type = segments[segments.length - 1] ?? type; - } + // 6. Canonicalize leading backslash off — keep the qualified path intact. + // `\App\Models\User` → `App\Models\User`. `App\Models\User` → unchanged. + // Unqualified `User` stays as `User`. The qualified form is the lookup + // key into the workspace QualifiedNameIndex (PHP defs are indexed by + // namespace-joined qualifiedName); the leading-backslash distinction in + // source is only an "absolute path" anchor, not part of the canonical key. + if (type.startsWith('\\')) type = type.replace(/^\\+/, ''); // 7. Reject primitives / pseudo-types if (isPrimitiveOrPseudo(type)) return null; - // Must be a simple identifier - if (!/^\w+$/.test(type)) return null; + // Must be a (possibly qualified) PHP identifier — segments of word chars + // separated by single backslashes. Empty segments (consecutive backslashes, + // trailing backslash) are rejected. + if (!/^\w+(?:\\\w+)*$/.test(type)) return null; return type; }