Resolve a class name using use-map, namespace, local classes, and cross-file / PSR-4 / stubs. This is the single canonical implementation of the "class_loader" logic used by the completion handler, definition resolver, and named-argument resolution. It handles: - Fully-qualified names (`\PDO`, `\Couchbase\Cluster`) - Unqualified names resolved via the import table (`use` statements), local clas
(
&self,
name: &str,
local_classes: &[Arc<ClassInfo>],
file_use_map: &HashMap<String, String>,
file_namespace: &Option<String>,
)
| 697 | /// local class list, current namespace, or global scope |
| 698 | /// - Qualified names with alias expansion and namespace prefixing |
| 699 | pub(crate) fn resolve_class_name( |
| 700 | &self, |
| 701 | name: &str, |
| 702 | local_classes: &[Arc<ClassInfo>], |
| 703 | file_use_map: &HashMap<String, String>, |
| 704 | file_namespace: &Option<String>, |
| 705 | ) -> Option<Arc<ClassInfo>> { |
| 706 | // ── Fully qualified name (leading `\`) ────────────── |
| 707 | if let Some(stripped) = name.strip_prefix('\\') { |
| 708 | return self.find_or_load_class(stripped); |
| 709 | } |
| 710 | |
| 711 | // ── Unqualified name (no `\` at all) ──────────────── |
| 712 | if !name.contains('\\') { |
| 713 | // Check the import table first (`use` statements). |
| 714 | if let Some(fqn) = file_use_map.get(name) { |
| 715 | return self.find_or_load_class(fqn); |
| 716 | } |
| 717 | // Check local classes (same-file shortcut). |
| 718 | // In multi-namespace files, prefer the class whose |
| 719 | // file_namespace matches the current namespace context. |
| 720 | let lookup = short_name(name); |
| 721 | let ns_atom = file_namespace.as_ref().map(|ns| crate::atom::atom(ns)); |
| 722 | let local_match = local_classes |
| 723 | .iter() |
| 724 | .find(|c| c.name == lookup && c.file_namespace == ns_atom) |
| 725 | .or_else(|| local_classes.iter().find(|c| c.name == lookup)); |
| 726 | if let Some(cls) = local_match { |
| 727 | return Some(Arc::clone(cls)); |
| 728 | } |
| 729 | // In a namespace, try the namespace-qualified form first. |
| 730 | // Per PHP semantics, class names do NOT fall back to global |
| 731 | // scope (unlike functions/constants). However, names that |
| 732 | // arrive here may be already-resolved FQNs from ClassInfo |
| 733 | // fields (e.g. `parent_class = "Exception"`) that happen to |
| 734 | // be single-segment global names. For those, the namespace- |
| 735 | // qualified attempt will fail, so we fall back to a direct |
| 736 | // lookup. To preserve PHP semantics for user-typed code, |
| 737 | // the namespace-qualified form is tried first and wins when |
| 738 | // a same-named class exists in the current namespace. |
| 739 | if let Some(ns) = file_namespace { |
| 740 | let ns_qualified = format!("{}\\{}", ns, name); |
| 741 | if let Some(cls) = self.find_or_load_class(&ns_qualified) { |
| 742 | return Some(cls); |
| 743 | } |
| 744 | } |
| 745 | // Global scope: either no namespace context, or the |
| 746 | // namespace-qualified lookup above did not find a match. |
| 747 | return self.find_or_load_class(name); |
| 748 | } |
| 749 | |
| 750 | // ── Qualified name (contains `\`, no leading `\`) ─── |
| 751 | // Check if the first segment is a use-map alias |
| 752 | // (e.g. `OA\Endpoint` where `use Swagger\OpenAPI as OA;` |
| 753 | // maps `OA` → `Swagger\OpenAPI`). Expand to FQN. |
| 754 | let first_segment = name.split('\\').next().unwrap_or(name); |
| 755 | if let Some(fqn_prefix) = file_use_map.get(first_segment) { |
| 756 | let rest = &name[first_segment.len()..]; |
no test coverage detected