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