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    /// Per-file `use` alias maps: alias → FQCN.  Populated during Pass 1.
32    pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
33    /// Per-file current namespace (if any).  Populated during Pass 1.
34    pub file_namespaces: DashMap<Arc<str>, String>,
35
36    /// Whether finalize() has been called.
37    finalized: std::sync::atomic::AtomicBool,
38}
39
40impl Codebase {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    // -----------------------------------------------------------------------
46    // Lookups
47    // -----------------------------------------------------------------------
48
49    /// Resolve a property, walking up the inheritance chain (parent classes and traits).
50    pub fn get_property(&self, fqcn: &str, prop_name: &str) -> Option<crate::storage::PropertyStorage> {
51        // Check direct class own_properties
52        if let Some(cls) = self.classes.get(fqcn) {
53            if let Some(p) = cls.own_properties.get(prop_name) {
54                return Some(p.clone());
55            }
56        }
57
58        // Walk all ancestors (collected during finalize)
59        let all_parents = {
60            if let Some(cls) = self.classes.get(fqcn) {
61                cls.all_parents.clone()
62            } else {
63                return None;
64            }
65        };
66
67        for ancestor_fqcn in &all_parents {
68            if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
69                if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
70                    return Some(p.clone());
71                }
72            }
73        }
74
75        // Check traits
76        let trait_list = {
77            if let Some(cls) = self.classes.get(fqcn) {
78                cls.traits.clone()
79            } else {
80                vec![]
81            }
82        };
83        for trait_fqcn in &trait_list {
84            if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
85                if let Some(p) = tr.own_properties.get(prop_name) {
86                    return Some(p.clone());
87                }
88            }
89        }
90
91        None
92    }
93
94    /// Resolve a method, walking up the inheritance chain.
95    pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<MethodStorage> {
96        // PHP method names are case-insensitive — normalize to lowercase for all lookups.
97        let method_lower = method_name.to_lowercase();
98        let method_name = method_lower.as_str();
99        // Check class methods first
100        if let Some(cls) = self.classes.get(fqcn) {
101            if let Some(m) = cls.get_method(method_name) {
102                return Some(m.clone());
103            }
104        }
105        // Check interface methods (including parent interfaces via all_parents)
106        if let Some(iface) = self.interfaces.get(fqcn) {
107            if let Some(m) = iface.own_methods.get(method_name)
108                .or_else(|| iface.own_methods.iter().find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name)).map(|(_, v)| v))
109            {
110                return Some(m.clone());
111            }
112            // Traverse parent interfaces
113            let parents = iface.all_parents.clone();
114            for parent_fqcn in &parents {
115                if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
116                    if let Some(m) = parent_iface.own_methods.get(method_name)
117                        .or_else(|| parent_iface.own_methods.iter().find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name)).map(|(_, v)| v))
118                    {
119                        return Some(m.clone());
120                    }
121                }
122            }
123        }
124        // Check trait methods (when a variable is annotated with a trait type)
125        if let Some(tr) = self.traits.get(fqcn) {
126            if let Some(m) = tr.own_methods.get(method_name)
127                .or_else(|| tr.own_methods.iter().find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name)).map(|(_, v)| v))
128            {
129                return Some(m.clone());
130            }
131        }
132        // Check enum methods
133        if let Some(e) = self.enums.get(fqcn) {
134            if let Some(m) = e.own_methods.get(method_name)
135                .or_else(|| e.own_methods.iter().find(|(k, _)| k.as_ref().eq_ignore_ascii_case(method_name)).map(|(_, v)| v))
136            {
137                return Some(m.clone());
138            }
139            // PHP 8.1 built-in enum methods: cases(), from(), tryFrom()
140            if matches!(method_name, "cases" | "from" | "tryfrom") {
141                return Some(crate::storage::MethodStorage {
142                    fqcn: Arc::from(fqcn),
143                    name: Arc::from(method_name),
144                    params: vec![],
145                    return_type: Some(mir_types::Union::mixed()),
146                    inferred_return_type: None,
147                    visibility: crate::storage::Visibility::Public,
148                    is_static: true,
149                    is_abstract: false,
150                    is_constructor: false,
151                    template_params: vec![],
152                    assertions: vec![],
153                    throws: vec![],
154                    is_final: false,
155                    is_internal: false,
156                    is_pure: false,
157                    is_deprecated: false,
158                    location: None,
159                });
160            }
161        }
162        None
163    }
164
165    /// Returns true if `child` extends or implements `ancestor` (transitively).
166    pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
167        if child == ancestor {
168            return true;
169        }
170        if let Some(cls) = self.classes.get(child) {
171            return cls.implements_or_extends(ancestor);
172        }
173        if let Some(iface) = self.interfaces.get(child) {
174            return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
175        }
176        // Enum: backed enums implicitly implement BackedEnum (and UnitEnum);
177        // pure enums implicitly implement UnitEnum.
178        if let Some(en) = self.enums.get(child) {
179            // Check explicitly declared interfaces (e.g. implements SomeInterface)
180            if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
181                return true;
182            }
183            // PHP built-in: every enum implements UnitEnum
184            if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
185                return true;
186            }
187            // Backed enums implement BackedEnum
188            if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some() {
189                return true;
190            }
191        }
192        false
193    }
194
195    /// Whether a class/interface/trait/enum with this FQCN exists.
196    pub fn type_exists(&self, fqcn: &str) -> bool {
197        self.classes.contains_key(fqcn)
198            || self.interfaces.contains_key(fqcn)
199            || self.traits.contains_key(fqcn)
200            || self.enums.contains_key(fqcn)
201    }
202
203    pub fn function_exists(&self, fqn: &str) -> bool {
204        self.functions.contains_key(fqn)
205    }
206
207    /// Returns true if the class (or any ancestor/trait) defines a `__get` magic method.
208    /// Such classes allow arbitrary property access, suppressing UndefinedProperty.
209    pub fn has_magic_get(&self, fqcn: &str) -> bool {
210        if let Some(cls) = self.classes.get(fqcn) {
211            if cls.own_methods.contains_key("__get") || cls.all_methods.contains_key("__get") {
212                return true;
213            }
214            // Check traits
215            let traits = cls.traits.clone();
216            drop(cls);
217            for tr in &traits {
218                if let Some(t) = self.traits.get(tr.as_ref()) {
219                    if t.own_methods.contains_key("__get") {
220                        return true;
221                    }
222                }
223            }
224            // Check ancestors
225            let all_parents = {
226                if let Some(c) = self.classes.get(fqcn) { c.all_parents.clone() } else { vec![] }
227            };
228            for ancestor in &all_parents {
229                if let Some(anc) = self.classes.get(ancestor.as_ref()) {
230                    if anc.own_methods.contains_key("__get") {
231                        return true;
232                    }
233                }
234            }
235        }
236        false
237    }
238
239    /// Returns true if the class (or any of its ancestors) has a parent/interface/trait
240    /// that is NOT present in the codebase.  Used to suppress `UndefinedMethod` false
241    /// positives: if a method might be inherited from an unscanned external class we
242    /// cannot confirm or deny its existence.
243    ///
244    /// We use the pre-computed `all_parents` list (built during finalization) rather
245    /// than recursive DashMap lookups to avoid potential deadlocks.
246    pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
247        // For interfaces: check whether any parent interface is unknown.
248        if let Some(iface) = self.interfaces.get(fqcn) {
249            let parents = iface.all_parents.clone();
250            drop(iface);
251            for p in &parents {
252                if !self.type_exists(p.as_ref()) {
253                    return true;
254                }
255            }
256            return false;
257        }
258
259        // Clone the data we need so the DashMap ref is dropped before any further lookups.
260        let (parent, interfaces, traits, all_parents) = {
261            let Some(cls) = self.classes.get(fqcn) else { return false };
262            (
263                cls.parent.clone(),
264                cls.interfaces.clone(),
265                cls.traits.clone(),
266                cls.all_parents.clone(),
267            )
268        };
269
270        // Fast path: check direct parent/interfaces/traits
271        if let Some(ref p) = parent {
272            if !self.type_exists(p.as_ref()) {
273                return true;
274            }
275        }
276        for iface in &interfaces {
277            if !self.type_exists(iface.as_ref()) {
278                return true;
279            }
280        }
281        for tr in &traits {
282            if !self.type_exists(tr.as_ref()) {
283                return true;
284            }
285        }
286
287        // Also check the full ancestor chain (pre-computed during finalization)
288        for ancestor in &all_parents {
289            if !self.type_exists(ancestor.as_ref()) {
290                return true;
291            }
292        }
293
294        false
295    }
296
297    /// Resolve a short class/function name to its FQCN using the import table
298    /// and namespace recorded for `file` during Pass 1.
299    ///
300    /// - Names already containing `\` (after stripping a leading `\`) are
301    ///   returned as-is (already fully qualified).
302    /// - `self`, `parent`, `static` are returned unchanged (caller handles them).
303    pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
304        let name = name.trim_start_matches('\\');
305        if name.is_empty() {
306            return name.to_string();
307        }
308        // Fully qualified absolute paths start with '\' (already stripped above).
309        // Names containing '\' but not starting with it may be:
310        //   - Already-resolved FQCNs (e.g. Frontify\Util\Foo) — check type_exists
311        //   - Qualified relative names (e.g. Option\Some from within Frontify\Utility) — need namespace prefix
312        if name.contains('\\') {
313            // Check if the leading segment matches a use-import alias
314            let first_segment = name.split('\\').next().unwrap_or(name);
315            if let Some(imports) = self.file_imports.get(file) {
316                if let Some(resolved_prefix) = imports.get(first_segment) {
317                    let rest = &name[first_segment.len()..]; // includes leading '\'
318                    return format!("{}{}", resolved_prefix, rest);
319                }
320            }
321            // If already known in codebase as-is, it's FQCN — trust it
322            if self.type_exists(name) {
323                return name.to_string();
324            }
325            // Otherwise it's a relative qualified name — prepend the file namespace
326            if let Some(ns) = self.file_namespaces.get(file) {
327                let qualified = format!("{}\\{}", *ns, name);
328                if self.type_exists(&qualified) {
329                    return qualified;
330                }
331            }
332            return name.to_string();
333        }
334        // Built-in pseudo-types / keywords handled by the caller
335        match name {
336            "self" | "parent" | "static" | "this" => return name.to_string(),
337            _ => {}
338        }
339        // Check use aliases for this file (PHP class names are case-insensitive)
340        if let Some(imports) = self.file_imports.get(file) {
341            if let Some(resolved) = imports.get(name) {
342                return resolved.clone();
343            }
344            // Fall back to case-insensitive alias lookup
345            let name_lower = name.to_lowercase();
346            for (alias, resolved) in imports.iter() {
347                if alias.to_lowercase() == name_lower {
348                    return resolved.clone();
349                }
350            }
351        }
352        // Qualify with the file's namespace if one exists
353        if let Some(ns) = self.file_namespaces.get(file) {
354            let qualified = format!("{}\\{}", *ns, name);
355            // If the namespaced version exists in the codebase, use it.
356            // Otherwise fall back to the global (unqualified) name if that exists.
357            // This handles `DateTimeInterface`, `Exception`, etc. used without import
358            // while not overriding user-defined classes in namespaces.
359            if self.type_exists(&qualified) {
360                return qualified;
361            }
362            if self.type_exists(name) {
363                return name.to_string();
364            }
365            return qualified;
366        }
367        name.to_string()
368    }
369
370    // -----------------------------------------------------------------------
371    // Reference tracking (M18 dead-code detection)
372    // -----------------------------------------------------------------------
373
374    /// Mark a method as referenced from user code.
375    pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
376        let key: Arc<str> = Arc::from(format!("{}::{}", fqcn, method_name.to_lowercase()).as_str());
377        self.referenced_methods.insert(key);
378    }
379
380    /// Mark a property as referenced from user code.
381    pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
382        let key: Arc<str> = Arc::from(format!("{}::{}", fqcn, prop_name).as_str());
383        self.referenced_properties.insert(key);
384    }
385
386    /// Mark a free function as referenced from user code.
387    pub fn mark_function_referenced(&self, fqn: &str) {
388        self.referenced_functions.insert(Arc::from(fqn));
389    }
390
391    pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
392        let key = format!("{}::{}", fqcn, method_name.to_lowercase());
393        self.referenced_methods.contains(key.as_str())
394    }
395
396    pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
397        let key = format!("{}::{}", fqcn, prop_name);
398        self.referenced_properties.contains(key.as_str())
399    }
400
401    pub fn is_function_referenced(&self, fqn: &str) -> bool {
402        self.referenced_functions.contains(fqn)
403    }
404
405    // -----------------------------------------------------------------------
406    // Finalization
407    // -----------------------------------------------------------------------
408
409    /// Must be called after all files have been parsed (pass 1 complete).
410    /// Resolves inheritance chains and builds method dispatch tables.
411    pub fn finalize(&self) {
412        if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
413            return;
414        }
415
416        // 1. Resolve all_parents for classes
417        let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
418        for fqcn in &class_keys {
419            let parents = self.collect_class_ancestors(fqcn);
420            if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
421                cls.all_parents = parents;
422            }
423        }
424
425        // 2. Build method dispatch tables for classes (own methods override inherited)
426        for fqcn in &class_keys {
427            let all_methods = self.build_method_table(fqcn);
428            if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
429                cls.all_methods = all_methods;
430            }
431        }
432
433        // 3. Resolve all_parents for interfaces
434        let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
435        for fqcn in &iface_keys {
436            let parents = self.collect_interface_ancestors(fqcn);
437            if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
438                iface.all_parents = parents;
439            }
440        }
441
442        self.finalized.store(true, std::sync::atomic::Ordering::SeqCst);
443    }
444
445    // -----------------------------------------------------------------------
446    // Private helpers
447    // -----------------------------------------------------------------------
448
449    fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
450        let mut result = Vec::new();
451        let mut visited = std::collections::HashSet::new();
452        self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
453        result
454    }
455
456    fn collect_class_ancestors_inner(
457        &self,
458        fqcn: &str,
459        out: &mut Vec<Arc<str>>,
460        visited: &mut std::collections::HashSet<String>,
461    ) {
462        if !visited.insert(fqcn.to_string()) {
463            return; // cycle guard
464        }
465        let (parent, interfaces, traits) = {
466            if let Some(cls) = self.classes.get(fqcn) {
467                (
468                    cls.parent.clone(),
469                    cls.interfaces.clone(),
470                    cls.traits.clone(),
471                )
472            } else {
473                return;
474            }
475        };
476
477        if let Some(p) = parent {
478            out.push(p.clone());
479            self.collect_class_ancestors_inner(&p, out, visited);
480        }
481        for iface in interfaces {
482            out.push(iface.clone());
483            self.collect_interface_ancestors_inner(&iface, out, visited);
484        }
485        for t in traits {
486            out.push(t);
487        }
488    }
489
490    fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
491        let mut result = Vec::new();
492        let mut visited = std::collections::HashSet::new();
493        self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
494        result
495    }
496
497    fn collect_interface_ancestors_inner(
498        &self,
499        fqcn: &str,
500        out: &mut Vec<Arc<str>>,
501        visited: &mut std::collections::HashSet<String>,
502    ) {
503        if !visited.insert(fqcn.to_string()) {
504            return;
505        }
506        let extends = {
507            if let Some(iface) = self.interfaces.get(fqcn) {
508                iface.extends.clone()
509            } else {
510                return;
511            }
512        };
513        for e in extends {
514            out.push(e.clone());
515            self.collect_interface_ancestors_inner(&e, out, visited);
516        }
517    }
518
519    /// Build the full method dispatch table for a class, with own methods taking
520    /// priority over inherited ones.
521    fn build_method_table(&self, fqcn: &str) -> indexmap::IndexMap<Arc<str>, MethodStorage> {
522        use indexmap::IndexMap;
523        let mut table: IndexMap<Arc<str>, MethodStorage> = IndexMap::new();
524
525        // Walk ancestor chain (broad-first from root → child, so child overrides root)
526        let ancestors = {
527            if let Some(cls) = self.classes.get(fqcn) {
528                cls.all_parents.clone()
529            } else {
530                return table;
531            }
532        };
533
534        // Insert ancestor methods (deepest ancestor first, so closer ancestors override).
535        // Also insert trait methods from ancestor classes.
536        for ancestor_fqcn in ancestors.iter().rev() {
537            if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
538                // First insert ancestor's own trait methods (lower priority)
539                let ancestor_traits = ancestor.traits.clone();
540                for trait_fqcn in ancestor_traits.iter().rev() {
541                    if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
542                        for (name, method) in &tr.own_methods {
543                            table.insert(name.clone(), method.clone());
544                        }
545                    }
546                }
547                // Then ancestor's own methods (override trait methods)
548                for (name, method) in &ancestor.own_methods {
549                    table.insert(name.clone(), method.clone());
550                }
551            } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
552                for (name, method) in &iface.own_methods {
553                    table.insert(name.clone(), method.clone());
554                }
555            }
556        }
557
558        // Insert the class's own trait methods
559        let trait_list = {
560            if let Some(cls) = self.classes.get(fqcn) {
561                cls.traits.clone()
562            } else {
563                vec![]
564            }
565        };
566        for trait_fqcn in &trait_list {
567            if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
568                for (name, method) in &tr.own_methods {
569                    table.insert(name.clone(), method.clone());
570                }
571            }
572        }
573
574        // Own methods override everything
575        if let Some(cls) = self.classes.get(fqcn) {
576            for (name, method) in &cls.own_methods {
577                table.insert(name.clone(), method.clone());
578            }
579        }
580
581        table
582    }
583}