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;
6
7use mago_database::file::File;
8use mago_database::file::FileId;
9use mago_reporting::Annotation;
10use mago_reporting::Issue;
11use mago_reporting::IssueCollection;
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::word;
20
21use crate::diff::CodebaseDiff;
22use crate::identifier::method::MethodIdentifier;
23use crate::issue::ScanningIssueKind;
24use crate::metadata::class_like::ClassLikeMetadata;
25use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
26use crate::metadata::constant::ConstantMetadata;
27use crate::metadata::enum_case::EnumCaseMetadata;
28use crate::metadata::flags::MetadataFlags;
29use crate::metadata::function_like::FunctionLikeMetadata;
30use crate::metadata::property::PropertyMetadata;
31use crate::metadata::ttype::TypeMetadata;
32use crate::reference::SymbolReferences;
33use crate::signature::FileSignature;
34use crate::symbol::SymbolKind;
35use crate::symbol::Symbols;
36use crate::ttype::atomic::TAtomic;
37use crate::ttype::atomic::object::TObject;
38use crate::ttype::union::TUnion;
39use crate::visibility::Visibility;
40
41pub mod attribute;
42pub mod class_like;
43pub mod class_like_constant;
44pub mod constant;
45pub mod enum_case;
46pub mod flags;
47pub mod function_like;
48pub mod parameter;
49pub mod property;
50pub mod property_hook;
51pub mod ttype;
52pub mod version_constraint;
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<Word>,
63    /// Function-like `(scope, name)` tuples.
64    pub function_like_keys: Vec<(Word, Word)>,
65    /// Constant FQN atoms.
66    pub constant_names: Vec<Word>,
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, Debug, PartialEq, Default)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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    /// Per-patch class-like metadata, keyed by FQCN.
104    ///
105    /// Vendor and patch files declare symbols under the same FQCN, so patches cannot share
106    /// the `class_likes` map. At most one patch may target a given symbol; a second patch for
107    /// the same FQCN is diagnosed as a [`PatchDuplicateTarget`](ScanningIssueKind::PatchDuplicateTarget)
108    /// rather than silently overwriting the first. Entries here are folded into `class_likes`
109    /// by [`apply_patches_pass`](Self::apply_patches_pass).
110    pub patch_class_likes: WordMap<ClassLikeMetadata>,
111    /// Per-patch function-like metadata, keyed by `(scope, name)`.
112    ///
113    /// The key matches the existing `function_likes` key shape: the FQCN for methods,
114    /// `empty_word()` for free functions.
115    pub patch_function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
116    /// Per-patch constant metadata, keyed by FQN.
117    pub patch_constants: WordMap<ConstantMetadata>,
118}
119
120impl CodebaseMetadata {
121    /// Creates a new, empty `CodebaseMetadata` with default values.
122    #[inline]
123    #[must_use]
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Checks if a class exists in the codebase (case-insensitive).
129    ///
130    /// # Examples
131    /// ```ignore
132    /// if codebase.class_exists("MyClass") {
133    ///     // MyClass is a class
134    /// }
135    /// ```
136    #[inline]
137    #[must_use]
138    pub fn class_exists(&self, name: &[u8]) -> bool {
139        let lowercase_name = ascii_lowercase_word(name);
140        matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class))
141    }
142
143    /// Checks if an interface exists in the codebase (case-insensitive).
144    #[inline]
145    #[must_use]
146    pub fn interface_exists(&self, name: &[u8]) -> bool {
147        let lowercase_name = ascii_lowercase_word(name);
148        matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Interface))
149    }
150
151    /// Checks if a trait exists in the codebase (case-insensitive).
152    #[inline]
153    #[must_use]
154    pub fn trait_exists(&self, name: &[u8]) -> bool {
155        let lowercase_name = ascii_lowercase_word(name);
156        matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Trait))
157    }
158
159    /// Checks if an enum exists in the codebase (case-insensitive).
160    #[inline]
161    #[must_use]
162    pub fn enum_exists(&self, name: &[u8]) -> bool {
163        let lowercase_name = ascii_lowercase_word(name);
164        matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Enum))
165    }
166
167    /// Checks if a class-like (class, interface, trait, or enum) exists (case-insensitive).
168    #[inline]
169    #[must_use]
170    pub fn class_like_exists(&self, name: &[u8]) -> bool {
171        let lowercase_name = ascii_lowercase_word(name);
172        self.symbols.contains(lowercase_name)
173    }
174
175    /// Checks if a namespace exists (case-insensitive).
176    #[inline]
177    #[must_use]
178    pub fn namespace_exists(&self, name: &[u8]) -> bool {
179        let lowercase_name = ascii_lowercase_word(name);
180        self.symbols.contains_namespace(lowercase_name)
181    }
182
183    /// Checks if a class or trait exists in the codebase (case-insensitive).
184    #[inline]
185    #[must_use]
186    pub fn class_or_trait_exists(&self, name: &[u8]) -> bool {
187        let lowercase_name = ascii_lowercase_word(name);
188        matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class | SymbolKind::Trait))
189    }
190
191    /// Checks if a class or interface exists in the codebase (case-insensitive).
192    #[inline]
193    #[must_use]
194    pub fn class_or_interface_exists(&self, name: &[u8]) -> bool {
195        let lowercase_name = ascii_lowercase_word(name);
196        matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class | SymbolKind::Interface))
197    }
198
199    /// Checks if a method identifier exists in the codebase.
200    #[inline]
201    #[must_use]
202    pub fn method_identifier_exists(&self, method_id: &MethodIdentifier) -> bool {
203        let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
204        let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
205        let identifier = (lowercase_class, lowercase_method);
206        self.function_likes.contains_key(&identifier)
207    }
208
209    /// Checks if a global function exists in the codebase (case-insensitive).
210    #[inline]
211    #[must_use]
212    pub fn function_exists(&self, name: &[u8]) -> bool {
213        let lowercase_name = ascii_lowercase_word(name);
214        let identifier = (empty_word(), lowercase_name);
215        self.function_likes.contains_key(&identifier)
216    }
217
218    /// Checks if a global constant exists in the codebase.
219    /// The namespace part is case-insensitive, but the constant name is case-sensitive.
220    #[inline]
221    #[must_use]
222    pub fn constant_exists(&self, name: &[u8]) -> bool {
223        let lowercase_name = ascii_lowercase_constant_name_word(name);
224        self.constants.contains_key(&lowercase_name)
225    }
226
227    /// Checks if a method exists on a class-like, including inherited methods (case-insensitive).
228    #[inline]
229    #[must_use]
230    pub fn method_exists(&self, class: &[u8], method: &[u8]) -> bool {
231        let lowercase_class = ascii_lowercase_word(class);
232        let lowercase_method = ascii_lowercase_word(method);
233        self.class_likes
234            .get(&lowercase_class)
235            .is_some_and(|meta| meta.appearing_method_ids.contains_key(&lowercase_method))
236    }
237
238    /// Checks if a property exists on a class-like, including inherited properties.
239    /// Class name is case-insensitive, property name is case-sensitive.
240    /// Sees real declarations only; magic `@property*` tags are reachable through
241    /// `ClassLikeMetadata::magic_property_ids`.
242    #[inline]
243    #[must_use]
244    pub fn property_exists(&self, class: &[u8], property: &[u8]) -> bool {
245        let lowercase_class = ascii_lowercase_word(class);
246        let property_name = word(property);
247        self.class_likes
248            .get(&lowercase_class)
249            .is_some_and(|meta| meta.appearing_property_ids.contains_key(&property_name))
250    }
251
252    /// Checks if a magic `@property*` exists on a class-like, including inherited tags.
253    /// Class name is case-insensitive, property name is case-sensitive.
254    #[inline]
255    #[must_use]
256    pub fn magic_property_exists(&self, class: &[u8], property: &[u8]) -> bool {
257        let lowercase_class = ascii_lowercase_word(class);
258        let property_name = word(property);
259        self.class_likes.get(&lowercase_class).is_some_and(|meta| meta.magic_property_ids.contains_key(&property_name))
260    }
261
262    /// Checks if a class constant or enum case exists on a class-like.
263    /// Class name is case-insensitive, constant/case name is case-sensitive.
264    #[inline]
265    #[must_use]
266    pub fn class_constant_exists(&self, class: &[u8], constant: &[u8]) -> bool {
267        let lowercase_class = ascii_lowercase_word(class);
268        let constant_name = word(constant);
269        self.class_likes.get(&lowercase_class).is_some_and(|meta| {
270            meta.constants.contains_key(&constant_name) || meta.enum_cases.contains_key(&constant_name)
271        })
272    }
273
274    /// Retrieves metadata for a class (case-insensitive).
275    /// Returns `None` if the name doesn't correspond to a class.
276    #[inline]
277    #[must_use]
278    pub fn get_class(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
279        let lowercase_name = ascii_lowercase_word(name);
280        if matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class)) {
281            self.class_likes.get(&lowercase_name)
282        } else {
283            None
284        }
285    }
286
287    /// Retrieves metadata for an interface (case-insensitive).
288    #[inline]
289    #[must_use]
290    pub fn get_interface(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
291        let lowercase_name = ascii_lowercase_word(name);
292        if matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Interface)) {
293            self.class_likes.get(&lowercase_name)
294        } else {
295            None
296        }
297    }
298
299    /// Retrieves metadata for a trait (case-insensitive).
300    #[inline]
301    #[must_use]
302    pub fn get_trait(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
303        let lowercase_name = ascii_lowercase_word(name);
304        if matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Trait)) {
305            self.class_likes.get(&lowercase_name)
306        } else {
307            None
308        }
309    }
310
311    /// Retrieves metadata for an enum (case-insensitive).
312    #[inline]
313    #[must_use]
314    pub fn get_enum(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
315        let lowercase_name = ascii_lowercase_word(name);
316        if self.symbols.contains_enum(lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
317    }
318
319    /// Retrieves metadata for any class-like structure (case-insensitive).
320    #[inline]
321    #[must_use]
322    pub fn get_class_like(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
323        let lowercase_name = ascii_lowercase_word(name);
324        self.class_likes.get(&lowercase_name)
325    }
326
327    /// Retrieves metadata for a global function (case-insensitive).
328    #[inline]
329    #[must_use]
330    pub fn get_function(&self, name: &[u8]) -> Option<&FunctionLikeMetadata> {
331        let lowercase_name = ascii_lowercase_word(name);
332        let identifier = (empty_word(), lowercase_name);
333        self.function_likes.get(&identifier)
334    }
335
336    /// Retrieves metadata for a method (case-insensitive for both class and method names).
337    #[inline]
338    #[must_use]
339    pub fn get_method(&self, class: &[u8], method: &[u8]) -> Option<&FunctionLikeMetadata> {
340        let lowercase_class = ascii_lowercase_word(class);
341        let lowercase_method = ascii_lowercase_word(method);
342        let identifier = (lowercase_class, lowercase_method);
343        self.function_likes.get(&identifier)
344    }
345
346    /// Retrieves metadata for a closure or arrow function by its synthetic
347    /// name (e.g. `{closure:src/foo.php:12:5}`).
348    #[inline]
349    #[must_use]
350    pub fn get_closure(&self, synthetic_name: &Word) -> Option<&FunctionLikeMetadata> {
351        self.function_likes.get(&(empty_word(), *synthetic_name))
352    }
353
354    /// Retrieves metadata for a closure declared at the given file and span.
355    /// Convenience wrapper that rebuilds the synthetic name and delegates to
356    /// [`Self::get_closure`].
357    #[inline]
358    #[must_use]
359    pub fn get_closure_at(&self, file: &File, span: Span) -> Option<&FunctionLikeMetadata> {
360        let name = crate::build_synthetic_name("closure", file, span);
361        self.get_closure(&name)
362    }
363
364    /// Retrieves method metadata by `MethodIdentifier`.
365    #[inline]
366    #[must_use]
367    pub fn get_method_by_id(&self, method_id: &MethodIdentifier) -> Option<&FunctionLikeMetadata> {
368        let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
369        let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
370        let identifier = (lowercase_class, lowercase_method);
371        self.function_likes.get(&identifier)
372    }
373
374    /// Retrieves the declaring method metadata, following the inheritance chain.
375    /// This finds where the method is actually implemented.
376    #[inline]
377    #[must_use]
378    pub fn get_declaring_method(&self, class: &[u8], method: &[u8]) -> Option<&FunctionLikeMetadata> {
379        let method_id = MethodIdentifier::new(word(class), word(method));
380        let declaring_method_id = self.get_declaring_method_identifier(&method_id);
381        self.get_method(
382            declaring_method_id.get_class_name().as_bytes(),
383            declaring_method_id.get_method_name().as_bytes(),
384        )
385    }
386
387    /// Retrieves metadata for any function-like construct (function, method, or closure).
388    /// This is a convenience method that delegates to the appropriate getter based on the identifier type.
389    #[inline]
390    #[must_use]
391    pub fn get_function_like(
392        &self,
393        identifier: &crate::identifier::function_like::FunctionLikeIdentifier,
394    ) -> Option<&FunctionLikeMetadata> {
395        use crate::identifier::function_like::FunctionLikeIdentifier;
396        match identifier {
397            FunctionLikeIdentifier::Function(name) => self.get_function(name.as_bytes()),
398            FunctionLikeIdentifier::Method(class, method) => self.get_method(class.as_bytes(), method.as_bytes()),
399            FunctionLikeIdentifier::Closure(name) => self.get_closure(name),
400        }
401    }
402
403    /// Retrieves metadata for a global constant.
404    /// Namespace lookup is case-insensitive, constant name is case-sensitive.
405    #[inline]
406    #[must_use]
407    pub fn get_constant(&self, name: &[u8]) -> Option<&ConstantMetadata> {
408        let lowercase_name = ascii_lowercase_constant_name_word(name);
409        self.constants.get(&lowercase_name)
410    }
411
412    /// The declaration name span of a top-level symbol named `name`: a
413    /// class-like (class/interface/trait/enum), a function, or a constant.
414    ///
415    /// Prefers the symbol's name span over its full declaration span, so callers
416    /// land on the identifier rather than the whole declaration body. The lookup
417    /// is case-insensitive, like every symbol lookup here.
418    #[inline]
419    #[must_use]
420    pub fn span_of(&self, name: &[u8]) -> Option<Span> {
421        if let Some(meta) = self.get_class_like(name) {
422            return Some(meta.name_span.unwrap_or(meta.span));
423        }
424
425        if let Some(meta) = self.get_function(name) {
426            return Some(meta.name_span.unwrap_or(meta.span));
427        }
428
429        self.get_constant(name).map(|meta| meta.span)
430    }
431
432    /// Retrieves metadata for a class constant.
433    /// Class name is case-insensitive, constant name is case-sensitive.
434    #[inline]
435    #[must_use]
436    pub fn get_class_constant(&self, class: &[u8], constant: &[u8]) -> Option<&ClassLikeConstantMetadata> {
437        let lowercase_class = ascii_lowercase_word(class);
438        let constant_name = word(constant);
439        self.class_likes.get(&lowercase_class).and_then(|meta| meta.constants.get(&constant_name))
440    }
441
442    /// Retrieves metadata for an enum case.
443    #[inline]
444    #[must_use]
445    pub fn get_enum_case(&self, class: &[u8], case: &[u8]) -> Option<&EnumCaseMetadata> {
446        let lowercase_class = ascii_lowercase_word(class);
447        let case_name = word(case);
448        self.class_likes.get(&lowercase_class).and_then(|meta| meta.enum_cases.get(&case_name))
449    }
450
451    /// Retrieves metadata for a property directly from the class where it's declared.
452    /// Class name is case-insensitive, property name is case-sensitive.
453    /// Sees real declarations only; magic `@property*` tags are reachable through
454    /// `ClassLikeMetadata::magic_property_ids`.
455    #[inline]
456    #[must_use]
457    pub fn get_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
458        let lowercase_class = ascii_lowercase_word(class);
459        let property_name = word(property);
460        self.class_likes.get(&lowercase_class)?.properties.get(&property_name)
461    }
462
463    /// Retrieves magic `@property*` metadata declared directly on a class-like.
464    /// Class name is case-insensitive, property name is case-sensitive.
465    #[inline]
466    #[must_use]
467    pub fn get_magic_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
468        let lowercase_class = ascii_lowercase_word(class);
469        let property_name = word(property);
470        self.class_likes.get(&lowercase_class)?.magic_properties.get(&property_name)
471    }
472
473    /// Retrieves the property metadata, potentially from a parent class if inherited.
474    #[inline]
475    #[must_use]
476    pub fn get_declaring_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
477        let lowercase_class = ascii_lowercase_word(class);
478        let property_name = word(property);
479        let declaring_class = self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name)?;
480        self.class_likes.get(declaring_class)?.properties.get(&property_name)
481    }
482
483    /// Retrieves magic `@property*` metadata, potentially from an inherited tag.
484    /// Class name is case-insensitive, property name is case-sensitive.
485    #[inline]
486    #[must_use]
487    pub fn get_declaring_magic_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
488        let lowercase_class = ascii_lowercase_word(class);
489        let property_name = word(property);
490        let declaring_class = self.class_likes.get(&lowercase_class)?.magic_property_ids.get(&property_name)?;
491        self.class_likes.get(declaring_class)?.magic_properties.get(&property_name)
492    }
493    // Type Resolution
494
495    /// Gets the type of a property, resolving it from the declaring class if needed.
496    #[inline]
497    #[must_use]
498    pub fn get_property_type(&self, class: &[u8], property: &[u8]) -> Option<&TUnion> {
499        let lowercase_class = ascii_lowercase_word(class);
500        let property_name = word(property);
501        let declaring_class = self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name)?;
502        let property_meta = self.class_likes.get(declaring_class)?.properties.get(&property_name)?;
503        property_meta.type_metadata.as_ref().map(|tm| &tm.type_union)
504    }
505
506    /// Gets the type of a class constant, considering both type hints and inferred types.
507    #[must_use]
508    pub fn get_class_constant_type<'meta>(&'meta self, class: &[u8], constant: &[u8]) -> Option<Cow<'meta, TUnion>> {
509        let lowercase_class = ascii_lowercase_word(class);
510        let constant_name = word(constant);
511        let class_meta = self.class_likes.get(&lowercase_class)?;
512
513        // Check if it's an enum case
514        if class_meta.kind.is_enum() && class_meta.enum_cases.contains_key(&constant_name) {
515            let atomic = TAtomic::Object(TObject::new_enum_case(class_meta.original_name, constant_name));
516            return Some(Cow::Owned(TUnion::from_atomic(atomic)));
517        }
518
519        // It's a regular class constant
520        let constant_meta = class_meta.constants.get(&constant_name)?;
521
522        // Prefer the type signature if available
523        if let Some(type_meta) = constant_meta.type_metadata.as_ref() {
524            return Some(Cow::Borrowed(&type_meta.type_union));
525        }
526
527        // Fall back to inferred type
528        constant_meta.inferred_type.as_ref().map(|atomic| Cow::Owned(TUnion::from_atomic(atomic.clone())))
529    }
530    // Inheritance Queries
531
532    /// Checks if a child class extends a parent class (case-insensitive).
533    #[inline]
534    #[must_use]
535    pub fn class_extends(&self, child: &[u8], parent: &[u8]) -> bool {
536        let lowercase_child = ascii_lowercase_word(child);
537        let lowercase_parent = ascii_lowercase_word(parent);
538        self.class_likes.get(&lowercase_child).is_some_and(|meta| meta.all_parent_classes.contains(&lowercase_parent))
539    }
540
541    /// Checks if a class implements an interface (case-insensitive).
542    #[inline]
543    #[must_use]
544    pub fn class_implements(&self, class: &[u8], interface: &[u8]) -> bool {
545        let lowercase_class = ascii_lowercase_word(class);
546        let lowercase_interface = ascii_lowercase_word(interface);
547        self.class_likes
548            .get(&lowercase_class)
549            .is_some_and(|meta| meta.all_parent_interfaces.contains(&lowercase_interface))
550    }
551
552    /// Checks if a class uses a trait (case-insensitive).
553    #[inline]
554    #[must_use]
555    pub fn class_uses_trait(&self, class: &[u8], trait_name: &[u8]) -> bool {
556        let lowercase_class = ascii_lowercase_word(class);
557        let lowercase_trait = ascii_lowercase_word(trait_name);
558        self.class_likes.get(&lowercase_class).is_some_and(|meta| meta.used_traits.contains(&lowercase_trait))
559    }
560
561    /// Checks if child is an instance of parent (via extends or implements).
562    #[inline]
563    #[must_use]
564    pub fn is_instance_of(&self, child: &[u8], parent: &[u8]) -> bool {
565        if child == parent {
566            return true;
567        }
568
569        let lowercase_child = ascii_lowercase_word(child);
570        let lowercase_parent = ascii_lowercase_word(parent);
571
572        if lowercase_child == lowercase_parent {
573            return true;
574        }
575
576        self.class_likes.get(&lowercase_child).is_some_and(|meta| {
577            meta.all_parent_classes.contains(&lowercase_parent)
578                || meta.all_parent_interfaces.contains(&lowercase_parent)
579                || meta.used_traits.contains(&lowercase_parent)
580                || meta.require_extends.contains(&lowercase_parent)
581                || meta.require_implements.contains(&lowercase_parent)
582        })
583    }
584
585    /// Checks if the given name is an enum or final class.
586    #[inline]
587    #[must_use]
588    pub fn is_enum_or_final_class(&self, name: &[u8]) -> bool {
589        let lowercase_name = ascii_lowercase_word(name);
590        self.class_likes.get(&lowercase_name).is_some_and(|meta| meta.kind.is_enum() || meta.flags.is_final())
591    }
592
593    /// Checks if a class-like can be part of an intersection.
594    /// Generally, only final classes and enums cannot be intersected.
595    #[inline]
596    #[must_use]
597    pub fn is_inheritable(&self, name: &[u8]) -> bool {
598        let lowercase_name = ascii_lowercase_word(name);
599        match self.symbols.get_kind(lowercase_name) {
600            Some(SymbolKind::Class) => self.class_likes.get(&lowercase_name).is_some_and(|meta| !meta.flags.is_final()),
601            Some(SymbolKind::Enum) => false,
602            Some(SymbolKind::Interface | SymbolKind::Trait) | None => true,
603        }
604    }
605
606    /// Gets all descendants of a class (recursive).
607    #[inline]
608    #[must_use]
609    pub fn get_class_descendants(&self, class: &[u8]) -> WordSet {
610        let lowercase_class = ascii_lowercase_word(class);
611        let mut all_descendants = WordSet::default();
612        let mut queue = vec![&lowercase_class];
613        let mut visited = WordSet::default();
614        visited.insert(lowercase_class);
615
616        while let Some(current_name) = queue.pop() {
617            if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
618                for descendant in direct_descendants {
619                    if visited.insert(*descendant) {
620                        all_descendants.insert(*descendant);
621                        queue.push(descendant);
622                    }
623                }
624            }
625        }
626
627        all_descendants
628    }
629
630    /// Gets all ancestors of a class (parents + interfaces).
631    #[inline]
632    #[must_use]
633    pub fn get_class_ancestors(&self, class: &[u8]) -> WordSet {
634        let lowercase_class = ascii_lowercase_word(class);
635        let mut ancestors = WordSet::default();
636        if let Some(meta) = self.class_likes.get(&lowercase_class) {
637            ancestors.extend(meta.all_parent_classes.iter().copied());
638            ancestors.extend(meta.all_parent_interfaces.iter().copied());
639        }
640        ancestors
641    }
642
643    /// Gets the class where a method is declared (following inheritance).
644    #[inline]
645    #[must_use]
646    pub fn get_declaring_method_class(&self, class: &[u8], method: &[u8]) -> Option<Word> {
647        let lowercase_class = ascii_lowercase_word(class);
648        let lowercase_method = ascii_lowercase_word(method);
649
650        self.class_likes
651            .get(&lowercase_class)?
652            .declaring_method_ids
653            .get(&lowercase_method)
654            .map(|method_id| method_id.get_class_name())
655    }
656
657    /// Gets the declaring method identifier for a method.
658    #[must_use]
659    pub fn get_declaring_method_identifier(&self, method_id: &MethodIdentifier) -> MethodIdentifier {
660        let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
661        let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
662
663        let Some(class_meta) = self.class_likes.get(&lowercase_class) else {
664            return *method_id;
665        };
666
667        if let Some(declaring_method_id) = class_meta.declaring_method_ids.get(&lowercase_method) {
668            return *declaring_method_id;
669        }
670
671        if class_meta.flags.is_abstract()
672            && let Some(overridden_map) = class_meta.overridden_method_ids.get(&lowercase_method)
673            && let Some((_, first_method_id)) = overridden_map.first()
674        {
675            return *first_method_id;
676        }
677
678        *method_id
679    }
680
681    /// Checks if a method is overriding a parent method.
682    #[inline]
683    #[must_use]
684    pub fn method_is_overriding(&self, class: &[u8], method: &[u8]) -> bool {
685        let lowercase_class = ascii_lowercase_word(class);
686        let lowercase_method = ascii_lowercase_word(method);
687        self.class_likes
688            .get(&lowercase_class)
689            .is_some_and(|meta| meta.overridden_method_ids.contains_key(&lowercase_method))
690    }
691
692    /// Checks if a method is abstract.
693    #[inline]
694    #[must_use]
695    pub fn method_is_abstract(&self, class: &[u8], method: &[u8]) -> bool {
696        let lowercase_class = ascii_lowercase_word(class);
697        let lowercase_method = ascii_lowercase_word(method);
698        let identifier = (lowercase_class, lowercase_method);
699        self.function_likes
700            .get(&identifier)
701            .and_then(|meta| meta.method_metadata.as_ref())
702            .is_some_and(|method_meta| method_meta.is_abstract)
703    }
704
705    /// Checks if a method is final.
706    #[inline]
707    #[must_use]
708    pub fn method_is_final(&self, class: &[u8], method: &[u8]) -> bool {
709        let lowercase_class = ascii_lowercase_word(class);
710        let lowercase_method = ascii_lowercase_word(method);
711        let identifier = (lowercase_class, lowercase_method);
712        self.function_likes
713            .get(&identifier)
714            .and_then(|meta| meta.method_metadata.as_ref())
715            .is_some_and(|method_meta| method_meta.is_final)
716    }
717
718    /// Gets the effective visibility of a method, taking into account trait alias visibility overrides.
719    ///
720    /// When a trait method is aliased with a visibility modifier (e.g., `use Trait { method as public aliasedMethod; }`),
721    /// the visibility is stored in the class's `trait_visibility_map`. This method checks that map first,
722    /// then falls back to the method's declared visibility.
723    #[inline]
724    #[must_use]
725    pub fn get_method_visibility(&self, class: &[u8], method: &[u8]) -> Option<Visibility> {
726        let lowercase_class = ascii_lowercase_word(class);
727        let lowercase_method = ascii_lowercase_word(method);
728
729        // First check if there's a trait visibility override for this method
730        if let Some(class_meta) = self.class_likes.get(&lowercase_class)
731            && let Some(overridden_visibility) = class_meta.trait_visibility_map.get(&lowercase_method)
732        {
733            return Some(*overridden_visibility);
734        }
735
736        // Fall back to the method's declared visibility
737        let declaring_class = self.get_declaring_method_class(class, method)?;
738        let identifier = (declaring_class, lowercase_method);
739
740        self.function_likes
741            .get(&identifier)
742            .and_then(|meta| meta.method_metadata.as_ref())
743            .map(|method_meta| method_meta.visibility)
744    }
745
746    /// Gets thrown types for a function-like, including inherited throws.
747    #[must_use]
748    pub fn get_function_like_thrown_types<'meta>(
749        &'meta self,
750        class_like: Option<&'meta ClassLikeMetadata>,
751        function_like: &'meta FunctionLikeMetadata,
752    ) -> &'meta [TypeMetadata] {
753        if !function_like.thrown_types.is_empty() {
754            return function_like.thrown_types.as_slice();
755        }
756
757        if !function_like.kind.is_method() {
758            return &[];
759        }
760
761        let Some(class_like) = class_like else {
762            return &[];
763        };
764
765        let method_name = &function_like.name;
766
767        if let Some(overridden_map) = class_like.overridden_method_ids.get(method_name) {
768            for (parent_class_name, parent_method_id) in overridden_map {
769                if class_like.name.as_bytes().eq_ignore_ascii_case(parent_class_name.as_bytes()) {
770                    continue; // Skip self-recursion if the method overrides itself
771                }
772
773                let Some(parent_class) = self.class_likes.get(parent_class_name) else {
774                    continue;
775                };
776
777                let parent_method_key = (parent_method_id.get_class_name(), parent_method_id.get_method_name());
778                if let Some(parent_method) = self.function_likes.get(&parent_method_key) {
779                    let thrown = self.get_function_like_thrown_types(Some(parent_class), parent_method);
780                    if !thrown.is_empty() {
781                        return thrown;
782                    }
783                }
784            }
785        }
786
787        &[]
788    }
789
790    /// Gets the class where a property is declared.
791    /// Sees real declarations only; magic `@property*` tags are reachable through
792    /// `ClassLikeMetadata::magic_property_ids`.
793    #[inline]
794    #[must_use]
795    pub fn get_declaring_property_class(&self, class: &[u8], property: &[u8]) -> Option<Word> {
796        let lowercase_class = ascii_lowercase_word(class);
797        let property_name = word(property);
798        self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name).copied()
799    }
800
801    /// Gets all descendants of a class (recursive).
802    #[must_use]
803    pub fn get_all_descendants(&self, class: &[u8]) -> WordSet {
804        let lowercase_class = ascii_lowercase_word(class);
805        let mut all_descendants = WordSet::default();
806        let mut queue = vec![&lowercase_class];
807        let mut visited = WordSet::default();
808        visited.insert(lowercase_class);
809
810        while let Some(current_name) = queue.pop() {
811            if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
812                for descendant in direct_descendants {
813                    if visited.insert(*descendant) {
814                        all_descendants.insert(*descendant);
815                        queue.push(descendant);
816                    }
817                }
818            }
819        }
820
821        all_descendants
822    }
823
824    /// Generates the synthetic display name for an anonymous class based on
825    /// its declaring file and span. Delegates to [`crate::get_anonymous_class_name`].
826    #[must_use]
827    pub fn get_anonymous_class_name(file: &File, span: Span) -> Word {
828        crate::get_anonymous_class_name(file, span)
829    }
830
831    /// Retrieves the metadata for an anonymous class based on its declaring
832    /// file and span.
833    #[must_use]
834    pub fn get_anonymous_class(&self, file: &File, span: Span) -> Option<&ClassLikeMetadata> {
835        let name = Self::get_anonymous_class_name(file, span);
836        self.get_class_like(name.as_bytes())
837    }
838
839    /// Gets the file signature for a given file ID.
840    ///
841    /// # Arguments
842    ///
843    /// * `file_id` - The file identifier
844    ///
845    /// # Returns
846    ///
847    /// A reference to the `FileSignature` if it exists, or `None` if the file has no signature.
848    #[inline]
849    #[must_use]
850    pub fn get_file_signature(&self, file_id: &FileId) -> Option<&FileSignature> {
851        self.file_signatures.get(file_id)
852    }
853
854    /// Adds or updates a file signature for a given file ID.
855    ///
856    /// # Arguments
857    ///
858    /// * `file_id` - The file identifier
859    /// * `signature` - The file signature
860    ///
861    /// # Returns
862    ///
863    /// The previous `FileSignature` if it existed.
864    #[inline]
865    pub fn set_file_signature(&mut self, file_id: FileId, signature: FileSignature) -> Option<FileSignature> {
866        self.file_signatures.insert(file_id, signature)
867    }
868
869    /// Marks safe symbols based on diff and invalidation cascade.
870    ///
871    /// After this function runs, `self.safe_symbols` and `self.safe_symbol_members`
872    /// will contain all symbols that can be safely skipped during analysis.
873    ///
874    /// # Arguments
875    ///
876    /// * `diff` - The computed diff between old and new code
877    /// * `references` - Symbol reference graph from previous run
878    ///
879    /// # Returns
880    /// Returns the logical names of files whose top-level code references an invalidated
881    /// symbol. Returns `None` if the cascade was too large to compute.
882    pub fn mark_safe_symbols(&mut self, diff: &CodebaseDiff, references: &SymbolReferences) -> Option<WordSet> {
883        let (invalid_symbols, partially_invalid, invalid_files) = references.get_invalid_symbols(diff)?;
884
885        // Mark all symbols in 'keep' set as safe (unless invalidated by cascade)
886        for keep_symbol in diff.get_keep() {
887            if !invalid_symbols.contains(keep_symbol) {
888                if keep_symbol.1.is_empty() {
889                    // Top-level symbol (class, function, constant)
890                    if !partially_invalid.contains(&keep_symbol.0) {
891                        self.safe_symbols.insert(keep_symbol.0);
892                    }
893                } else {
894                    // Member (method, property, class constant)
895                    self.safe_symbol_members.insert(*keep_symbol);
896                }
897            }
898        }
899
900        Some(invalid_files)
901    }
902
903    /// Merges information from another `CodebaseMetadata` into this one.
904    ///
905    /// When both metadata have the same priority, the one with the smaller span is kept
906    /// for deterministic results regardless of scan order.
907    pub fn extend(&mut self, other: CodebaseMetadata) {
908        for (k, mut v) in other.class_likes {
909            match self.class_likes.entry(k) {
910                Entry::Occupied(mut entry) => {
911                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
912                        v.version_constraint.merge(entry.get().version_constraint.clone());
913                        entry.insert(v);
914                    } else {
915                        entry.get_mut().version_constraint.merge(v.version_constraint);
916                    }
917                }
918                Entry::Vacant(entry) => {
919                    entry.insert(v);
920                }
921            }
922        }
923
924        for (k, mut v) in other.function_likes {
925            match self.function_likes.entry(k) {
926                Entry::Occupied(mut entry) => {
927                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
928                        v.version_constraint.merge(entry.get().version_constraint.clone());
929                        entry.insert(v);
930                    } else {
931                        entry.get_mut().version_constraint.merge(v.version_constraint);
932                    }
933                }
934                Entry::Vacant(entry) => {
935                    entry.insert(v);
936                }
937            }
938        }
939
940        for (k, mut v) in other.constants {
941            match self.constants.entry(k) {
942                Entry::Occupied(mut entry) => {
943                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
944                        v.version_constraint.merge(entry.get().version_constraint.clone());
945                        entry.insert(v);
946                    } else {
947                        entry.get_mut().version_constraint.merge(v.version_constraint);
948                    }
949                }
950                Entry::Vacant(entry) => {
951                    entry.insert(v);
952                }
953            }
954        }
955
956        self.symbols.extend(other.symbols);
957
958        for (k, v) in other.all_class_like_descendants {
959            self.all_class_like_descendants.entry(k).or_default().extend(v);
960        }
961
962        for (k, v) in other.direct_classlike_descendants {
963            self.direct_classlike_descendants.entry(k).or_default().extend(v);
964        }
965
966        self.file_signatures.extend(other.file_signatures);
967        self.safe_symbols.extend(other.safe_symbols);
968        self.safe_symbol_members.extend(other.safe_symbol_members);
969        self.infer_types_from_usage |= other.infer_types_from_usage;
970        self.merge_patch_class_likes(other.patch_class_likes);
971        self.merge_patch_function_likes(other.patch_function_likes);
972        self.merge_patch_constants(other.patch_constants);
973    }
974
975    /// Extends this codebase with another by reference, cloning only individual entries.
976    ///
977    /// This is more efficient than `extend(other.clone())` because it avoids allocating
978    /// a full clone of the source metadata's outer HashMap/WordMap structures. Only
979    /// individual entries that need insertion are cloned.
980    pub fn extend_ref(&mut self, other: &CodebaseMetadata) {
981        for (k, v) in &other.class_likes {
982            match self.class_likes.entry(*k) {
983                Entry::Occupied(mut entry) => {
984                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
985                        let mut new = v.clone();
986                        new.version_constraint.merge(entry.get().version_constraint.clone());
987                        entry.insert(new);
988                    } else {
989                        entry.get_mut().version_constraint.merge(v.version_constraint.clone());
990                    }
991                }
992                Entry::Vacant(entry) => {
993                    entry.insert(v.clone());
994                }
995            }
996        }
997
998        for (k, v) in &other.function_likes {
999            match self.function_likes.entry(*k) {
1000                Entry::Occupied(mut entry) => {
1001                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1002                        let mut new = v.clone();
1003                        new.version_constraint.merge(entry.get().version_constraint.clone());
1004                        entry.insert(new);
1005                    } else {
1006                        entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1007                    }
1008                }
1009                Entry::Vacant(entry) => {
1010                    entry.insert(v.clone());
1011                }
1012            }
1013        }
1014
1015        for (k, v) in &other.constants {
1016            match self.constants.entry(*k) {
1017                Entry::Occupied(mut entry) => {
1018                    if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1019                        let mut new = v.clone();
1020                        new.version_constraint.merge(entry.get().version_constraint.clone());
1021                        entry.insert(new);
1022                    } else {
1023                        entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1024                    }
1025                }
1026                Entry::Vacant(entry) => {
1027                    entry.insert(v.clone());
1028                }
1029            }
1030        }
1031
1032        self.symbols.extend_ref(&other.symbols);
1033
1034        for (k, v) in &other.all_class_like_descendants {
1035            self.all_class_like_descendants.entry(*k).or_default().extend(v.iter().copied());
1036        }
1037
1038        for (k, v) in &other.direct_classlike_descendants {
1039            self.direct_classlike_descendants.entry(*k).or_default().extend(v.iter().copied());
1040        }
1041
1042        for (k, v) in &other.file_signatures {
1043            self.file_signatures.insert(*k, v.clone());
1044        }
1045        self.safe_symbols.extend(other.safe_symbols.iter().copied());
1046        self.safe_symbol_members.extend(other.safe_symbol_members.iter().copied());
1047        self.infer_types_from_usage |= other.infer_types_from_usage;
1048        self.merge_patch_class_likes(other.patch_class_likes.iter().map(|(k, v)| (*k, v.clone())));
1049        self.merge_patch_function_likes(other.patch_function_likes.iter().map(|(k, v)| (*k, v.clone())));
1050        self.merge_patch_constants(other.patch_constants.iter().map(|(k, v)| (*k, v.clone())));
1051    }
1052
1053    /// Merges patch class-likes from another codebase, diagnosing collisions.
1054    ///
1055    /// At most one patch may target a given symbol. When two patches collide, the first-merged
1056    /// entry is kept and a [`PatchDuplicateTarget`](ScanningIssueKind::PatchDuplicateTarget)
1057    /// diagnostic referencing both sites is attached to it, rather than letting one silently
1058    /// overwrite the other in hash-order.
1059    fn merge_patch_class_likes(&mut self, incoming: impl IntoIterator<Item = (Word, ClassLikeMetadata)>) {
1060        for (k, v) in incoming {
1061            match self.patch_class_likes.entry(k) {
1062                Entry::Occupied(mut entry) => {
1063                    let diagnostic = duplicate_patch_class_diagnostic(entry.get(), &v);
1064                    entry.get_mut().issues.push(diagnostic);
1065                }
1066                Entry::Vacant(entry) => {
1067                    entry.insert(v);
1068                }
1069            }
1070        }
1071    }
1072
1073    /// Merges patch function-likes from another codebase, diagnosing collisions on free
1074    /// functions. Method collisions are subsumed by the enclosing class's duplicate
1075    /// diagnostic, so only keys with an empty class component are reported here.
1076    fn merge_patch_function_likes(&mut self, incoming: impl IntoIterator<Item = ((Word, Word), FunctionLikeMetadata)>) {
1077        for (k, v) in incoming {
1078            match self.patch_function_likes.entry(k) {
1079                Entry::Occupied(mut entry) => {
1080                    if k.0.is_empty() {
1081                        let diagnostic = duplicate_patch_function_diagnostic(entry.get(), &v);
1082                        entry.get_mut().issues.push(diagnostic);
1083                    }
1084                }
1085                Entry::Vacant(entry) => {
1086                    entry.insert(v);
1087                }
1088            }
1089        }
1090    }
1091
1092    /// Merges patch constants from another codebase, diagnosing collisions.
1093    fn merge_patch_constants(&mut self, incoming: impl IntoIterator<Item = (Word, ConstantMetadata)>) {
1094        for (k, v) in incoming {
1095            match self.patch_constants.entry(k) {
1096                Entry::Occupied(mut entry) => {
1097                    let diagnostic = duplicate_patch_constant_diagnostic(entry.get(), &v);
1098                    entry.get_mut().issues.push(diagnostic);
1099                }
1100                Entry::Vacant(entry) => {
1101                    entry.insert(v);
1102                }
1103            }
1104        }
1105    }
1106
1107    /// Moves every scanned entry of this per-file partial into the patch maps.
1108    ///
1109    /// Called on a per-file partial right after `scan_program` when the file is a
1110    /// [`FileType::Patch`]. Symbols and descendants from a patch partial are dropped — the
1111    /// FQCN belongs to whichever non-patch source originally declared it (or it's an orphan
1112    /// which `apply_patches_pass` will diagnose later).
1113    pub fn convert_partial_to_patch(&mut self) {
1114        for (k, v) in std::mem::take(&mut self.class_likes) {
1115            self.patch_class_likes.insert(k, v);
1116        }
1117
1118        for (k, v) in std::mem::take(&mut self.function_likes) {
1119            self.patch_function_likes.insert(k, v);
1120        }
1121
1122        for (k, v) in std::mem::take(&mut self.constants) {
1123            self.patch_constants.insert(k, v);
1124        }
1125
1126        self.symbols = Symbols::new();
1127        self.all_class_like_descendants.clear();
1128        self.direct_classlike_descendants.clear();
1129    }
1130
1131    /// Folds every entry in the `patch_*` maps into the matching vendor / built-in entry,
1132    /// attaching validation diagnostics to the patch entry's `issues` list.
1133    ///
1134    /// At most one patch may target a given symbol, so each entry is applied directly to its
1135    /// target. A patch whose target is user-defined is inert (user definitions win); a patch
1136    /// with no matching target is diagnosed as an orphan.
1137    ///
1138    /// Must be called after all partials have been merged so the slots patches target are
1139    /// present.
1140    pub fn apply_patches_pass(&mut self) {
1141        // `(class, method)` slots where a patch overrides a method inherited from an ancestor.
1142        // No function-like exists at these keys yet, so the function loop below materializes
1143        // them from the patch's own scanned declaration rather than treating them as orphans.
1144        let mut inherited_overrides: HashSet<(Word, Word)> = HashSet::default();
1145
1146        let class_keys: Vec<Word> = self.patch_class_likes.keys().copied().collect();
1147        for fqcn in class_keys {
1148            let Some(target) = self.class_likes.get(&fqcn) else {
1149                if let Some(p) = self.patch_class_likes.get_mut(&fqcn) {
1150                    let diag = orphan_patch_class_diagnostic(p);
1151                    p.issues.push(diag);
1152                }
1153                continue;
1154            };
1155            // User-defined targets win; the patch entry is inert. Leave any scan-time issues
1156            // on it intact — they still belong to the patch source.
1157            if target.flags.is_user_defined() {
1158                continue;
1159            }
1160
1161            let mut working = target.clone();
1162            let inherited =
1163                collect_inherited_patch_methods(&working, &self.patch_class_likes[&fqcn], &self.class_likes);
1164            inherited_overrides.extend(inherited.iter().map(|method| (fqcn, *method)));
1165            if let Some(patch_entry) = self.patch_class_likes.get_mut(&fqcn) {
1166                working.apply_patch(patch_entry, &inherited);
1167            }
1168            self.class_likes.insert(fqcn, working);
1169        }
1170
1171        let func_keys: Vec<(Word, Word)> = self.patch_function_likes.keys().copied().collect();
1172        for key in func_keys {
1173            let Some(target) = self.function_likes.get(&key) else {
1174                if inherited_overrides.contains(&key) {
1175                    // The patch overrides a method inherited from an ancestor, so no slot exists
1176                    // at `(class, method)` yet. The patch file declares the method in full, so
1177                    // promote its scanned function-like as this class's own declaration; the
1178                    // class loop has already pointed the declaring/appearing ids at this slot.
1179                    if let Some(p) = self.patch_function_likes.get(&key) {
1180                        let materialized = p.clone();
1181                        self.function_likes.insert(key, materialized);
1182                    }
1183                    continue;
1184                }
1185                // Methods of an orphan patch class are covered by the class-level diagnostic;
1186                // only free functions need their own orphan diagnostic.
1187                if key.0.is_empty()
1188                    && let Some(p) = self.patch_function_likes.get_mut(&key)
1189                {
1190                    let diag = orphan_patch_function_diagnostic(p);
1191                    p.issues.push(diag);
1192                }
1193                continue;
1194            };
1195            if target.flags.is_user_defined() {
1196                continue;
1197            }
1198
1199            let mut working = target.clone();
1200            if let Some(patch_entry) = self.patch_function_likes.get_mut(&key) {
1201                working.apply_patch(patch_entry);
1202            }
1203            self.function_likes.insert(key, working);
1204        }
1205
1206        let const_keys: Vec<Word> = self.patch_constants.keys().copied().collect();
1207        for fqcn in const_keys {
1208            let Some(target) = self.constants.get(&fqcn) else {
1209                if let Some(p) = self.patch_constants.get_mut(&fqcn) {
1210                    let diag = orphan_patch_constant_diagnostic(p);
1211                    p.issues.push(diag);
1212                }
1213                continue;
1214            };
1215            if target.flags.is_user_defined() {
1216                continue;
1217            }
1218
1219            let mut working = target.clone();
1220            if let Some(patch_entry) = self.patch_constants.get(&fqcn) {
1221                working.apply_patch(patch_entry);
1222            }
1223            self.constants.insert(fqcn, working);
1224        }
1225    }
1226
1227    /// Extracts only the keys that this per-file metadata currently "owns" in the given
1228    /// merged codebase; i.e. keys whose span in `merged` matches this metadata's span.
1229    ///
1230    /// This is what you want for incremental fingerprints. [`extract_keys`](Self::extract_keys)
1231    /// captures *every* key the scan produced, including ones that lost the tiebreak in
1232    /// [`extend`](Self::extend) / [`extend_ref`](Self::extend_ref) when another file defined
1233    /// the same FQN. Using `extract_keys` as a removal fingerprint then causes a nasty
1234    /// cross-file bug: touching file *B* can remove an entry that file *A* actually owns,
1235    /// because [`remove_entries_by_keys`](Self::remove_entries_by_keys) deletes by FQN
1236    /// without checking who the current owner is. The analyzer then reports a spurious
1237    /// "duplicate definition" when it walks *A* and finds *B*'s span in the codebase.
1238    ///
1239    /// By only recording the keys whose spans still match *this* metadata, removing the
1240    /// fingerprint later becomes a safe no-op when another file won the merge. The
1241    /// removal only drops the entries this file genuinely put into the merged codebase.
1242    #[must_use]
1243    pub fn extract_owned_keys(&self, merged: &CodebaseMetadata) -> CodebaseEntryKeys {
1244        let class_like_names = self
1245            .class_likes
1246            .iter()
1247            .filter(|(name, meta)| merged.class_likes.get(*name).is_some_and(|m| m.span == meta.span))
1248            .map(|(name, _)| *name)
1249            .collect();
1250
1251        let function_like_keys = self
1252            .function_likes
1253            .iter()
1254            .filter(|(key, meta)| merged.function_likes.get(*key).is_some_and(|m| m.span == meta.span))
1255            .map(|(key, _)| *key)
1256            .collect();
1257
1258        let constant_names = self
1259            .constants
1260            .iter()
1261            .filter(|(name, meta)| merged.constants.get(*name).is_some_and(|m| m.span == meta.span))
1262            .map(|(name, _)| *name)
1263            .collect();
1264
1265        // A file signature is always owned by its file (there is at most one per file).
1266        let file_ids = self.file_signatures.keys().copied().collect();
1267
1268        CodebaseEntryKeys { class_like_names, function_like_keys, constant_names, file_ids }
1269    }
1270
1271    /// Removes entries whose keys match the given [`CodebaseEntryKeys`].
1272    ///
1273    /// This is the lightweight equivalent of [`remove_entries_of()`] — it performs the
1274    /// same removals but from a compact key set instead of a full `CodebaseMetadata` reference.
1275    pub fn remove_entries_by_keys(&mut self, keys: &CodebaseEntryKeys) {
1276        for k in &keys.class_like_names {
1277            self.class_likes.remove(k);
1278            self.symbols.remove(*k);
1279        }
1280
1281        for k in &keys.function_like_keys {
1282            self.function_likes.remove(k);
1283        }
1284
1285        for k in &keys.constant_names {
1286            self.constants.remove(k);
1287        }
1288
1289        for k in &keys.file_ids {
1290            self.file_signatures.remove(k);
1291        }
1292
1293        // Drop any patch entry that originated from a file signature we just removed; a patch
1294        // entry's originating file is recorded on its span.
1295        let removed_files: HashSet<FileId> = keys.file_ids.iter().copied().collect();
1296        self.patch_class_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1297        self.patch_function_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1298        self.patch_constants.retain(|_, m| !removed_files.contains(&m.span.file_id));
1299    }
1300
1301    /// Takes all issues from the codebase metadata.
1302    pub fn take_issues(&mut self, user_defined: bool) -> IssueCollection {
1303        let mut issues = IssueCollection::new();
1304
1305        for meta in self.class_likes.values_mut() {
1306            if user_defined && !meta.flags.is_user_defined() {
1307                continue;
1308            }
1309            issues.extend(meta.take_issues());
1310        }
1311
1312        for meta in self.function_likes.values_mut() {
1313            if user_defined && !meta.flags.is_user_defined() {
1314                continue;
1315            }
1316            issues.extend(meta.take_issues());
1317        }
1318
1319        for meta in self.constants.values_mut() {
1320            if user_defined && !meta.flags.is_user_defined() {
1321                continue;
1322            }
1323            issues.extend(meta.take_issues());
1324        }
1325
1326        // Patches are user-authored, so their issues are always reported regardless of the
1327        // `user_defined` filter. They live in their own maps and never appear in the regular
1328        // class_likes/function_likes/constants iteration above.
1329        for meta in self.patch_class_likes.values_mut() {
1330            issues.extend(meta.take_issues());
1331        }
1332
1333        for meta in self.patch_function_likes.values_mut() {
1334            issues.extend(meta.take_issues());
1335        }
1336
1337        for meta in self.patch_constants.values_mut() {
1338            issues.extend(meta.take_issues());
1339        }
1340
1341        issues
1342    }
1343
1344    /// Gets all file IDs that have signatures in this metadata.
1345    ///
1346    /// This is a helper method for incremental analysis to iterate over all files.
1347    #[must_use]
1348    pub fn get_all_file_ids(&self) -> Vec<FileId> {
1349        self.file_signatures.keys().copied().collect()
1350    }
1351}
1352
1353/// Returns the subset of methods declared by `patch` that are inherited by `target` from
1354/// an ancestor but not declared on `target` itself. Used by `apply_patch` on class-like
1355/// metadata to distinguish patch-declared overrides of inherited methods (allowed) from
1356/// patch-introduced new methods (disallowed).
1357fn collect_inherited_patch_methods(
1358    target: &ClassLikeMetadata,
1359    patch: &ClassLikeMetadata,
1360    class_likes: &WordMap<ClassLikeMetadata>,
1361) -> WordSet {
1362    if patch.methods.is_empty() {
1363        return WordSet::default();
1364    }
1365    let ancestor_methods = class_like::collect_ancestor_methods(target, class_likes);
1366    patch.methods.iter().filter(|m| ancestor_methods.contains(*m)).copied().collect()
1367}
1368
1369fn duplicate_patch_class_diagnostic(kept: &ClassLikeMetadata, dropped: &ClassLikeMetadata) -> Issue {
1370    Issue::error(format!(
1371        "Multiple patches target `{}`; at most one patch may target a given symbol.",
1372        kept.original_name
1373    ))
1374    .with_code(ScanningIssueKind::PatchDuplicateTarget)
1375    .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this symbol."))
1376    .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1377    .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1378}
1379
1380fn duplicate_patch_function_diagnostic(kept: &FunctionLikeMetadata, dropped: &FunctionLikeMetadata) -> Issue {
1381    Issue::error(format!(
1382        "Multiple patches target function `{}`; at most one patch may target a given symbol.",
1383        kept.name
1384    ))
1385    .with_code(ScanningIssueKind::PatchDuplicateTarget)
1386    .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this function."))
1387    .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1388    .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1389}
1390
1391fn duplicate_patch_constant_diagnostic(kept: &ConstantMetadata, dropped: &ConstantMetadata) -> Issue {
1392    Issue::error(format!(
1393        "Multiple patches target constant `{}`; at most one patch may target a given symbol.",
1394        kept.name
1395    ))
1396    .with_code(ScanningIssueKind::PatchDuplicateTarget)
1397    .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this constant."))
1398    .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1399    .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1400}
1401
1402fn orphan_patch_class_diagnostic(meta: &ClassLikeMetadata) -> Issue {
1403    Issue::error(format!(
1404        "Patch declares `{}` but no vendored or built-in definition exists to patch.",
1405        meta.original_name,
1406    ))
1407    .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1408    .with_annotation(Annotation::primary(meta.span))
1409    .with_help(
1410        "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1411         check the symbol name and verify the patch still matches the upstream source.",
1412    )
1413}
1414
1415fn orphan_patch_function_diagnostic(meta: &FunctionLikeMetadata) -> Issue {
1416    Issue::error(format!(
1417        "Patch declares function `{}` but no vendored or built-in definition exists to patch.",
1418        meta.name,
1419    ))
1420    .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1421    .with_annotation(Annotation::primary(meta.span))
1422    .with_help(
1423        "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1424         check the function name and verify the patch still matches the upstream source.",
1425    )
1426}
1427
1428fn orphan_patch_constant_diagnostic(meta: &ConstantMetadata) -> Issue {
1429    Issue::error(format!(
1430        "Patch declares constant `{}` but no vendored or built-in definition exists to patch.",
1431        meta.name,
1432    ))
1433    .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1434    .with_annotation(Annotation::primary(meta.span))
1435    .with_help(
1436        "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1437         check the constant name and verify the patch still matches the upstream source.",
1438    )
1439}
1440
1441/// Determines which metadata value to keep when merging duplicates.
1442///
1443/// Priority:
1444///   1. user-defined > patch > external > built-in > other.
1445///   2. non-polyfill > polyfill — tools like rector/phpstan/psalm ship
1446///      skeleton stubs gated by `if (!class_exists('X'))` that should never
1447///      shadow a concrete definition.
1448///   3. smaller span wins as a deterministic tie-breaker.
1449///
1450/// Returns `true` if the new value should replace the existing one.
1451fn should_replace_metadata(
1452    existing_flags: MetadataFlags,
1453    existing_span: Span,
1454    new_flags: MetadataFlags,
1455    new_span: Span,
1456) -> bool {
1457    let new_is_user_defined = new_flags.is_user_defined();
1458    let existing_is_user_defined = existing_flags.is_user_defined();
1459
1460    if new_is_user_defined != existing_is_user_defined {
1461        return new_is_user_defined;
1462    }
1463
1464    let new_is_patch = new_flags.is_patch();
1465    let existing_is_patch = existing_flags.is_patch();
1466
1467    if new_is_patch != existing_is_patch {
1468        return new_is_patch;
1469    }
1470
1471    let new_is_external = new_flags.is_external();
1472    let existing_is_external = existing_flags.is_external();
1473
1474    if new_is_external != existing_is_external {
1475        return new_is_external;
1476    }
1477
1478    let new_is_built_in = new_flags.is_built_in();
1479    let existing_is_built_in = existing_flags.is_built_in();
1480
1481    if new_is_built_in != existing_is_built_in {
1482        return new_is_built_in;
1483    }
1484
1485    let new_is_polyfill = new_flags.is_polyfill();
1486    let existing_is_polyfill = existing_flags.is_polyfill();
1487
1488    if new_is_polyfill != existing_is_polyfill {
1489        return !new_is_polyfill;
1490    }
1491
1492    new_span < existing_span
1493}
1494
1495#[cfg(test)]
1496mod should_replace_metadata_tests {
1497    use super::*;
1498
1499    #[test]
1500    fn non_polyfill_replaces_polyfill() {
1501        let polyfill = MetadataFlags::POLYFILL;
1502        let real = MetadataFlags::empty();
1503        assert!(should_replace_metadata(polyfill, Span::dummy(0, 100), real, Span::dummy(0, 100)));
1504        assert!(!should_replace_metadata(real, Span::dummy(0, 100), polyfill, Span::dummy(0, 100)));
1505    }
1506
1507    #[test]
1508    fn polyfill_does_not_replace_non_polyfill_even_with_smaller_span() {
1509        let real = MetadataFlags::empty();
1510        let polyfill = MetadataFlags::POLYFILL;
1511        assert!(!should_replace_metadata(real, Span::dummy(500, 600), polyfill, Span::dummy(0, 10)));
1512    }
1513
1514    #[test]
1515    fn user_defined_beats_polyfill_flag() {
1516        let polyfill_user = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1517        let plain = MetadataFlags::empty();
1518        assert!(!should_replace_metadata(polyfill_user, Span::dummy(0, 10), plain, Span::dummy(0, 10)));
1519        assert!(should_replace_metadata(plain, Span::dummy(0, 10), polyfill_user, Span::dummy(0, 10)));
1520    }
1521
1522    #[test]
1523    fn two_user_defined_fall_through_to_polyfill_check() {
1524        let a = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1525        let b = MetadataFlags::USER_DEFINED;
1526        assert!(should_replace_metadata(a, Span::dummy(0, 10), b, Span::dummy(0, 10)));
1527        assert!(!should_replace_metadata(b, Span::dummy(0, 10), a, Span::dummy(0, 10)));
1528    }
1529
1530    #[test]
1531    fn two_non_polyfills_fall_through_to_priority_rules() {
1532        let user = MetadataFlags::USER_DEFINED;
1533        let builtin = MetadataFlags::BUILTIN;
1534        assert!(!should_replace_metadata(user, Span::dummy(0, 10), builtin, Span::dummy(0, 10)));
1535        assert!(should_replace_metadata(builtin, Span::dummy(0, 10), user, Span::dummy(0, 10)));
1536    }
1537
1538    #[test]
1539    fn patch_beats_vendored() {
1540        let vendored = MetadataFlags::empty();
1541        let patch = MetadataFlags::PATCH;
1542        assert!(should_replace_metadata(vendored, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1543        assert!(!should_replace_metadata(patch, Span::dummy(0, 100), vendored, Span::dummy(0, 100)));
1544    }
1545
1546    #[test]
1547    fn patch_beats_builtin() {
1548        let builtin = MetadataFlags::BUILTIN;
1549        let patch = MetadataFlags::PATCH;
1550        assert!(should_replace_metadata(builtin, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1551        assert!(!should_replace_metadata(patch, Span::dummy(0, 100), builtin, Span::dummy(0, 100)));
1552    }
1553
1554    #[test]
1555    fn external_beats_builtin_and_vendored() {
1556        let external = MetadataFlags::EXTERNAL;
1557        let builtin = MetadataFlags::BUILTIN;
1558        let vendored = MetadataFlags::empty();
1559
1560        assert!(should_replace_metadata(builtin, Span::dummy(0, 100), external, Span::dummy(500, 600)));
1561        assert!(!should_replace_metadata(external, Span::dummy(500, 600), builtin, Span::dummy(0, 100)));
1562        assert!(should_replace_metadata(vendored, Span::dummy(0, 100), external, Span::dummy(500, 600)));
1563        assert!(!should_replace_metadata(external, Span::dummy(500, 600), vendored, Span::dummy(0, 100)));
1564    }
1565
1566    #[test]
1567    fn patch_and_user_defined_beat_external() {
1568        let external = MetadataFlags::EXTERNAL;
1569        let patch = MetadataFlags::PATCH;
1570        let user = MetadataFlags::USER_DEFINED;
1571
1572        assert!(should_replace_metadata(external, Span::dummy(0, 100), patch, Span::dummy(500, 600)));
1573        assert!(!should_replace_metadata(patch, Span::dummy(500, 600), external, Span::dummy(0, 100)));
1574        assert!(should_replace_metadata(external, Span::dummy(0, 100), user, Span::dummy(500, 600)));
1575        assert!(!should_replace_metadata(user, Span::dummy(500, 600), external, Span::dummy(0, 100)));
1576    }
1577
1578    #[test]
1579    fn user_defined_beats_patch() {
1580        let user = MetadataFlags::USER_DEFINED;
1581        let patch = MetadataFlags::PATCH;
1582        assert!(!should_replace_metadata(user, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1583        assert!(should_replace_metadata(patch, Span::dummy(0, 100), user, Span::dummy(0, 100)));
1584    }
1585
1586    #[test]
1587    fn patch_does_not_beat_user_defined_even_with_smaller_span() {
1588        let user = MetadataFlags::USER_DEFINED;
1589        let patch = MetadataFlags::PATCH;
1590        assert!(!should_replace_metadata(user, Span::dummy(500, 600), patch, Span::dummy(0, 10)));
1591    }
1592
1593    #[test]
1594    #[allow(clippy::expect_used)]
1595    fn patch_function_like_leaves_vendor_owning_slot() {
1596        // Patches may only refine type information on an existing function-like; they must
1597        // never become the slot owner. The non-patch source's span/file id stay put so that
1598        // `extract_owned_keys` records vendor-as-owner — otherwise the patch's entry would
1599        // outlive a vendor deletion in incremental mode (orphan function-like bug).
1600        use crate::metadata::function_like::FunctionLikeKind;
1601
1602        let name = word("foo");
1603        let key = (empty_word(), name);
1604        let vendor_span = Span::dummy(0, 100);
1605        let patch_span = Span::dummy(500, 600);
1606
1607        let vendor =
1608            FunctionLikeMetadata::new(FunctionLikeKind::Function, name, name, vendor_span, MetadataFlags::empty());
1609        let mut codebase = CodebaseMetadata::new();
1610        codebase.function_likes.insert(key, vendor);
1611
1612        let patch = FunctionLikeMetadata::new(FunctionLikeKind::Function, name, name, patch_span, MetadataFlags::PATCH);
1613        codebase.patch_function_likes.insert(key, patch);
1614
1615        codebase.apply_patches_pass();
1616
1617        let merged = codebase.function_likes.get(&key).expect("function-like must remain after patch");
1618        assert_eq!(merged.span, vendor_span, "patch must not move the slot's span");
1619        assert!(!merged.flags.is_patch(), "patch must not flip the slot's flags");
1620    }
1621
1622    #[test]
1623    fn patch_does_not_apply_to_user_defined_class() {
1624        let class_name = word("MyClass");
1625        let method_existing = word("doIt");
1626
1627        let mut user_class =
1628            ClassLikeMetadata::new(class_name, class_name, Span::dummy(0, 100), None, MetadataFlags::USER_DEFINED);
1629        user_class.methods.insert(method_existing);
1630
1631        let mut codebase = CodebaseMetadata::new();
1632        codebase.class_likes.insert(class_name, user_class);
1633
1634        let mut patch_class =
1635            ClassLikeMetadata::new(class_name, class_name, Span::dummy(0, 50), None, MetadataFlags::PATCH);
1636        let method_new = word("patchedMethod");
1637        patch_class.methods.insert(method_new);
1638
1639        codebase.patch_class_likes.insert(class_name, patch_class);
1640
1641        codebase.apply_patches_pass();
1642
1643        let class = &codebase.class_likes[&class_name];
1644        // Patch must not apply to a user-defined class.
1645        assert!(!class.methods.contains(&method_new));
1646        // User-defined class must be preserved intact.
1647        assert!(class.methods.contains(&method_existing));
1648        assert!(class.flags.is_user_defined());
1649        // No issues should be emitted.
1650        assert!(class.issues.is_empty());
1651    }
1652}