Skip to main content

mago_codex/metadata/
mod.rs

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