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