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