Skip to main content

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