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