Skip to main content

mago_codex/metadata/
mod.rs

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