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