Skip to main content

mir_codebase/
codebase.rs

1use std::sync::Arc;
2
3use dashmap::{DashMap, DashSet};
4
5use crate::storage::{
6    ClassStorage, EnumStorage, FunctionStorage, InterfaceStorage, MethodStorage, TraitStorage,
7};
8use mir_types::Union;
9
10// ---------------------------------------------------------------------------
11// Codebase — thread-safe global symbol registry
12// ---------------------------------------------------------------------------
13
14#[derive(Debug, Default)]
15pub struct Codebase {
16    pub classes: DashMap<Arc<str>, ClassStorage>,
17    pub interfaces: DashMap<Arc<str>, InterfaceStorage>,
18    pub traits: DashMap<Arc<str>, TraitStorage>,
19    pub enums: DashMap<Arc<str>, EnumStorage>,
20    pub functions: DashMap<Arc<str>, FunctionStorage>,
21    pub constants: DashMap<Arc<str>, Union>,
22
23    /// Methods referenced during Pass 2 — key format: `"ClassName::methodName"`.
24    /// Used by the dead-code detector (M18).
25    pub referenced_methods: DashSet<Arc<str>>,
26    /// Properties referenced during Pass 2 — key format: `"ClassName::propName"`.
27    pub referenced_properties: DashSet<Arc<str>>,
28    /// Free functions referenced during Pass 2 — key: fully-qualified name.
29    pub referenced_functions: DashSet<Arc<str>>,
30
31    /// Maps every FQCN (class, interface, trait, enum, function) to the absolute
32    /// path of the file that defines it. Populated during Pass 1.
33    pub symbol_to_file: DashMap<Arc<str>, Arc<str>>,
34
35    /// Lightweight FQCN index populated by `SymbolTable` before Pass 1.
36    /// Enables O(1) "does this symbol exist?" checks before full definitions
37    /// are available.
38    pub known_symbols: DashSet<Arc<str>>,
39
40    /// Per-file `use` alias maps: alias → FQCN.  Populated during Pass 1.
41    ///
42    /// Key: absolute file path (as `Arc<str>`).
43    /// Value: map of `alias → fully-qualified class name`.
44    ///
45    /// Exposed as `pub` so that external consumers (e.g. `php-lsp`) can read
46    /// import data that mir already collects, instead of reimplementing it.
47    pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
48    /// Per-file current namespace (if any).  Populated during Pass 1.
49    ///
50    /// Key: absolute file path (as `Arc<str>`).
51    /// Value: the declared namespace string (e.g. `"App\\Controller"`).
52    ///
53    /// Exposed as `pub` so that external consumers (e.g. `php-lsp`) can read
54    /// namespace data that mir already collects, instead of reimplementing it.
55    pub file_namespaces: DashMap<Arc<str>, String>,
56
57    /// Whether finalize() has been called.
58    finalized: std::sync::atomic::AtomicBool,
59}
60
61impl Codebase {
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Reset the finalization flag so that `finalize()` will run again.
67    ///
68    /// Use this when new class definitions have been added after an initial
69    /// `finalize()` call (e.g., lazily loaded via PSR-4) and the inheritance
70    /// graph needs to be rebuilt.
71    pub fn invalidate_finalization(&self) {
72        self.finalized
73            .store(false, std::sync::atomic::Ordering::SeqCst);
74    }
75
76    // -----------------------------------------------------------------------
77    // Incremental: remove all definitions from a single file
78    // -----------------------------------------------------------------------
79
80    /// Remove all definitions that were defined in the given file.
81    /// This clears classes, interfaces, traits, enums, functions, and constants
82    /// whose defining file matches `file_path`, as well as the file's import
83    /// and namespace entries. After calling this, `invalidate_finalization()`
84    /// is called so the next `finalize()` rebuilds inheritance.
85    pub fn remove_file_definitions(&self, file_path: &str) {
86        // Collect all symbols defined in this file
87        let symbols: Vec<Arc<str>> = self
88            .symbol_to_file
89            .iter()
90            .filter(|entry| entry.value().as_ref() == file_path)
91            .map(|entry| entry.key().clone())
92            .collect();
93
94        // Remove each symbol from its respective map and from symbol_to_file
95        for sym in &symbols {
96            self.classes.remove(sym.as_ref());
97            self.interfaces.remove(sym.as_ref());
98            self.traits.remove(sym.as_ref());
99            self.enums.remove(sym.as_ref());
100            self.functions.remove(sym.as_ref());
101            self.constants.remove(sym.as_ref());
102            self.symbol_to_file.remove(sym.as_ref());
103            self.known_symbols.remove(sym.as_ref());
104        }
105
106        // Remove file-level metadata
107        self.file_imports.remove(file_path);
108        self.file_namespaces.remove(file_path);
109
110        self.invalidate_finalization();
111    }
112
113    // -----------------------------------------------------------------------
114    // Lookups
115    // -----------------------------------------------------------------------
116
117    /// Resolve a property, walking up the inheritance chain (parent classes and traits).
118    pub fn get_property(
119        &self,
120        fqcn: &str,
121        prop_name: &str,
122    ) -> Option<crate::storage::PropertyStorage> {
123        // Check direct class own_properties
124        if let Some(cls) = self.classes.get(fqcn) {
125            if let Some(p) = cls.own_properties.get(prop_name) {
126                return Some(p.clone());
127            }
128        }
129
130        // Walk all ancestors (collected during finalize)
131        let all_parents = {
132            if let Some(cls) = self.classes.get(fqcn) {
133                cls.all_parents.clone()
134            } else {
135                return None;
136            }
137        };
138
139        for ancestor_fqcn in &all_parents {
140            if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
141                if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
142                    return Some(p.clone());
143                }
144            }
145        }
146
147        // Check traits
148        let trait_list = {
149            if let Some(cls) = self.classes.get(fqcn) {
150                cls.traits.clone()
151            } else {
152                vec![]
153            }
154        };
155        for trait_fqcn in &trait_list {
156            if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
157                if let Some(p) = tr.own_properties.get(prop_name) {
158                    return Some(p.clone());
159                }
160            }
161        }
162
163        None
164    }
165
166    /// Resolve a method, walking up the inheritance chain.
167    pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<MethodStorage> {
168        // PHP method names are case-insensitive — normalize to lowercase for all lookups.
169        let method_lower = method_name.to_lowercase();
170        let method_name = method_lower.as_str();
171        // Check class methods first
172        if let Some(cls) = self.classes.get(fqcn) {
173            if let Some(m) = cls.get_method(method_name) {
174                return Some(m.clone());
175            }
176        }
177        // Check interface methods (including parent interfaces via all_parents)
178        if let Some(iface) = self.interfaces.get(fqcn) {
179            if let Some(m) = iface.own_methods.get(method_name).or_else(|| {
180                iface
181                    .own_methods
182                    .iter()
183                    .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name))
184                    .map(|(_, v)| v)
185            }) {
186                return Some(m.clone());
187            }
188            // Traverse parent interfaces
189            let parents = iface.all_parents.clone();
190            for parent_fqcn in &parents {
191                if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
192                    if let Some(m) = parent_iface.own_methods.get(method_name).or_else(|| {
193                        parent_iface
194                            .own_methods
195                            .iter()
196                            .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name))
197                            .map(|(_, v)| v)
198                    }) {
199                        return Some(m.clone());
200                    }
201                }
202            }
203        }
204        // Check trait methods (when a variable is annotated with a trait type)
205        if let Some(tr) = self.traits.get(fqcn) {
206            if let Some(m) = tr.own_methods.get(method_name).or_else(|| {
207                tr.own_methods
208                    .iter()
209                    .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name))
210                    .map(|(_, v)| v)
211            }) {
212                return Some(m.clone());
213            }
214        }
215        // Check enum methods
216        if let Some(e) = self.enums.get(fqcn) {
217            if let Some(m) = e.own_methods.get(method_name).or_else(|| {
218                e.own_methods
219                    .iter()
220                    .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name))
221                    .map(|(_, v)| v)
222            }) {
223                return Some(m.clone());
224            }
225            // PHP 8.1 built-in enum methods: cases(), from(), tryFrom()
226            if matches!(method_name, "cases" | "from" | "tryfrom") {
227                return Some(crate::storage::MethodStorage {
228                    fqcn: Arc::from(fqcn),
229                    name: Arc::from(method_name),
230                    params: vec![],
231                    return_type: Some(mir_types::Union::mixed()),
232                    inferred_return_type: None,
233                    visibility: crate::storage::Visibility::Public,
234                    is_static: true,
235                    is_abstract: false,
236                    is_constructor: false,
237                    template_params: vec![],
238                    assertions: vec![],
239                    throws: vec![],
240                    is_final: false,
241                    is_internal: false,
242                    is_pure: false,
243                    is_deprecated: false,
244                    location: None,
245                });
246            }
247        }
248        None
249    }
250
251    /// Returns true if `child` extends or implements `ancestor` (transitively).
252    pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
253        if child == ancestor {
254            return true;
255        }
256        if let Some(cls) = self.classes.get(child) {
257            return cls.implements_or_extends(ancestor);
258        }
259        if let Some(iface) = self.interfaces.get(child) {
260            return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
261        }
262        // Enum: backed enums implicitly implement BackedEnum (and UnitEnum);
263        // pure enums implicitly implement UnitEnum.
264        if let Some(en) = self.enums.get(child) {
265            // Check explicitly declared interfaces (e.g. implements SomeInterface)
266            if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
267                return true;
268            }
269            // PHP built-in: every enum implements UnitEnum
270            if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
271                return true;
272            }
273            // Backed enums implement BackedEnum
274            if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
275            {
276                return true;
277            }
278        }
279        false
280    }
281
282    /// Whether a class/interface/trait/enum with this FQCN exists.
283    pub fn type_exists(&self, fqcn: &str) -> bool {
284        self.classes.contains_key(fqcn)
285            || self.interfaces.contains_key(fqcn)
286            || self.traits.contains_key(fqcn)
287            || self.enums.contains_key(fqcn)
288    }
289
290    pub fn function_exists(&self, fqn: &str) -> bool {
291        self.functions.contains_key(fqn)
292    }
293
294    /// Returns true if the class is declared abstract.
295    /// Used to suppress `UndefinedMethod` on abstract class receivers: the concrete
296    /// subclass is expected to implement the method, matching Psalm errorLevel=3 behaviour.
297    pub fn is_abstract_class(&self, fqcn: &str) -> bool {
298        self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
299    }
300
301    /// Return the declared template params for `fqcn` (class or interface), or
302    /// an empty vec if the type is not found or has no templates.
303    pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
304        if let Some(cls) = self.classes.get(fqcn) {
305            return cls.template_params.clone();
306        }
307        if let Some(iface) = self.interfaces.get(fqcn) {
308            return iface.template_params.clone();
309        }
310        if let Some(tr) = self.traits.get(fqcn) {
311            return tr.template_params.clone();
312        }
313        vec![]
314    }
315
316    /// Returns true if the class (or any ancestor/trait) defines a `__get` magic method.
317    /// Such classes allow arbitrary property access, suppressing UndefinedProperty.
318    pub fn has_magic_get(&self, fqcn: &str) -> bool {
319        if let Some(cls) = self.classes.get(fqcn) {
320            if cls.own_methods.contains_key("__get") || cls.all_methods.contains_key("__get") {
321                return true;
322            }
323            // Check traits
324            let traits = cls.traits.clone();
325            drop(cls);
326            for tr in &traits {
327                if let Some(t) = self.traits.get(tr.as_ref()) {
328                    if t.own_methods.contains_key("__get") {
329                        return true;
330                    }
331                }
332            }
333            // Check ancestors
334            let all_parents = {
335                if let Some(c) = self.classes.get(fqcn) {
336                    c.all_parents.clone()
337                } else {
338                    vec![]
339                }
340            };
341            for ancestor in &all_parents {
342                if let Some(anc) = self.classes.get(ancestor.as_ref()) {
343                    if anc.own_methods.contains_key("__get") {
344                        return true;
345                    }
346                }
347            }
348        }
349        false
350    }
351
352    /// Returns true if the class (or any of its ancestors) has a parent/interface/trait
353    /// that is NOT present in the codebase.  Used to suppress `UndefinedMethod` false
354    /// positives: if a method might be inherited from an unscanned external class we
355    /// cannot confirm or deny its existence.
356    ///
357    /// We use the pre-computed `all_parents` list (built during finalization) rather
358    /// than recursive DashMap lookups to avoid potential deadlocks.
359    pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
360        // For interfaces: check whether any parent interface is unknown.
361        if let Some(iface) = self.interfaces.get(fqcn) {
362            let parents = iface.all_parents.clone();
363            drop(iface);
364            for p in &parents {
365                if !self.type_exists(p.as_ref()) {
366                    return true;
367                }
368            }
369            return false;
370        }
371
372        // Clone the data we need so the DashMap ref is dropped before any further lookups.
373        let (parent, interfaces, traits, all_parents) = {
374            let Some(cls) = self.classes.get(fqcn) else {
375                return false;
376            };
377            (
378                cls.parent.clone(),
379                cls.interfaces.clone(),
380                cls.traits.clone(),
381                cls.all_parents.clone(),
382            )
383        };
384
385        // Fast path: check direct parent/interfaces/traits
386        if let Some(ref p) = parent {
387            if !self.type_exists(p.as_ref()) {
388                return true;
389            }
390        }
391        for iface in &interfaces {
392            if !self.type_exists(iface.as_ref()) {
393                return true;
394            }
395        }
396        for tr in &traits {
397            if !self.type_exists(tr.as_ref()) {
398                return true;
399            }
400        }
401
402        // Also check the full ancestor chain (pre-computed during finalization)
403        for ancestor in &all_parents {
404            if !self.type_exists(ancestor.as_ref()) {
405                return true;
406            }
407        }
408
409        false
410    }
411
412    /// Resolve a short class/function name to its FQCN using the import table
413    /// and namespace recorded for `file` during Pass 1.
414    ///
415    /// - Names already containing `\` (after stripping a leading `\`) are
416    ///   returned as-is (already fully qualified).
417    /// - `self`, `parent`, `static` are returned unchanged (caller handles them).
418    pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
419        let name = name.trim_start_matches('\\');
420        if name.is_empty() {
421            return name.to_string();
422        }
423        // Fully qualified absolute paths start with '\' (already stripped above).
424        // Names containing '\' but not starting with it may be:
425        //   - Already-resolved FQCNs (e.g. Frontify\Util\Foo) — check type_exists
426        //   - Qualified relative names (e.g. Option\Some from within Frontify\Utility) — need namespace prefix
427        if name.contains('\\') {
428            // Check if the leading segment matches a use-import alias
429            let first_segment = name.split('\\').next().unwrap_or(name);
430            if let Some(imports) = self.file_imports.get(file) {
431                if let Some(resolved_prefix) = imports.get(first_segment) {
432                    let rest = &name[first_segment.len()..]; // includes leading '\'
433                    return format!("{}{}", resolved_prefix, rest);
434                }
435            }
436            // If already known in codebase as-is, it's FQCN — trust it
437            if self.type_exists(name) {
438                return name.to_string();
439            }
440            // Otherwise it's a relative qualified name — prepend the file namespace
441            if let Some(ns) = self.file_namespaces.get(file) {
442                let qualified = format!("{}\\{}", *ns, name);
443                if self.type_exists(&qualified) {
444                    return qualified;
445                }
446            }
447            return name.to_string();
448        }
449        // Built-in pseudo-types / keywords handled by the caller
450        match name {
451            "self" | "parent" | "static" | "this" => return name.to_string(),
452            _ => {}
453        }
454        // Check use aliases for this file (PHP class names are case-insensitive)
455        if let Some(imports) = self.file_imports.get(file) {
456            if let Some(resolved) = imports.get(name) {
457                return resolved.clone();
458            }
459            // Fall back to case-insensitive alias lookup
460            let name_lower = name.to_lowercase();
461            for (alias, resolved) in imports.iter() {
462                if alias.to_lowercase() == name_lower {
463                    return resolved.clone();
464                }
465            }
466        }
467        // Qualify with the file's namespace if one exists
468        if let Some(ns) = self.file_namespaces.get(file) {
469            let qualified = format!("{}\\{}", *ns, name);
470            // If the namespaced version exists in the codebase, use it.
471            // Otherwise fall back to the global (unqualified) name if that exists.
472            // This handles `DateTimeInterface`, `Exception`, etc. used without import
473            // while not overriding user-defined classes in namespaces.
474            if self.type_exists(&qualified) {
475                return qualified;
476            }
477            if self.type_exists(name) {
478                return name.to_string();
479            }
480            return qualified;
481        }
482        name.to_string()
483    }
484
485    // -----------------------------------------------------------------------
486    // Definition location lookups
487    // -----------------------------------------------------------------------
488
489    /// Look up the definition location of any symbol (class, interface, trait, enum, function).
490    /// Returns the file path and byte offsets.
491    pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
492        if let Some(cls) = self.classes.get(fqcn) {
493            return cls.location.clone();
494        }
495        if let Some(iface) = self.interfaces.get(fqcn) {
496            return iface.location.clone();
497        }
498        if let Some(tr) = self.traits.get(fqcn) {
499            return tr.location.clone();
500        }
501        if let Some(en) = self.enums.get(fqcn) {
502            return en.location.clone();
503        }
504        if let Some(func) = self.functions.get(fqcn) {
505            return func.location.clone();
506        }
507        None
508    }
509
510    /// Look up the definition location of a class member (method, property, constant).
511    pub fn get_member_location(
512        &self,
513        fqcn: &str,
514        member_name: &str,
515    ) -> Option<crate::storage::Location> {
516        // Check methods
517        if let Some(method) = self.get_method(fqcn, member_name) {
518            return method.location.clone();
519        }
520        // Check properties
521        if let Some(prop) = self.get_property(fqcn, member_name) {
522            return prop.location.clone();
523        }
524        // Check class constants
525        if let Some(cls) = self.classes.get(fqcn) {
526            if let Some(c) = cls.own_constants.get(member_name) {
527                return c.location.clone();
528            }
529        }
530        // Check interface constants
531        if let Some(iface) = self.interfaces.get(fqcn) {
532            if let Some(c) = iface.own_constants.get(member_name) {
533                return c.location.clone();
534            }
535        }
536        // Check trait constants
537        if let Some(tr) = self.traits.get(fqcn) {
538            if let Some(c) = tr.own_constants.get(member_name) {
539                return c.location.clone();
540            }
541        }
542        // Check enum constants and cases
543        if let Some(en) = self.enums.get(fqcn) {
544            if let Some(c) = en.own_constants.get(member_name) {
545                return c.location.clone();
546            }
547            if let Some(case) = en.cases.get(member_name) {
548                return case.location.clone();
549            }
550        }
551        None
552    }
553
554    // -----------------------------------------------------------------------
555    // Reference tracking (M18 dead-code detection)
556    // -----------------------------------------------------------------------
557
558    /// Mark a method as referenced from user code.
559    pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
560        let key: Arc<str> = Arc::from(format!("{}::{}", fqcn, method_name.to_lowercase()).as_str());
561        self.referenced_methods.insert(key);
562    }
563
564    /// Mark a property as referenced from user code.
565    pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
566        let key: Arc<str> = Arc::from(format!("{}::{}", fqcn, prop_name).as_str());
567        self.referenced_properties.insert(key);
568    }
569
570    /// Mark a free function as referenced from user code.
571    pub fn mark_function_referenced(&self, fqn: &str) {
572        self.referenced_functions.insert(Arc::from(fqn));
573    }
574
575    pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
576        let key = format!("{}::{}", fqcn, method_name.to_lowercase());
577        self.referenced_methods.contains(key.as_str())
578    }
579
580    pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
581        let key = format!("{}::{}", fqcn, prop_name);
582        self.referenced_properties.contains(key.as_str())
583    }
584
585    pub fn is_function_referenced(&self, fqn: &str) -> bool {
586        self.referenced_functions.contains(fqn)
587    }
588
589    // -----------------------------------------------------------------------
590    // Finalization
591    // -----------------------------------------------------------------------
592
593    /// Must be called after all files have been parsed (pass 1 complete).
594    /// Resolves inheritance chains and builds method dispatch tables.
595    pub fn finalize(&self) {
596        if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
597            return;
598        }
599
600        // 1. Resolve all_parents for classes
601        let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
602        for fqcn in &class_keys {
603            let parents = self.collect_class_ancestors(fqcn);
604            if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
605                cls.all_parents = parents;
606            }
607        }
608
609        // 2. Build method dispatch tables for classes (own methods override inherited)
610        for fqcn in &class_keys {
611            let all_methods = self.build_method_table(fqcn);
612            if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
613                cls.all_methods = all_methods;
614            }
615        }
616
617        // 3. Resolve all_parents for interfaces
618        let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
619        for fqcn in &iface_keys {
620            let parents = self.collect_interface_ancestors(fqcn);
621            if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
622                iface.all_parents = parents;
623            }
624        }
625
626        self.finalized
627            .store(true, std::sync::atomic::Ordering::SeqCst);
628    }
629
630    // -----------------------------------------------------------------------
631    // Private helpers
632    // -----------------------------------------------------------------------
633
634    fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
635        let mut result = Vec::new();
636        let mut visited = std::collections::HashSet::new();
637        self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
638        result
639    }
640
641    fn collect_class_ancestors_inner(
642        &self,
643        fqcn: &str,
644        out: &mut Vec<Arc<str>>,
645        visited: &mut std::collections::HashSet<String>,
646    ) {
647        if !visited.insert(fqcn.to_string()) {
648            return; // cycle guard
649        }
650        let (parent, interfaces, traits) = {
651            if let Some(cls) = self.classes.get(fqcn) {
652                (
653                    cls.parent.clone(),
654                    cls.interfaces.clone(),
655                    cls.traits.clone(),
656                )
657            } else {
658                return;
659            }
660        };
661
662        if let Some(p) = parent {
663            out.push(p.clone());
664            self.collect_class_ancestors_inner(&p, out, visited);
665        }
666        for iface in interfaces {
667            out.push(iface.clone());
668            self.collect_interface_ancestors_inner(&iface, out, visited);
669        }
670        for t in traits {
671            out.push(t);
672        }
673    }
674
675    fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
676        let mut result = Vec::new();
677        let mut visited = std::collections::HashSet::new();
678        self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
679        result
680    }
681
682    fn collect_interface_ancestors_inner(
683        &self,
684        fqcn: &str,
685        out: &mut Vec<Arc<str>>,
686        visited: &mut std::collections::HashSet<String>,
687    ) {
688        if !visited.insert(fqcn.to_string()) {
689            return;
690        }
691        let extends = {
692            if let Some(iface) = self.interfaces.get(fqcn) {
693                iface.extends.clone()
694            } else {
695                return;
696            }
697        };
698        for e in extends {
699            out.push(e.clone());
700            self.collect_interface_ancestors_inner(&e, out, visited);
701        }
702    }
703
704    /// Build the full method dispatch table for a class, with own methods taking
705    /// priority over inherited ones.
706    fn build_method_table(&self, fqcn: &str) -> indexmap::IndexMap<Arc<str>, MethodStorage> {
707        use indexmap::IndexMap;
708        let mut table: IndexMap<Arc<str>, MethodStorage> = IndexMap::new();
709
710        // Walk ancestor chain (broad-first from root → child, so child overrides root)
711        let ancestors = {
712            if let Some(cls) = self.classes.get(fqcn) {
713                cls.all_parents.clone()
714            } else {
715                return table;
716            }
717        };
718
719        // Insert ancestor methods (deepest ancestor first, so closer ancestors override).
720        // Also insert trait methods from ancestor classes.
721        for ancestor_fqcn in ancestors.iter().rev() {
722            if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
723                // First insert ancestor's own trait methods (lower priority)
724                let ancestor_traits = ancestor.traits.clone();
725                for trait_fqcn in ancestor_traits.iter().rev() {
726                    if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
727                        for (name, method) in &tr.own_methods {
728                            table.insert(name.clone(), method.clone());
729                        }
730                    }
731                }
732                // Then ancestor's own methods (override trait methods)
733                for (name, method) in &ancestor.own_methods {
734                    table.insert(name.clone(), method.clone());
735                }
736            } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
737                for (name, method) in &iface.own_methods {
738                    table.insert(name.clone(), method.clone());
739                }
740            }
741        }
742
743        // Insert the class's own trait methods
744        let trait_list = {
745            if let Some(cls) = self.classes.get(fqcn) {
746                cls.traits.clone()
747            } else {
748                vec![]
749            }
750        };
751        for trait_fqcn in &trait_list {
752            if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
753                for (name, method) in &tr.own_methods {
754                    table.insert(name.clone(), method.clone());
755                }
756            }
757        }
758
759        // Own methods override everything
760        if let Some(cls) = self.classes.get(fqcn) {
761            for (name, method) in &cls.own_methods {
762                table.insert(name.clone(), method.clone());
763            }
764        }
765
766        table
767    }
768}