mago_codex/metadata/
mod.rs

1use std::borrow::Cow;
2use std::collections::hash_map::Entry;
3
4use ahash::HashMap;
5use ahash::HashSet;
6use serde::Deserialize;
7use serde::Serialize;
8
9use mago_atom::Atom;
10use mago_atom::AtomMap;
11use mago_atom::AtomSet;
12use mago_atom::ascii_lowercase_atom;
13use mago_atom::ascii_lowercase_constant_name_atom;
14use mago_atom::atom;
15use mago_atom::empty_atom;
16use mago_atom::u32_atom;
17use mago_atom::u64_atom;
18use mago_database::file::FileId;
19use mago_reporting::IssueCollection;
20use mago_span::Position;
21
22use crate::identifier::method::MethodIdentifier;
23use crate::metadata::class_like::ClassLikeMetadata;
24use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
25use crate::metadata::constant::ConstantMetadata;
26use crate::metadata::enum_case::EnumCaseMetadata;
27use crate::metadata::function_like::FunctionLikeMetadata;
28use crate::metadata::property::PropertyMetadata;
29use crate::metadata::ttype::TypeMetadata;
30use crate::symbol::SymbolKind;
31use crate::symbol::Symbols;
32use crate::ttype::atomic::TAtomic;
33use crate::ttype::atomic::object::TObject;
34use crate::ttype::union::TUnion;
35
36pub mod attribute;
37pub mod class_like;
38pub mod class_like_constant;
39pub mod constant;
40pub mod enum_case;
41pub mod flags;
42pub mod function_like;
43pub mod parameter;
44pub mod property;
45pub mod ttype;
46
47/// Holds all analyzed information about the symbols, structures, and relationships within a codebase.
48///
49/// This acts as the central repository for metadata gathered during static analysis,
50/// including details about classes, interfaces, traits, enums, functions, constants,
51/// their members, inheritance, dependencies, and associated types.
52#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
53pub struct CodebaseMetadata {
54    /// Configuration flag: Should types be inferred based on usage patterns?
55    pub infer_types_from_usage: bool,
56    /// Map from type alias name (`Atom`) to its metadata (`TypeMetadata`).
57    pub aliases: AtomMap<TypeMetadata>,
58    /// Map from class-like FQCN (`Atom`) to its detailed metadata (`ClassLikeMetadata`).
59    pub class_likes: AtomMap<ClassLikeMetadata>,
60    /// Map from a function/method identifier tuple `(scope_id, function_id)` to its metadata (`FunctionLikeMetadata`).
61    /// `scope_id` is the FQCN for methods or often `Atom::empty()` for global functions.
62    pub function_likes: HashMap<(Atom, Atom), FunctionLikeMetadata>,
63    /// Stores the kind (Class, Interface, etc.) for every known symbol FQCN.
64    pub symbols: Symbols,
65    /// Map from global constant FQN (`Atom`) to its metadata (`ConstantMetadata`).
66    pub constants: AtomMap<ConstantMetadata>,
67    /// Map from class/interface FQCN to the set of all its descendants (recursive).
68    pub all_class_like_descendants: AtomMap<AtomSet>,
69    /// Map from class/interface FQCN to the set of its direct descendants (children).
70    pub direct_classlike_descendants: AtomMap<AtomSet>,
71    /// Set of symbols (FQCNs) that are considered safe/validated.
72    pub safe_symbols: AtomSet,
73    /// Set of specific members `(SymbolFQCN, MemberName)` that are considered safe/validated.
74    pub safe_symbol_members: HashSet<(Atom, Atom)>,
75}
76
77impl CodebaseMetadata {
78    // Construction
79
80    /// Creates a new, empty `CodebaseMetadata` with default values.
81    #[inline]
82    pub fn new() -> Self {
83        Self::default()
84    }
85    // Symbol Existence Checks
86
87    /// Checks if a class exists in the codebase (case-insensitive).
88    ///
89    /// # Examples
90    /// ```ignore
91    /// if codebase.class_exists("MyClass") {
92    ///     // MyClass is a class
93    /// }
94    /// ```
95    #[inline]
96    pub fn class_exists(&self, name: &str) -> bool {
97        let lowercase_name = ascii_lowercase_atom(name);
98        matches!(self.symbols.get_kind(&lowercase_name), Some(SymbolKind::Class))
99    }
100
101    /// Checks if an interface exists in the codebase (case-insensitive).
102    #[inline]
103    pub fn interface_exists(&self, name: &str) -> bool {
104        let lowercase_name = ascii_lowercase_atom(name);
105        matches!(self.symbols.get_kind(&lowercase_name), Some(SymbolKind::Interface))
106    }
107
108    /// Checks if a trait exists in the codebase (case-insensitive).
109    #[inline]
110    pub fn trait_exists(&self, name: &str) -> bool {
111        let lowercase_name = ascii_lowercase_atom(name);
112        matches!(self.symbols.get_kind(&lowercase_name), Some(SymbolKind::Trait))
113    }
114
115    /// Checks if an enum exists in the codebase (case-insensitive).
116    #[inline]
117    pub fn enum_exists(&self, name: &str) -> bool {
118        let lowercase_name = ascii_lowercase_atom(name);
119        matches!(self.symbols.get_kind(&lowercase_name), Some(SymbolKind::Enum))
120    }
121
122    /// Checks if a class-like (class, interface, trait, or enum) exists (case-insensitive).
123    #[inline]
124    pub fn class_like_exists(&self, name: &str) -> bool {
125        let lowercase_name = ascii_lowercase_atom(name);
126        self.symbols.contains(&lowercase_name)
127    }
128
129    /// Checks if a class or trait exists in the codebase (case-insensitive).
130    #[inline]
131    pub fn class_or_trait_exists(&self, name: &str) -> bool {
132        let lowercase_name = ascii_lowercase_atom(name);
133        matches!(self.symbols.get_kind(&lowercase_name), Some(SymbolKind::Class | SymbolKind::Trait))
134    }
135
136    /// Checks if a class or interface exists in the codebase (case-insensitive).
137    #[inline]
138    pub fn class_or_interface_exists(&self, name: &str) -> bool {
139        let lowercase_name = ascii_lowercase_atom(name);
140        matches!(self.symbols.get_kind(&lowercase_name), Some(SymbolKind::Class | SymbolKind::Interface))
141    }
142
143    /// Checks if a method identifier exists in the codebase.
144    #[inline]
145    pub fn method_identifier_exists(&self, method_id: &MethodIdentifier) -> bool {
146        let lowercase_class = ascii_lowercase_atom(method_id.get_class_name());
147        let lowercase_method = ascii_lowercase_atom(method_id.get_method_name());
148        let identifier = (lowercase_class, lowercase_method);
149        self.function_likes.contains_key(&identifier)
150    }
151
152    /// Checks if a global function exists in the codebase (case-insensitive).
153    #[inline]
154    pub fn function_exists(&self, name: &str) -> bool {
155        let lowercase_name = ascii_lowercase_atom(name);
156        let identifier = (empty_atom(), lowercase_name);
157        self.function_likes.contains_key(&identifier)
158    }
159
160    /// Checks if a global constant exists in the codebase.
161    /// The namespace part is case-insensitive, but the constant name is case-sensitive.
162    #[inline]
163    pub fn constant_exists(&self, name: &str) -> bool {
164        let lowercase_name = ascii_lowercase_constant_name_atom(name);
165        self.constants.contains_key(&lowercase_name)
166    }
167
168    /// Checks if a method exists on a class-like, including inherited methods (case-insensitive).
169    #[inline]
170    pub fn method_exists(&self, class: &str, method: &str) -> bool {
171        let lowercase_class = ascii_lowercase_atom(class);
172        let lowercase_method = ascii_lowercase_atom(method);
173        self.class_likes
174            .get(&lowercase_class)
175            .is_some_and(|meta| meta.appearing_method_ids.contains_key(&lowercase_method))
176    }
177
178    /// Checks if a property exists on a class-like, including inherited properties.
179    /// Class name is case-insensitive, property name is case-sensitive.
180    #[inline]
181    pub fn property_exists(&self, class: &str, property: &str) -> bool {
182        let lowercase_class = ascii_lowercase_atom(class);
183        let property_name = atom(property);
184        self.class_likes
185            .get(&lowercase_class)
186            .is_some_and(|meta| meta.appearing_property_ids.contains_key(&property_name))
187    }
188
189    /// Checks if a class constant or enum case exists on a class-like.
190    /// Class name is case-insensitive, constant/case name is case-sensitive.
191    #[inline]
192    pub fn class_constant_exists(&self, class: &str, constant: &str) -> bool {
193        let lowercase_class = ascii_lowercase_atom(class);
194        let constant_name = atom(constant);
195        self.class_likes.get(&lowercase_class).is_some_and(|meta| {
196            meta.constants.contains_key(&constant_name) || meta.enum_cases.contains_key(&constant_name)
197        })
198    }
199
200    /// Checks if a method is declared directly in a class (not inherited).
201    #[inline]
202    pub fn method_is_declared_in_class(&self, class: &str, method: &str) -> bool {
203        let lowercase_class = ascii_lowercase_atom(class);
204        let lowercase_method = ascii_lowercase_atom(method);
205        self.class_likes
206            .get(&lowercase_class)
207            .and_then(|meta| meta.declaring_method_ids.get(&lowercase_method))
208            .is_some_and(|method_id| method_id.get_class_name() == &lowercase_class)
209    }
210
211    /// Checks if a property is declared directly in a class (not inherited).
212    #[inline]
213    pub fn property_is_declared_in_class(&self, class: &str, property: &str) -> bool {
214        let lowercase_class = ascii_lowercase_atom(class);
215        let property_name = atom(property);
216        self.class_likes.get(&lowercase_class).is_some_and(|meta| meta.properties.contains_key(&property_name))
217    }
218    // Metadata Retrieval - Class-likes
219
220    /// Retrieves metadata for a class (case-insensitive).
221    /// Returns `None` if the name doesn't correspond to a class.
222    #[inline]
223    pub fn get_class(&self, name: &str) -> Option<&ClassLikeMetadata> {
224        let lowercase_name = ascii_lowercase_atom(name);
225        if self.symbols.contains_class(&lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
226    }
227
228    /// Retrieves metadata for an interface (case-insensitive).
229    #[inline]
230    pub fn get_interface(&self, name: &str) -> Option<&ClassLikeMetadata> {
231        let lowercase_name = ascii_lowercase_atom(name);
232        if self.symbols.contains_interface(&lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
233    }
234
235    /// Retrieves metadata for a trait (case-insensitive).
236    #[inline]
237    pub fn get_trait(&self, name: &str) -> Option<&ClassLikeMetadata> {
238        let lowercase_name = ascii_lowercase_atom(name);
239        if self.symbols.contains_trait(&lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
240    }
241
242    /// Retrieves metadata for an enum (case-insensitive).
243    #[inline]
244    pub fn get_enum(&self, name: &str) -> Option<&ClassLikeMetadata> {
245        let lowercase_name = ascii_lowercase_atom(name);
246        if self.symbols.contains_enum(&lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
247    }
248
249    /// Retrieves metadata for any class-like structure (case-insensitive).
250    #[inline]
251    pub fn get_class_like(&self, name: &str) -> Option<&ClassLikeMetadata> {
252        let lowercase_name = ascii_lowercase_atom(name);
253        self.class_likes.get(&lowercase_name)
254    }
255    // Metadata Retrieval - Functions & Methods
256
257    /// Retrieves metadata for a global function (case-insensitive).
258    #[inline]
259    pub fn get_function(&self, name: &str) -> Option<&FunctionLikeMetadata> {
260        let lowercase_name = ascii_lowercase_atom(name);
261        let identifier = (empty_atom(), lowercase_name);
262        self.function_likes.get(&identifier)
263    }
264
265    /// Retrieves metadata for a method (case-insensitive for both class and method names).
266    #[inline]
267    pub fn get_method(&self, class: &str, method: &str) -> Option<&FunctionLikeMetadata> {
268        let lowercase_class = ascii_lowercase_atom(class);
269        let lowercase_method = ascii_lowercase_atom(method);
270        let identifier = (lowercase_class, lowercase_method);
271        self.function_likes.get(&identifier)
272    }
273
274    /// Retrieves metadata for a closure based on its file and position.
275    #[inline]
276    pub fn get_closure(&self, file_id: &FileId, position: &Position) -> Option<&FunctionLikeMetadata> {
277        let file_ref = u64_atom(file_id.as_u64());
278        let closure_ref = u32_atom(position.offset);
279        let identifier = (file_ref, closure_ref);
280        self.function_likes.get(&identifier)
281    }
282
283    /// Retrieves method metadata by MethodIdentifier.
284    #[inline]
285    pub fn get_method_by_id(&self, method_id: &MethodIdentifier) -> Option<&FunctionLikeMetadata> {
286        let lowercase_class = ascii_lowercase_atom(method_id.get_class_name());
287        let lowercase_method = ascii_lowercase_atom(method_id.get_method_name());
288        let identifier = (lowercase_class, lowercase_method);
289        self.function_likes.get(&identifier)
290    }
291
292    /// Retrieves the declaring method metadata, following the inheritance chain.
293    /// This finds where the method is actually implemented.
294    #[inline]
295    pub fn get_declaring_method(&self, class: &str, method: &str) -> Option<&FunctionLikeMetadata> {
296        let method_id = MethodIdentifier::new(atom(class), atom(method));
297        let declaring_method_id = self.get_declaring_method_identifier(&method_id);
298        self.get_method(declaring_method_id.get_class_name(), declaring_method_id.get_method_name())
299    }
300
301    /// Retrieves metadata for any function-like construct (function, method, or closure).
302    /// This is a convenience method that delegates to the appropriate getter based on the identifier type.
303    #[inline]
304    pub fn get_function_like(
305        &self,
306        identifier: &crate::identifier::function_like::FunctionLikeIdentifier,
307    ) -> Option<&FunctionLikeMetadata> {
308        use crate::identifier::function_like::FunctionLikeIdentifier;
309        match identifier {
310            FunctionLikeIdentifier::Function(name) => self.get_function(name),
311            FunctionLikeIdentifier::Method(class, method) => self.get_method(class, method),
312            FunctionLikeIdentifier::Closure(file_id, position) => self.get_closure(file_id, position),
313        }
314    }
315    // Metadata Retrieval - Constants
316
317    /// Retrieves metadata for a global constant.
318    /// Namespace lookup is case-insensitive, constant name is case-sensitive.
319    #[inline]
320    pub fn get_constant(&self, name: &str) -> Option<&ConstantMetadata> {
321        let lowercase_name = ascii_lowercase_constant_name_atom(name);
322        self.constants.get(&lowercase_name)
323    }
324
325    /// Retrieves metadata for a class constant.
326    /// Class name is case-insensitive, constant name is case-sensitive.
327    #[inline]
328    pub fn get_class_constant(&self, class: &str, constant: &str) -> Option<&ClassLikeConstantMetadata> {
329        let lowercase_class = ascii_lowercase_atom(class);
330        let constant_name = atom(constant);
331        self.class_likes.get(&lowercase_class).and_then(|meta| meta.constants.get(&constant_name))
332    }
333
334    /// Retrieves metadata for an enum case.
335    #[inline]
336    pub fn get_enum_case(&self, class: &str, case: &str) -> Option<&EnumCaseMetadata> {
337        let lowercase_class = ascii_lowercase_atom(class);
338        let case_name = atom(case);
339        self.class_likes.get(&lowercase_class).and_then(|meta| meta.enum_cases.get(&case_name))
340    }
341    // Metadata Retrieval - Properties
342
343    /// Retrieves metadata for a property directly from the class where it's declared.
344    /// Class name is case-insensitive, property name is case-sensitive.
345    #[inline]
346    pub fn get_property(&self, class: &str, property: &str) -> Option<&PropertyMetadata> {
347        let lowercase_class = ascii_lowercase_atom(class);
348        let property_name = atom(property);
349        self.class_likes.get(&lowercase_class)?.properties.get(&property_name)
350    }
351
352    /// Retrieves the property metadata, potentially from a parent class if inherited.
353    #[inline]
354    pub fn get_declaring_property(&self, class: &str, property: &str) -> Option<&PropertyMetadata> {
355        let lowercase_class = ascii_lowercase_atom(class);
356        let property_name = atom(property);
357        let declaring_class = self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name)?;
358        self.class_likes.get(declaring_class)?.properties.get(&property_name)
359    }
360    // Type Resolution
361
362    /// Gets the type of a property, resolving it from the declaring class if needed.
363    #[inline]
364    pub fn get_property_type(&self, class: &str, property: &str) -> Option<&TUnion> {
365        let lowercase_class = ascii_lowercase_atom(class);
366        let property_name = atom(property);
367        let declaring_class = self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name)?;
368        let property_meta = self.class_likes.get(declaring_class)?.properties.get(&property_name)?;
369        property_meta.type_metadata.as_ref().map(|tm| &tm.type_union)
370    }
371
372    /// Gets the type of a class constant, considering both type hints and inferred types.
373    pub fn get_class_constant_type<'a>(&'a self, class: &str, constant: &str) -> Option<Cow<'a, TUnion>> {
374        let lowercase_class = ascii_lowercase_atom(class);
375        let constant_name = atom(constant);
376        let class_meta = self.class_likes.get(&lowercase_class)?;
377
378        // Check if it's an enum case
379        if class_meta.kind.is_enum() && class_meta.enum_cases.contains_key(&constant_name) {
380            let atomic = TAtomic::Object(TObject::new_enum_case(class_meta.original_name, constant_name));
381            return Some(Cow::Owned(TUnion::from_atomic(atomic)));
382        }
383
384        // It's a regular class constant
385        let constant_meta = class_meta.constants.get(&constant_name)?;
386
387        // Prefer the type signature if available
388        if let Some(type_meta) = constant_meta.type_metadata.as_ref() {
389            return Some(Cow::Borrowed(&type_meta.type_union));
390        }
391
392        // Fall back to inferred type
393        constant_meta.inferred_type.as_ref().map(|atomic| Cow::Owned(TUnion::from_atomic(atomic.clone())))
394    }
395
396    /// Gets the literal value of a class constant if it was inferred.
397    #[inline]
398    pub fn get_class_constant_literal_value(&self, class: &str, constant: &str) -> Option<&TAtomic> {
399        let lowercase_class = ascii_lowercase_atom(class);
400        let constant_name = atom(constant);
401        self.class_likes
402            .get(&lowercase_class)
403            .and_then(|meta| meta.constants.get(&constant_name))
404            .and_then(|constant_meta| constant_meta.inferred_type.as_ref())
405    }
406    // Inheritance Queries
407
408    /// Checks if a child class extends a parent class (case-insensitive).
409    #[inline]
410    pub fn class_extends(&self, child: &str, parent: &str) -> bool {
411        let lowercase_child = ascii_lowercase_atom(child);
412        let lowercase_parent = ascii_lowercase_atom(parent);
413        self.class_likes.get(&lowercase_child).is_some_and(|meta| meta.all_parent_classes.contains(&lowercase_parent))
414    }
415
416    /// Checks if a class directly extends a parent class (case-insensitive).
417    #[inline]
418    pub fn class_directly_extends(&self, child: &str, parent: &str) -> bool {
419        let lowercase_child = ascii_lowercase_atom(child);
420        let lowercase_parent = ascii_lowercase_atom(parent);
421        self.class_likes
422            .get(&lowercase_child)
423            .is_some_and(|meta| meta.direct_parent_class.as_ref() == Some(&lowercase_parent))
424    }
425
426    /// Checks if a class implements an interface (case-insensitive).
427    #[inline]
428    pub fn class_implements(&self, class: &str, interface: &str) -> bool {
429        let lowercase_class = ascii_lowercase_atom(class);
430        let lowercase_interface = ascii_lowercase_atom(interface);
431        self.class_likes
432            .get(&lowercase_class)
433            .is_some_and(|meta| meta.all_parent_interfaces.contains(&lowercase_interface))
434    }
435
436    /// Checks if a class directly implements an interface (case-insensitive).
437    #[inline]
438    pub fn class_directly_implements(&self, class: &str, interface: &str) -> bool {
439        let lowercase_class = ascii_lowercase_atom(class);
440        let lowercase_interface = ascii_lowercase_atom(interface);
441        self.class_likes
442            .get(&lowercase_class)
443            .is_some_and(|meta| meta.direct_parent_interfaces.contains(&lowercase_interface))
444    }
445
446    /// Checks if a class uses a trait (case-insensitive).
447    #[inline]
448    pub fn class_uses_trait(&self, class: &str, trait_name: &str) -> bool {
449        let lowercase_class = ascii_lowercase_atom(class);
450        let lowercase_trait = ascii_lowercase_atom(trait_name);
451        self.class_likes.get(&lowercase_class).is_some_and(|meta| meta.used_traits.contains(&lowercase_trait))
452    }
453
454    /// Checks if child is an instance of parent (via extends or implements).
455    #[inline]
456    pub fn is_instance_of(&self, child: &str, parent: &str) -> bool {
457        if child == parent {
458            return true;
459        }
460
461        let lowercase_child = ascii_lowercase_atom(child);
462        let lowercase_parent = ascii_lowercase_atom(parent);
463
464        if lowercase_child == lowercase_parent {
465            return true;
466        }
467
468        self.class_likes.get(&lowercase_child).is_some_and(|meta| {
469            meta.all_parent_classes.contains(&lowercase_parent)
470                || meta.all_parent_interfaces.contains(&lowercase_parent)
471        })
472    }
473
474    /// Checks if the given name is an enum or final class.
475    #[inline]
476    pub fn is_enum_or_final_class(&self, name: &str) -> bool {
477        let lowercase_name = ascii_lowercase_atom(name);
478        self.class_likes.get(&lowercase_name).is_some_and(|meta| meta.kind.is_enum() || meta.flags.is_final())
479    }
480
481    /// Checks if a class-like can be part of an intersection.
482    /// Generally, only final classes and enums cannot be intersected.
483    #[inline]
484    pub fn is_inheritable(&self, name: &str) -> bool {
485        let lowercase_name = ascii_lowercase_atom(name);
486        match self.symbols.get_kind(&lowercase_name) {
487            Some(SymbolKind::Class) => self.class_likes.get(&lowercase_name).is_some_and(|meta| !meta.flags.is_final()),
488            Some(SymbolKind::Enum) => false,
489            Some(SymbolKind::Interface) | Some(SymbolKind::Trait) | None => true,
490        }
491    }
492
493    /// Gets all descendants of a class (recursive).
494    #[inline]
495    pub fn get_class_descendants(&self, class: &str) -> AtomSet {
496        let lowercase_class = ascii_lowercase_atom(class);
497        let mut all_descendants = AtomSet::default();
498        let mut queue = vec![&lowercase_class];
499        let mut visited = AtomSet::default();
500        visited.insert(lowercase_class);
501
502        while let Some(current_name) = queue.pop() {
503            if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
504                for descendant in direct_descendants {
505                    if visited.insert(*descendant) {
506                        all_descendants.insert(*descendant);
507                        queue.push(descendant);
508                    }
509                }
510            }
511        }
512
513        all_descendants
514    }
515
516    /// Gets all ancestors of a class (parents + interfaces).
517    #[inline]
518    pub fn get_class_ancestors(&self, class: &str) -> AtomSet {
519        let lowercase_class = ascii_lowercase_atom(class);
520        let mut ancestors = AtomSet::default();
521        if let Some(meta) = self.class_likes.get(&lowercase_class) {
522            ancestors.extend(meta.all_parent_classes.iter().copied());
523            ancestors.extend(meta.all_parent_interfaces.iter().copied());
524        }
525        ancestors
526    }
527    // Method Resolution
528
529    /// Gets the class where a method is declared (following inheritance).
530    #[inline]
531    pub fn get_declaring_method_class(&self, class: &str, method: &str) -> Option<Atom> {
532        let lowercase_class = ascii_lowercase_atom(class);
533        let lowercase_method = ascii_lowercase_atom(method);
534        self.class_likes
535            .get(&lowercase_class)?
536            .declaring_method_ids
537            .get(&lowercase_method)
538            .map(|method_id| *method_id.get_class_name())
539    }
540
541    /// Gets the class where a method appears (could be the declaring class or child class).
542    #[inline]
543    pub fn get_appearing_method_class(&self, class: &str, method: &str) -> Option<Atom> {
544        let lowercase_class = ascii_lowercase_atom(class);
545        let lowercase_method = ascii_lowercase_atom(method);
546        self.class_likes
547            .get(&lowercase_class)?
548            .appearing_method_ids
549            .get(&lowercase_method)
550            .map(|method_id| *method_id.get_class_name())
551    }
552
553    /// Gets the declaring method identifier for a method.
554    pub fn get_declaring_method_identifier(&self, method_id: &MethodIdentifier) -> MethodIdentifier {
555        let lowercase_class = ascii_lowercase_atom(method_id.get_class_name());
556        let lowercase_method = ascii_lowercase_atom(method_id.get_method_name());
557
558        let Some(class_meta) = self.class_likes.get(&lowercase_class) else {
559            return *method_id;
560        };
561
562        if let Some(declaring_method_id) = class_meta.declaring_method_ids.get(&lowercase_method) {
563            return *declaring_method_id;
564        }
565
566        if class_meta.flags.is_abstract()
567            && let Some(overridden_map) = class_meta.overridden_method_ids.get(&lowercase_method)
568            && let Some((_, first_method_id)) = overridden_map.iter().next()
569        {
570            return *first_method_id;
571        }
572
573        *method_id
574    }
575
576    /// Checks if a method is overriding a parent method.
577    #[inline]
578    pub fn method_is_overriding(&self, class: &str, method: &str) -> bool {
579        let lowercase_class = ascii_lowercase_atom(class);
580        let lowercase_method = ascii_lowercase_atom(method);
581        self.class_likes
582            .get(&lowercase_class)
583            .is_some_and(|meta| meta.overridden_method_ids.contains_key(&lowercase_method))
584    }
585
586    /// Checks if a method is abstract.
587    #[inline]
588    pub fn method_is_abstract(&self, class: &str, method: &str) -> bool {
589        let lowercase_class = ascii_lowercase_atom(class);
590        let lowercase_method = ascii_lowercase_atom(method);
591        let identifier = (lowercase_class, lowercase_method);
592        self.function_likes
593            .get(&identifier)
594            .and_then(|meta| meta.method_metadata.as_ref())
595            .is_some_and(|method_meta| method_meta.is_abstract)
596    }
597
598    /// Checks if a method is static.
599    #[inline]
600    pub fn method_is_static(&self, class: &str, method: &str) -> bool {
601        let lowercase_class = ascii_lowercase_atom(class);
602        let lowercase_method = ascii_lowercase_atom(method);
603        let identifier = (lowercase_class, lowercase_method);
604        self.function_likes
605            .get(&identifier)
606            .and_then(|meta| meta.method_metadata.as_ref())
607            .is_some_and(|method_meta| method_meta.is_static)
608    }
609
610    /// Checks if a method is final.
611    #[inline]
612    pub fn method_is_final(&self, class: &str, method: &str) -> bool {
613        let lowercase_class = ascii_lowercase_atom(class);
614        let lowercase_method = ascii_lowercase_atom(method);
615        let identifier = (lowercase_class, lowercase_method);
616        self.function_likes
617            .get(&identifier)
618            .and_then(|meta| meta.method_metadata.as_ref())
619            .is_some_and(|method_meta| method_meta.is_final)
620    }
621
622    /// Gets the effective visibility of a method, taking into account trait alias visibility overrides.
623    ///
624    /// When a trait method is aliased with a visibility modifier (e.g., `use Trait { method as public aliasedMethod; }`),
625    /// the visibility is stored in the class's `trait_visibility_map`. This method checks that map first,
626    /// then falls back to the method's declared visibility.
627    #[inline]
628    pub fn get_method_visibility(&self, class: &str, method: &str) -> Option<crate::visibility::Visibility> {
629        let lowercase_class = ascii_lowercase_atom(class);
630        let lowercase_method = ascii_lowercase_atom(method);
631
632        // First check if there's a trait visibility override for this method
633        if let Some(class_meta) = self.class_likes.get(&lowercase_class)
634            && let Some(overridden_visibility) = class_meta.trait_visibility_map.get(&lowercase_method)
635        {
636            return Some(*overridden_visibility);
637        }
638
639        // Fall back to the method's declared visibility
640        let identifier = (lowercase_class, lowercase_method);
641
642        self.function_likes
643            .get(&identifier)
644            .and_then(|meta| meta.method_metadata.as_ref())
645            .map(|method_meta| method_meta.visibility)
646    }
647
648    /// Gets thrown types for a function-like, including inherited throws.
649    pub fn get_function_like_thrown_types<'a>(
650        &'a self,
651        class_like: Option<&'a ClassLikeMetadata>,
652        function_like: &'a FunctionLikeMetadata,
653    ) -> &'a [TypeMetadata] {
654        if !function_like.thrown_types.is_empty() {
655            return function_like.thrown_types.as_slice();
656        }
657
658        if !function_like.kind.is_method() {
659            return &[];
660        }
661
662        let Some(class_like) = class_like else {
663            return &[];
664        };
665
666        let Some(method_name) = function_like.name.as_ref() else {
667            return &[];
668        };
669
670        if let Some(overridden_map) = class_like.overridden_method_ids.get(method_name) {
671            for (parent_class_name, parent_method_id) in overridden_map {
672                let Some(parent_class) = self.class_likes.get(parent_class_name) else {
673                    continue;
674                };
675
676                let parent_method_key = (*parent_method_id.get_class_name(), *parent_method_id.get_method_name());
677                if let Some(parent_method) = self.function_likes.get(&parent_method_key) {
678                    let thrown = self.get_function_like_thrown_types(Some(parent_class), parent_method);
679                    if !thrown.is_empty() {
680                        return thrown;
681                    }
682                }
683            }
684        }
685
686        &[]
687    }
688    // Property Resolution
689
690    /// Gets the class where a property is declared.
691    #[inline]
692    pub fn get_declaring_property_class(&self, class: &str, property: &str) -> Option<Atom> {
693        let lowercase_class = ascii_lowercase_atom(class);
694        let property_name = atom(property);
695        self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name).copied()
696    }
697
698    /// Gets the class where a property appears.
699    #[inline]
700    pub fn get_appearing_property_class(&self, class: &str, property: &str) -> Option<Atom> {
701        let lowercase_class = ascii_lowercase_atom(class);
702        let property_name = atom(property);
703        self.class_likes.get(&lowercase_class)?.appearing_property_ids.get(&property_name).copied()
704    }
705
706    /// Gets all descendants of a class (recursive).
707    pub fn get_all_descendants(&self, class: &str) -> AtomSet {
708        let lowercase_class = ascii_lowercase_atom(class);
709        let mut all_descendants = AtomSet::default();
710        let mut queue = vec![&lowercase_class];
711        let mut visited = AtomSet::default();
712        visited.insert(lowercase_class);
713
714        while let Some(current_name) = queue.pop() {
715            if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
716                for descendant in direct_descendants {
717                    if visited.insert(*descendant) {
718                        all_descendants.insert(*descendant);
719                        queue.push(descendant);
720                    }
721                }
722            }
723        }
724
725        all_descendants
726    }
727
728    /// Generates a unique name for an anonymous class based on its span.
729    pub fn get_anonymous_class_name(span: mago_span::Span) -> Atom {
730        use std::io::Write;
731
732        let mut buffer = [0u8; 64];
733        let mut writer = &mut buffer[..];
734
735        unsafe {
736            write!(writer, "class@anonymous:{}-{}:{}", span.file_id, span.start.offset, span.end.offset)
737                .unwrap_unchecked()
738        };
739
740        let written_len = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
741
742        atom(unsafe { std::str::from_utf8(&buffer[..written_len]).unwrap_unchecked() })
743    }
744
745    /// Retrieves the metadata for an anonymous class based on its span.
746    pub fn get_anonymous_class(&self, span: mago_span::Span) -> Option<&ClassLikeMetadata> {
747        let name = Self::get_anonymous_class_name(span);
748        if self.class_exists(&name) { self.class_likes.get(&name) } else { None }
749    }
750    // Utility Methods
751
752    /// Merges information from another `CodebaseMetadata` into this one.
753    pub fn extend(&mut self, other: CodebaseMetadata) {
754        for (k, v) in other.aliases {
755            self.aliases.entry(k).or_insert(v);
756        }
757
758        for (k, v) in other.class_likes {
759            let metadata_to_keep = match self.class_likes.entry(k) {
760                Entry::Occupied(entry) => {
761                    let existing = entry.remove();
762                    if v.flags.is_user_defined() {
763                        v
764                    } else if existing.flags.is_user_defined() {
765                        existing
766                    } else if v.flags.is_built_in() {
767                        v
768                    } else if existing.flags.is_built_in() {
769                        existing
770                    } else {
771                        v
772                    }
773                }
774                Entry::Vacant(_) => v,
775            };
776            self.class_likes.insert(k, metadata_to_keep);
777        }
778
779        for (k, v) in other.function_likes {
780            let metadata_to_keep = match self.function_likes.entry(k) {
781                Entry::Occupied(entry) => {
782                    let existing = entry.remove();
783                    if v.flags.is_user_defined() {
784                        v
785                    } else if existing.flags.is_user_defined() {
786                        existing
787                    } else if v.flags.is_built_in() {
788                        v
789                    } else if existing.flags.is_built_in() {
790                        existing
791                    } else {
792                        v
793                    }
794                }
795                Entry::Vacant(_) => v,
796            };
797            self.function_likes.insert(k, metadata_to_keep);
798        }
799
800        for (k, v) in other.constants {
801            let metadata_to_keep = match self.constants.entry(k) {
802                Entry::Occupied(entry) => {
803                    let existing = entry.remove();
804                    if v.flags.is_user_defined() {
805                        v
806                    } else if existing.flags.is_user_defined() {
807                        existing
808                    } else if v.flags.is_built_in() {
809                        v
810                    } else if existing.flags.is_built_in() {
811                        existing
812                    } else {
813                        v
814                    }
815                }
816                Entry::Vacant(_) => v,
817            };
818            self.constants.insert(k, metadata_to_keep);
819        }
820
821        self.symbols.extend(other.symbols);
822
823        for (k, v) in other.all_class_like_descendants {
824            self.all_class_like_descendants.entry(k).or_default().extend(v);
825        }
826
827        for (k, v) in other.direct_classlike_descendants {
828            self.direct_classlike_descendants.entry(k).or_default().extend(v);
829        }
830
831        self.safe_symbols.extend(other.safe_symbols);
832        self.safe_symbol_members.extend(other.safe_symbol_members);
833        self.infer_types_from_usage |= other.infer_types_from_usage;
834    }
835
836    /// Takes all issues from the codebase metadata.
837    pub fn take_issues(&mut self, user_defined: bool) -> IssueCollection {
838        let mut issues = IssueCollection::new();
839
840        for meta in self.class_likes.values_mut() {
841            if user_defined && !meta.flags.is_user_defined() {
842                continue;
843            }
844            issues.extend(meta.take_issues());
845        }
846
847        for meta in self.function_likes.values_mut() {
848            if user_defined && !meta.flags.is_user_defined() {
849                continue;
850            }
851            issues.extend(meta.take_issues());
852        }
853
854        for meta in self.constants.values_mut() {
855            if user_defined && !meta.flags.is_user_defined() {
856                continue;
857            }
858            issues.extend(meta.take_issues());
859        }
860
861        issues
862    }
863}
864
865impl Default for CodebaseMetadata {
866    #[inline]
867    fn default() -> Self {
868        Self {
869            class_likes: AtomMap::default(),
870            aliases: AtomMap::default(),
871            function_likes: HashMap::default(),
872            symbols: Symbols::new(),
873            infer_types_from_usage: false,
874            constants: AtomMap::default(),
875            all_class_like_descendants: AtomMap::default(),
876            direct_classlike_descendants: AtomMap::default(),
877            safe_symbols: AtomSet::default(),
878            safe_symbol_members: HashSet::default(),
879        }
880    }
881}