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