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    /// Returns `Some(global_scope_invalid)` on success, where `global_scope_invalid`
912    /// is `true` when global-scope code (the `(empty, empty)` pseudo-symbol) references
913    /// something that changed. Returns `None` if the cascade was too large to compute.
914    pub fn mark_safe_symbols(&mut self, diff: &CodebaseDiff, references: &SymbolReferences) -> Option<bool> {
915        let (invalid_symbols, partially_invalid) = references.get_invalid_symbols(diff)?;
916
917        // Mark all symbols in 'keep' set as safe (unless invalidated by cascade)
918        for keep_symbol in diff.get_keep() {
919            if !invalid_symbols.contains(keep_symbol) {
920                if keep_symbol.1.is_empty() {
921                    // Top-level symbol (class, function, constant)
922                    if !partially_invalid.contains(&keep_symbol.0) {
923                        self.safe_symbols.insert(keep_symbol.0);
924                    }
925                } else {
926                    // Member (method, property, class constant)
927                    self.safe_symbol_members.insert(*keep_symbol);
928                }
929            }
930        }
931
932        Some(invalid_symbols.contains(&(empty_atom(), empty_atom())))
933    }
934
935    /// Merges information from another `CodebaseMetadata` into this one.
936    ///
937    /// When both metadata have the same priority, the one with the smaller span is kept
938    /// for deterministic results regardless of scan order.
939    pub fn extend(&mut self, other: CodebaseMetadata) {
940        for (k, v) in other.class_likes {
941            match self.class_likes.entry(k) {
942                Entry::Occupied(mut entry) => {
943                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
944                        entry.insert(v);
945                    }
946                }
947                Entry::Vacant(entry) => {
948                    entry.insert(v);
949                }
950            }
951        }
952
953        for (k, v) in other.function_likes {
954            match self.function_likes.entry(k) {
955                Entry::Occupied(mut entry) => {
956                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
957                        entry.insert(v);
958                    }
959                }
960                Entry::Vacant(entry) => {
961                    entry.insert(v);
962                }
963            }
964        }
965
966        for (k, v) in other.constants {
967            match self.constants.entry(k) {
968                Entry::Occupied(mut entry) => {
969                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
970                        entry.insert(v);
971                    }
972                }
973                Entry::Vacant(entry) => {
974                    entry.insert(v);
975                }
976            }
977        }
978
979        self.symbols.extend(other.symbols);
980
981        for (k, v) in other.all_class_like_descendants {
982            self.all_class_like_descendants.entry(k).or_default().extend(v);
983        }
984
985        for (k, v) in other.direct_classlike_descendants {
986            self.direct_classlike_descendants.entry(k).or_default().extend(v);
987        }
988
989        self.file_signatures.extend(other.file_signatures);
990        self.safe_symbols.extend(other.safe_symbols);
991        self.safe_symbol_members.extend(other.safe_symbol_members);
992        self.infer_types_from_usage |= other.infer_types_from_usage;
993    }
994
995    /// Extends this codebase with another by reference, cloning only individual entries.
996    ///
997    /// This is more efficient than `extend(other.clone())` because it avoids allocating
998    /// a full clone of the source metadata's outer HashMap/AtomMap structures. Only
999    /// individual entries that need insertion are cloned.
1000    pub fn extend_ref(&mut self, other: &CodebaseMetadata) {
1001        for (k, v) in &other.class_likes {
1002            match self.class_likes.entry(*k) {
1003                Entry::Occupied(mut entry) => {
1004                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1005                        entry.insert(v.clone());
1006                    }
1007                }
1008                Entry::Vacant(entry) => {
1009                    entry.insert(v.clone());
1010                }
1011            }
1012        }
1013
1014        for (k, v) in &other.function_likes {
1015            match self.function_likes.entry(*k) {
1016                Entry::Occupied(mut entry) => {
1017                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1018                        entry.insert(v.clone());
1019                    }
1020                }
1021                Entry::Vacant(entry) => {
1022                    entry.insert(v.clone());
1023                }
1024            }
1025        }
1026
1027        for (k, v) in &other.constants {
1028            match self.constants.entry(*k) {
1029                Entry::Occupied(mut entry) => {
1030                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1031                        entry.insert(v.clone());
1032                    }
1033                }
1034                Entry::Vacant(entry) => {
1035                    entry.insert(v.clone());
1036                }
1037            }
1038        }
1039
1040        self.symbols.extend_ref(&other.symbols);
1041
1042        for (k, v) in &other.all_class_like_descendants {
1043            self.all_class_like_descendants.entry(*k).or_default().extend(v.iter().copied());
1044        }
1045
1046        for (k, v) in &other.direct_classlike_descendants {
1047            self.direct_classlike_descendants.entry(*k).or_default().extend(v.iter().copied());
1048        }
1049
1050        for (k, v) in &other.file_signatures {
1051            self.file_signatures.insert(*k, v.clone());
1052        }
1053        self.safe_symbols.extend(other.safe_symbols.iter().copied());
1054        self.safe_symbol_members.extend(other.safe_symbol_members.iter().cloned());
1055        self.infer_types_from_usage |= other.infer_types_from_usage;
1056    }
1057
1058    /// Removes all entries that were contributed by the given per-file scan metadata.
1059    ///
1060    /// This is the inverse of [`extend_ref()`]: it removes class_likes, function_likes,
1061    /// constants, symbols, and file_signatures whose keys match those in `file_metadata`.
1062    ///
1063    /// Used by the incremental engine to patch the codebase in-place when files change,
1064    /// avoiding a full rebuild from base + all files.
1065    ///
1066    /// Note: This does NOT remove descendant map entries — those are rebuilt from scratch
1067    /// by `populate_codebase()` on every run.
1068    pub fn remove_entries_of(&mut self, file_metadata: &CodebaseMetadata) {
1069        for k in file_metadata.class_likes.keys() {
1070            self.class_likes.remove(k);
1071        }
1072
1073        for k in file_metadata.function_likes.keys() {
1074            self.function_likes.remove(k);
1075        }
1076
1077        for k in file_metadata.constants.keys() {
1078            self.constants.remove(k);
1079        }
1080
1081        // Remove symbols that were contributed by this file.
1082        // We can only remove class-like symbols (not namespaces, since they may be shared).
1083        for k in file_metadata.class_likes.keys() {
1084            self.symbols.remove(*k);
1085        }
1086
1087        for k in file_metadata.file_signatures.keys() {
1088            self.file_signatures.remove(k);
1089        }
1090    }
1091
1092    /// Extracts the set of keys from this metadata for use with [`remove_entries_by_keys()`].
1093    ///
1094    /// This is much cheaper than keeping a full `CodebaseMetadata` clone — it only stores
1095    /// the keys needed to undo an `extend_ref()` operation.
1096    pub fn extract_keys(&self) -> CodebaseEntryKeys {
1097        CodebaseEntryKeys {
1098            class_like_names: self.class_likes.keys().copied().collect(),
1099            function_like_keys: self.function_likes.keys().copied().collect(),
1100            constant_names: self.constants.keys().copied().collect(),
1101            file_ids: self.file_signatures.keys().copied().collect(),
1102        }
1103    }
1104
1105    /// Extracts only the keys that this per-file metadata currently "owns" in the given
1106    /// merged codebase; i.e. keys whose span in `merged` matches this metadata's span.
1107    ///
1108    /// This is what you want for incremental fingerprints. [`extract_keys`](Self::extract_keys)
1109    /// captures *every* key the scan produced, including ones that lost the tiebreak in
1110    /// [`extend`](Self::extend) / [`extend_ref`](Self::extend_ref) when another file defined
1111    /// the same FQN. Using `extract_keys` as a removal fingerprint then causes a nasty
1112    /// cross-file bug: touching file *B* can remove an entry that file *A* actually owns,
1113    /// because [`remove_entries_by_keys`](Self::remove_entries_by_keys) deletes by FQN
1114    /// without checking who the current owner is. The analyzer then reports a spurious
1115    /// "duplicate definition" when it walks *A* and finds *B*'s span in the codebase.
1116    ///
1117    /// By only recording the keys whose spans still match *this* metadata, removing the
1118    /// fingerprint later becomes a safe no-op when another file won the merge. The
1119    /// removal only drops the entries this file genuinely put into the merged codebase.
1120    pub fn extract_owned_keys(&self, merged: &CodebaseMetadata) -> CodebaseEntryKeys {
1121        let class_like_names = self
1122            .class_likes
1123            .iter()
1124            .filter(|(name, meta)| merged.class_likes.get(*name).is_some_and(|m| m.span == meta.span))
1125            .map(|(name, _)| *name)
1126            .collect();
1127
1128        let function_like_keys = self
1129            .function_likes
1130            .iter()
1131            .filter(|(key, meta)| merged.function_likes.get(*key).is_some_and(|m| m.span == meta.span))
1132            .map(|(key, _)| *key)
1133            .collect();
1134
1135        let constant_names = self
1136            .constants
1137            .iter()
1138            .filter(|(name, meta)| merged.constants.get(*name).is_some_and(|m| m.span == meta.span))
1139            .map(|(name, _)| *name)
1140            .collect();
1141
1142        // A file signature is always owned by its file (there is at most one per file).
1143        let file_ids = self.file_signatures.keys().copied().collect();
1144
1145        CodebaseEntryKeys { class_like_names, function_like_keys, constant_names, file_ids }
1146    }
1147
1148    /// Removes entries whose keys match the given [`CodebaseEntryKeys`].
1149    ///
1150    /// This is the lightweight equivalent of [`remove_entries_of()`] — it performs the
1151    /// same removals but from a compact key set instead of a full `CodebaseMetadata` reference.
1152    pub fn remove_entries_by_keys(&mut self, keys: &CodebaseEntryKeys) {
1153        for k in &keys.class_like_names {
1154            self.class_likes.remove(k);
1155            self.symbols.remove(*k);
1156        }
1157
1158        for k in &keys.function_like_keys {
1159            self.function_likes.remove(k);
1160        }
1161
1162        for k in &keys.constant_names {
1163            self.constants.remove(k);
1164        }
1165
1166        for k in &keys.file_ids {
1167            self.file_signatures.remove(k);
1168        }
1169    }
1170
1171    /// Takes all issues from the codebase metadata.
1172    pub fn take_issues(&mut self, user_defined: bool) -> IssueCollection {
1173        let mut issues = IssueCollection::new();
1174
1175        for meta in self.class_likes.values_mut() {
1176            if user_defined && !meta.flags.is_user_defined() {
1177                continue;
1178            }
1179            issues.extend(meta.take_issues());
1180        }
1181
1182        for meta in self.function_likes.values_mut() {
1183            if user_defined && !meta.flags.is_user_defined() {
1184                continue;
1185            }
1186            issues.extend(meta.take_issues());
1187        }
1188
1189        for meta in self.constants.values_mut() {
1190            if user_defined && !meta.flags.is_user_defined() {
1191                continue;
1192            }
1193            issues.extend(meta.take_issues());
1194        }
1195
1196        issues
1197    }
1198
1199    /// Gets all file IDs that have signatures in this metadata.
1200    ///
1201    /// This is a helper method for incremental analysis to iterate over all files.
1202    #[must_use]
1203    pub fn get_all_file_ids(&self) -> Vec<FileId> {
1204        self.file_signatures.keys().copied().collect()
1205    }
1206}
1207
1208impl Default for CodebaseMetadata {
1209    #[inline]
1210    fn default() -> Self {
1211        Self {
1212            class_likes: AtomMap::default(),
1213            function_likes: HashMap::default(),
1214            symbols: Symbols::new(),
1215            infer_types_from_usage: false,
1216            constants: AtomMap::default(),
1217            all_class_like_descendants: AtomMap::default(),
1218            direct_classlike_descendants: AtomMap::default(),
1219            safe_symbols: AtomSet::default(),
1220            safe_symbol_members: HashSet::default(),
1221            file_signatures: HashMap::default(),
1222        }
1223    }
1224}
1225
1226/// Determines which metadata value to keep when merging duplicates.
1227///
1228/// Priority:
1229///   1. user-defined > built-in > other.
1230///   2. non-polyfill > polyfill — tools like rector/phpstan/psalm ship
1231///      skeleton stubs gated by `if (!class_exists('X'))` that should never
1232///      shadow a concrete definition.
1233///   3. smaller span wins as a deterministic tie-breaker.
1234///
1235/// Returns `true` if the new value should replace the existing one.
1236fn should_replace_metadata(
1237    existing_flags: MetadataFlags,
1238    existing_span: Span,
1239    new_flags: MetadataFlags,
1240    new_span: Span,
1241) -> bool {
1242    let new_is_user_defined = new_flags.is_user_defined();
1243    let existing_is_user_defined = existing_flags.is_user_defined();
1244
1245    if new_is_user_defined != existing_is_user_defined {
1246        return new_is_user_defined;
1247    }
1248
1249    let new_is_built_in = new_flags.is_built_in();
1250    let existing_is_built_in = existing_flags.is_built_in();
1251
1252    if new_is_built_in != existing_is_built_in {
1253        return new_is_built_in;
1254    }
1255
1256    let new_is_polyfill = new_flags.is_polyfill();
1257    let existing_is_polyfill = existing_flags.is_polyfill();
1258
1259    if new_is_polyfill != existing_is_polyfill {
1260        return !new_is_polyfill;
1261    }
1262
1263    new_span < existing_span
1264}
1265
1266#[cfg(test)]
1267mod should_replace_metadata_tests {
1268    use super::*;
1269
1270    #[test]
1271    fn non_polyfill_replaces_polyfill() {
1272        let polyfill = MetadataFlags::POLYFILL;
1273        let real = MetadataFlags::empty();
1274        assert!(should_replace_metadata(polyfill, Span::dummy(0, 100), real, Span::dummy(0, 100)));
1275        assert!(!should_replace_metadata(real, Span::dummy(0, 100), polyfill, Span::dummy(0, 100)));
1276    }
1277
1278    #[test]
1279    fn polyfill_does_not_replace_non_polyfill_even_with_smaller_span() {
1280        let real = MetadataFlags::empty();
1281        let polyfill = MetadataFlags::POLYFILL;
1282        assert!(!should_replace_metadata(real, Span::dummy(500, 600), polyfill, Span::dummy(0, 10)));
1283    }
1284
1285    #[test]
1286    fn user_defined_beats_polyfill_flag() {
1287        let polyfill_user = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1288        let plain = MetadataFlags::empty();
1289        assert!(!should_replace_metadata(polyfill_user, Span::dummy(0, 10), plain, Span::dummy(0, 10)));
1290        assert!(should_replace_metadata(plain, Span::dummy(0, 10), polyfill_user, Span::dummy(0, 10)));
1291    }
1292
1293    #[test]
1294    fn two_user_defined_fall_through_to_polyfill_check() {
1295        let a = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1296        let b = MetadataFlags::USER_DEFINED;
1297        assert!(should_replace_metadata(a, Span::dummy(0, 10), b, Span::dummy(0, 10)));
1298        assert!(!should_replace_metadata(b, Span::dummy(0, 10), a, Span::dummy(0, 10)));
1299    }
1300
1301    #[test]
1302    fn two_non_polyfills_fall_through_to_priority_rules() {
1303        let user = MetadataFlags::USER_DEFINED;
1304        let builtin = MetadataFlags::BUILTIN;
1305        assert!(!should_replace_metadata(user, Span::dummy(0, 10), builtin, Span::dummy(0, 10)));
1306        assert!(should_replace_metadata(builtin, Span::dummy(0, 10), user, Span::dummy(0, 10)));
1307    }
1308}