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