@@ -293,51 +293,57 @@ impl LanguageServer for Backend {
293293 // 3. Search the full ast_map
294294 // 4. Load files on demand via PSR-4
295295 let class_loader = |name : & str | -> Option < crate :: ClassInfo > {
296- // If the name has no namespace separator, it might be a
297- // short name imported via `use`. Resolve it first.
298- let resolved_name = if !name. contains ( '\\' ) {
296+ // ── Fully qualified name (leading `\`) ──────────────
297+ // `\PDO`, `\Couchbase\Cluster` — strip the leading `\`
298+ // and resolve globally. PHP rule: fully qualified names
299+ // always resolve to the name without the leading `\`.
300+ if let Some ( stripped) = name. strip_prefix ( '\\' ) {
301+ return self . find_or_load_class ( stripped) ;
302+ }
303+
304+ // ── Unqualified name (no `\` at all) ────────────────
305+ if !name. contains ( '\\' ) {
306+ // Check the import table first (`use` statements).
299307 if let Some ( fqn) = file_use_map. get ( name) {
300- fqn. as_str ( )
301- } else if let Some ( ref ns) = file_namespace {
302- // Not in use map — try current namespace
303- // (e.g. bare `Sibling` inside `namespace Foo\Bar;`
304- // becomes `Foo\Bar\Sibling`)
305- // We build a temporary owned string and leak it into
306- // a short-lived search, so use a two-phase approach:
307- // first try the namespace-qualified name, then fall
308- // back to the bare name.
309- let ns_qualified = format ! ( "{}\\ {}" , ns, name) ;
310- if let Some ( cls) = self . find_or_load_class ( & ns_qualified) {
311- return Some ( cls) ;
312- }
313- name
314- } else {
315- name
316- }
317- } else {
318- // The name contains `\` — check if the first segment
319- // is a use-map alias (e.g. `OA\Endpoint` where
320- // `use Swagger\OpenAPI as OA;` maps `OA` →
321- // `Swagger\OpenAPI`). Expand it to the FQN.
322- let first_segment = name. split ( '\\' ) . next ( ) . unwrap_or ( name) ;
323- if let Some ( fqn_prefix) = file_use_map. get ( first_segment) {
324- let rest = & name[ first_segment. len ( ) ..] ;
325- let expanded = format ! ( "{}{}" , fqn_prefix, rest) ;
326- if let Some ( cls) = self . find_or_load_class ( & expanded) {
327- return Some ( cls) ;
328- }
308+ return self . find_or_load_class ( fqn) ;
329309 }
330- // Also try prefixing with the current namespace.
310+ // In a namespace, prepend the current namespace.
311+ // Class names do NOT fall back to global scope —
312+ // unlike functions/constants. See:
313+ // https://www.php.net/manual/en/language.namespaces.fallback.php
331314 if let Some ( ref ns) = file_namespace {
332315 let ns_qualified = format ! ( "{}\\ {}" , ns, name) ;
333- if let Some ( cls) = self . find_or_load_class ( & ns_qualified) {
334- return Some ( cls) ;
335- }
316+ return self . find_or_load_class ( & ns_qualified) ;
336317 }
337- name
338- } ;
318+ // No namespace — we're in global scope already.
319+ return self . find_or_load_class ( name) ;
320+ }
339321
340- self . find_or_load_class ( resolved_name)
322+ // ── Qualified name (contains `\`, no leading `\`) ───
323+ // Check if the first segment is a use-map alias
324+ // (e.g. `OA\Endpoint` where `use Swagger\OpenAPI as OA;`
325+ // maps `OA` → `Swagger\OpenAPI`). Expand to FQN.
326+ let first_segment = name. split ( '\\' ) . next ( ) . unwrap_or ( name) ;
327+ if let Some ( fqn_prefix) = file_use_map. get ( first_segment) {
328+ let rest = & name[ first_segment. len ( ) ..] ;
329+ let expanded = format ! ( "{}{}" , fqn_prefix, rest) ;
330+ if let Some ( cls) = self . find_or_load_class ( & expanded) {
331+ return Some ( cls) ;
332+ }
333+ }
334+ // Prepend current namespace (if any).
335+ if let Some ( ref ns) = file_namespace {
336+ let ns_qualified = format ! ( "{}\\ {}" , ns, name) ;
337+ if let Some ( cls) = self . find_or_load_class ( & ns_qualified) {
338+ return Some ( cls) ;
339+ }
340+ }
341+ // Fall back to the name as-is. Qualified names that
342+ // reach here are typically already-resolved FQNs from
343+ // the parser (parent classes, traits, mixins) that
344+ // were resolved by `resolve_parent_class_names` before
345+ // being stored.
346+ self . find_or_load_class ( name)
341347 } ;
342348
343349 // Build a function_loader closure that looks up standalone
0 commit comments