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