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#[derive(Debug, Clone)]
60pub struct CodebaseEntryKeys {
61 pub class_like_names: Vec<Word>,
63 pub class_like_aliases: Vec<(Word, Word, Span)>,
64 pub function_like_keys: Vec<(Word, Word)>,
66 pub constant_names: Vec<Word>,
68 pub file_ids: Vec<FileId>,
70}
71
72#[derive(Clone, Debug, PartialEq, Default)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79#[non_exhaustive]
80#[allow(clippy::unsafe_derive_deserialize)]
81pub struct CodebaseMetadata {
82 pub infer_types_from_usage: bool,
84 pub class_likes: WordMap<ClassLikeMetadata>,
86 #[cfg_attr(feature = "serde", serde(default))]
87 pub class_like_aliases: WordMap<Word>,
88 #[cfg_attr(feature = "serde", serde(default))]
89 class_like_alias_declarations: WordMap<(Word, Span, MetadataFlags)>,
90 #[cfg_attr(feature = "serde", serde(skip, default))]
91 class_like_aliases_dirty: bool,
92 pub function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
95 pub symbols: Symbols,
97 pub constants: WordMap<ConstantMetadata>,
99 pub all_class_like_descendants: WordMap<WordSet>,
101 pub direct_classlike_descendants: WordMap<WordSet>,
103 pub safe_symbols: WordSet,
105 pub safe_symbol_members: HashSet<(Word, Word)>,
107 pub file_signatures: HashMap<FileId, FileSignature>,
110 pub patch_class_likes: WordMap<ClassLikeMetadata>,
118 pub patch_function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
123 pub patch_constants: WordMap<ConstantMetadata>,
125}
126
127impl CodebaseMetadata {
128 #[inline]
130 #[must_use]
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub(crate) fn add_class_like_alias(&mut self, alias: Word, target: Word, span: Span, flags: MetadataFlags) {
136 let replace =
137 self.class_like_alias_declarations.get(&alias).is_none_or(|(_, existing_span, existing_flags)| {
138 should_replace_metadata(*existing_flags, *existing_span, flags, span)
139 });
140
141 if replace {
142 self.class_like_alias_declarations.insert(alias, (target, span, flags));
143 self.class_like_aliases_dirty = true;
144 }
145 }
146
147 #[inline]
148 pub fn class_like_alias_declarations(&self) -> impl Iterator<Item = (Word, Word, Span)> + '_ {
149 self.class_like_alias_declarations.iter().map(|(alias, (target, span, _))| (*alias, *target, *span))
150 }
151
152 pub(crate) fn populate_class_like_aliases(&mut self) -> bool {
153 if !self.class_like_aliases_dirty {
154 return false;
155 }
156
157 let previous_aliases = std::mem::take(&mut self.class_like_aliases);
158 for alias in previous_aliases.keys().copied() {
159 if let Some(metadata) = self.class_likes.get(&alias) {
160 self.symbols.add_symbol_name(alias, metadata.kind);
161 } else {
162 self.symbols.remove(alias);
163 }
164 }
165
166 for metadata in self.class_likes.values_mut() {
167 metadata.aliases.clear();
168 }
169
170 let mut aliases = WordMap::default();
171 for alias in self.class_like_alias_declarations.keys().copied() {
172 if self.class_likes.contains_key(&alias) {
173 continue;
174 }
175
176 if let Some(actual) = self.resolve_class_like_alias_declaration(alias) {
177 aliases.insert(alias, actual);
178 }
179 }
180
181 let aliases_changed = aliases != previous_aliases;
182 self.class_like_aliases = aliases;
183 let mut aliased_classes = WordSet::default();
184 for (alias, actual) in &self.class_like_aliases {
185 let Some(metadata) = self.class_likes.get_mut(actual) else {
186 continue;
187 };
188
189 metadata.aliases.push(*alias);
190 aliased_classes.insert(*actual);
191 self.symbols.add_symbol_name(*alias, metadata.kind);
192 }
193
194 for actual in aliased_classes {
195 if let Some(metadata) = self.class_likes.get_mut(&actual) {
196 metadata.aliases.sort_unstable();
197 }
198 }
199
200 self.class_like_aliases_dirty = false;
201 aliases_changed
202 }
203
204 fn resolve_class_like_alias_declaration(&self, alias: Word) -> Option<Word> {
205 let mut current = self.class_like_alias_declarations.get(&alias)?.0;
206
207 for _ in 0..self.class_like_alias_declarations.len() {
208 if self.class_likes.contains_key(¤t) {
209 return Some(current);
210 }
211
212 current = self.class_like_alias_declarations.get(¤t)?.0;
213 }
214
215 None
216 }
217
218 #[inline]
219 pub(crate) fn resolve_class_like_word(&self, name: Word) -> Option<Word> {
220 if self.class_likes.contains_key(&name) {
221 return Some(name);
222 }
223
224 if self.class_like_aliases.is_empty() {
225 return None;
226 }
227
228 self.class_like_aliases.get(&name).copied()
229 }
230
231 #[inline]
232 pub(crate) fn get_class_like_by_word(&self, name: Word) -> Option<&ClassLikeMetadata> {
233 if let Some(metadata) = self.class_likes.get(&name) {
234 return Some(metadata);
235 }
236
237 if self.class_like_aliases.is_empty() {
238 return None;
239 }
240
241 self.class_like_aliases.get(&name).and_then(|actual| self.class_likes.get(actual))
242 }
243
244 #[inline]
245 fn get_method_by_words(&self, class: Word, method: Word) -> Option<&FunctionLikeMetadata> {
246 if let Some(metadata) = self.function_likes.get(&(class, method)) {
247 return Some(metadata);
248 }
249
250 if self.class_like_aliases.is_empty() {
251 return None;
252 }
253
254 let actual = self.class_like_aliases.get(&class)?;
255 self.function_likes.get(&(*actual, method))
256 }
257
258 #[inline]
267 #[must_use]
268 pub fn class_exists(&self, name: &[u8]) -> bool {
269 let lowercase_name = ascii_lowercase_word(name);
270 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class))
271 }
272
273 #[inline]
275 #[must_use]
276 pub fn interface_exists(&self, name: &[u8]) -> bool {
277 let lowercase_name = ascii_lowercase_word(name);
278 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Interface))
279 }
280
281 #[inline]
283 #[must_use]
284 pub fn trait_exists(&self, name: &[u8]) -> bool {
285 let lowercase_name = ascii_lowercase_word(name);
286 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Trait))
287 }
288
289 #[inline]
291 #[must_use]
292 pub fn enum_exists(&self, name: &[u8]) -> bool {
293 let lowercase_name = ascii_lowercase_word(name);
294 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Enum))
295 }
296
297 #[inline]
299 #[must_use]
300 pub fn class_like_exists(&self, name: &[u8]) -> bool {
301 let lowercase_name = ascii_lowercase_word(name);
302 self.symbols.contains(lowercase_name)
303 }
304
305 #[inline]
307 #[must_use]
308 pub fn namespace_exists(&self, name: &[u8]) -> bool {
309 let lowercase_name = ascii_lowercase_word(name);
310 self.symbols.contains_namespace(lowercase_name)
311 }
312
313 #[inline]
315 #[must_use]
316 pub fn class_or_trait_exists(&self, name: &[u8]) -> bool {
317 let lowercase_name = ascii_lowercase_word(name);
318 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class | SymbolKind::Trait))
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn class_or_interface_exists(&self, name: &[u8]) -> bool {
325 let lowercase_name = ascii_lowercase_word(name);
326 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class | SymbolKind::Interface))
327 }
328
329 #[inline]
331 #[must_use]
332 pub fn method_identifier_exists(&self, method_id: &MethodIdentifier) -> bool {
333 let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
334 let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
335 self.get_method_by_words(lowercase_class, lowercase_method).is_some()
336 }
337
338 #[inline]
340 #[must_use]
341 pub fn function_exists(&self, name: &[u8]) -> bool {
342 let lowercase_name = ascii_lowercase_word(name);
343 let identifier = (empty_word(), lowercase_name);
344 self.function_likes.contains_key(&identifier)
345 }
346
347 #[inline]
350 #[must_use]
351 pub fn constant_exists(&self, name: &[u8]) -> bool {
352 let lowercase_name = ascii_lowercase_constant_name_word(name);
353 self.constants.contains_key(&lowercase_name)
354 }
355
356 #[inline]
358 #[must_use]
359 pub fn method_exists(&self, class: &[u8], method: &[u8]) -> bool {
360 let lowercase_class = ascii_lowercase_word(class);
361 let lowercase_method = ascii_lowercase_word(method);
362 self.get_class_like_by_word(lowercase_class)
363 .is_some_and(|meta| meta.appearing_method_ids.contains_key(&lowercase_method))
364 }
365
366 #[inline]
371 #[must_use]
372 pub fn property_exists(&self, class: &[u8], property: &[u8]) -> bool {
373 let lowercase_class = ascii_lowercase_word(class);
374 let property_name = word(property);
375 self.get_class_like_by_word(lowercase_class)
376 .is_some_and(|meta| meta.appearing_property_ids.contains_key(&property_name))
377 }
378
379 #[inline]
382 #[must_use]
383 pub fn magic_property_exists(&self, class: &[u8], property: &[u8]) -> bool {
384 let lowercase_class = ascii_lowercase_word(class);
385 let property_name = word(property);
386 self.get_class_like_by_word(lowercase_class)
387 .is_some_and(|meta| meta.magic_property_ids.contains_key(&property_name))
388 }
389
390 #[inline]
393 #[must_use]
394 pub fn class_constant_exists(&self, class: &[u8], constant: &[u8]) -> bool {
395 let lowercase_class = ascii_lowercase_word(class);
396 let constant_name = word(constant);
397 self.get_class_like_by_word(lowercase_class).is_some_and(|meta| {
398 meta.constants.contains_key(&constant_name) || meta.enum_cases.contains_key(&constant_name)
399 })
400 }
401
402 #[inline]
405 #[must_use]
406 pub fn get_class(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
407 let lowercase_name = ascii_lowercase_word(name);
408 self.get_class_like_by_word(lowercase_name).filter(|metadata| metadata.kind.is_class())
409 }
410
411 #[inline]
413 #[must_use]
414 pub fn get_interface(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
415 let lowercase_name = ascii_lowercase_word(name);
416 self.get_class_like_by_word(lowercase_name).filter(|metadata| metadata.kind.is_interface())
417 }
418
419 #[inline]
421 #[must_use]
422 pub fn get_trait(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
423 let lowercase_name = ascii_lowercase_word(name);
424 self.get_class_like_by_word(lowercase_name).filter(|metadata| metadata.kind.is_trait())
425 }
426
427 #[inline]
429 #[must_use]
430 pub fn get_enum(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
431 let lowercase_name = ascii_lowercase_word(name);
432 self.get_class_like_by_word(lowercase_name).filter(|metadata| metadata.kind.is_enum())
433 }
434
435 #[inline]
437 #[must_use]
438 pub fn get_class_like(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
439 let lowercase_name = ascii_lowercase_word(name);
440 self.get_class_like_by_word(lowercase_name)
441 }
442
443 #[inline]
445 #[must_use]
446 pub fn get_function(&self, name: &[u8]) -> Option<&FunctionLikeMetadata> {
447 let lowercase_name = ascii_lowercase_word(name);
448 let identifier = (empty_word(), lowercase_name);
449 self.function_likes.get(&identifier)
450 }
451
452 #[inline]
454 #[must_use]
455 pub fn get_method(&self, class: &[u8], method: &[u8]) -> Option<&FunctionLikeMetadata> {
456 let lowercase_class = ascii_lowercase_word(class);
457 let lowercase_method = ascii_lowercase_word(method);
458 self.get_method_by_words(lowercase_class, lowercase_method)
459 }
460
461 #[inline]
464 #[must_use]
465 pub fn get_closure(&self, synthetic_name: &Word) -> Option<&FunctionLikeMetadata> {
466 self.function_likes.get(&(empty_word(), *synthetic_name))
467 }
468
469 #[inline]
473 #[must_use]
474 pub fn get_closure_at(&self, file: &File, span: Span) -> Option<&FunctionLikeMetadata> {
475 let name = crate::build_synthetic_name("closure", file, span);
476 self.get_closure(&name)
477 }
478
479 #[inline]
481 #[must_use]
482 pub fn get_method_by_id(&self, method_id: &MethodIdentifier) -> Option<&FunctionLikeMetadata> {
483 let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
484 let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
485 self.get_method_by_words(lowercase_class, lowercase_method)
486 }
487
488 #[inline]
491 #[must_use]
492 pub fn get_declaring_method(&self, class: &[u8], method: &[u8]) -> Option<&FunctionLikeMetadata> {
493 let method_id = MethodIdentifier::new(word(class), word(method));
494 let declaring_method_id = self.get_declaring_method_identifier(&method_id);
495 self.get_method(
496 declaring_method_id.get_class_name().as_bytes(),
497 declaring_method_id.get_method_name().as_bytes(),
498 )
499 }
500
501 #[inline]
504 #[must_use]
505 pub fn get_function_like(
506 &self,
507 identifier: &crate::identifier::function_like::FunctionLikeIdentifier,
508 ) -> Option<&FunctionLikeMetadata> {
509 use crate::identifier::function_like::FunctionLikeIdentifier;
510 match identifier {
511 FunctionLikeIdentifier::Function(name) => self.get_function(name.as_bytes()),
512 FunctionLikeIdentifier::Method(class, method) => self.get_method(class.as_bytes(), method.as_bytes()),
513 FunctionLikeIdentifier::Closure(name) => self.get_closure(name),
514 }
515 }
516
517 #[inline]
520 #[must_use]
521 pub fn get_constant(&self, name: &[u8]) -> Option<&ConstantMetadata> {
522 let lowercase_name = ascii_lowercase_constant_name_word(name);
523 self.constants.get(&lowercase_name)
524 }
525
526 #[inline]
533 #[must_use]
534 pub fn span_of(&self, name: &[u8]) -> Option<Span> {
535 if let Some(meta) = self.get_class_like(name) {
536 return Some(meta.name_span.unwrap_or(meta.span));
537 }
538
539 if let Some(meta) = self.get_function(name) {
540 return Some(meta.name_span.unwrap_or(meta.span));
541 }
542
543 self.get_constant(name).map(|meta| meta.span)
544 }
545
546 #[inline]
549 #[must_use]
550 pub fn get_class_constant(&self, class: &[u8], constant: &[u8]) -> Option<&ClassLikeConstantMetadata> {
551 let lowercase_class = ascii_lowercase_word(class);
552 let constant_name = word(constant);
553 self.get_class_like_by_word(lowercase_class).and_then(|meta| meta.constants.get(&constant_name))
554 }
555
556 #[inline]
558 #[must_use]
559 pub fn get_enum_case(&self, class: &[u8], case: &[u8]) -> Option<&EnumCaseMetadata> {
560 let lowercase_class = ascii_lowercase_word(class);
561 let case_name = word(case);
562 self.get_class_like_by_word(lowercase_class).and_then(|meta| meta.enum_cases.get(&case_name))
563 }
564
565 #[inline]
570 #[must_use]
571 pub fn get_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
572 let lowercase_class = ascii_lowercase_word(class);
573 let property_name = word(property);
574 self.get_class_like_by_word(lowercase_class)?.properties.get(&property_name)
575 }
576
577 #[inline]
580 #[must_use]
581 pub fn get_magic_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
582 let lowercase_class = ascii_lowercase_word(class);
583 let property_name = word(property);
584 self.get_class_like_by_word(lowercase_class)?.magic_properties.get(&property_name)
585 }
586
587 #[inline]
589 #[must_use]
590 pub fn get_declaring_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
591 let lowercase_class = ascii_lowercase_word(class);
592 let property_name = word(property);
593 let declaring_class =
594 self.get_class_like_by_word(lowercase_class)?.declaring_property_ids.get(&property_name)?;
595 self.class_likes.get(declaring_class)?.properties.get(&property_name)
596 }
597
598 #[inline]
601 #[must_use]
602 pub fn get_declaring_magic_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
603 let lowercase_class = ascii_lowercase_word(class);
604 let property_name = word(property);
605 let declaring_class = self.get_class_like_by_word(lowercase_class)?.magic_property_ids.get(&property_name)?;
606 self.class_likes.get(declaring_class)?.magic_properties.get(&property_name)
607 }
608 #[inline]
612 #[must_use]
613 pub fn get_property_type(&self, class: &[u8], property: &[u8]) -> Option<&TUnion> {
614 let lowercase_class = ascii_lowercase_word(class);
615 let property_name = word(property);
616 let declaring_class =
617 self.get_class_like_by_word(lowercase_class)?.declaring_property_ids.get(&property_name)?;
618 let property_meta = self.class_likes.get(declaring_class)?.properties.get(&property_name)?;
619 property_meta.type_metadata.as_ref().map(|tm| &tm.type_union)
620 }
621
622 #[must_use]
624 pub fn get_class_constant_type<'meta>(&'meta self, class: &[u8], constant: &[u8]) -> Option<Cow<'meta, TUnion>> {
625 let lowercase_class = ascii_lowercase_word(class);
626 let constant_name = word(constant);
627 let class_meta = self.get_class_like_by_word(lowercase_class)?;
628
629 if class_meta.kind.is_enum() && class_meta.enum_cases.contains_key(&constant_name) {
631 let atomic = TAtomic::Object(TObject::new_enum_case(class_meta.original_name, constant_name));
632 return Some(Cow::Owned(TUnion::from_atomic(atomic)));
633 }
634
635 let constant_meta = class_meta.constants.get(&constant_name)?;
637
638 if let Some(type_meta) = constant_meta.type_metadata.as_ref() {
640 return Some(Cow::Borrowed(&type_meta.type_union));
641 }
642
643 constant_meta.inferred_type.as_ref().map(|atomic| Cow::Owned(TUnion::from_atomic(atomic.clone())))
645 }
646 #[inline]
650 #[must_use]
651 pub fn class_extends(&self, child: &[u8], parent: &[u8]) -> bool {
652 let lowercase_child = ascii_lowercase_word(child);
653 let lowercase_parent = ascii_lowercase_word(parent);
654 let Some(metadata) = self.get_class_like_by_word(lowercase_child) else {
655 return false;
656 };
657
658 if metadata.all_parent_classes.contains(&lowercase_parent) {
659 return true;
660 }
661
662 if self.class_like_aliases.is_empty() || self.class_likes.contains_key(&lowercase_parent) {
663 return false;
664 }
665
666 self.class_like_aliases
667 .get(&lowercase_parent)
668 .is_some_and(|actual| metadata.all_parent_classes.contains(actual))
669 }
670
671 #[inline]
673 #[must_use]
674 pub fn class_implements(&self, class: &[u8], interface: &[u8]) -> bool {
675 let lowercase_class = ascii_lowercase_word(class);
676 let lowercase_interface = ascii_lowercase_word(interface);
677 let Some(metadata) = self.get_class_like_by_word(lowercase_class) else {
678 return false;
679 };
680
681 if metadata.all_parent_interfaces.contains(&lowercase_interface) {
682 return true;
683 }
684
685 if self.class_like_aliases.is_empty() || self.class_likes.contains_key(&lowercase_interface) {
686 return false;
687 }
688
689 self.class_like_aliases
690 .get(&lowercase_interface)
691 .is_some_and(|actual| metadata.all_parent_interfaces.contains(actual))
692 }
693
694 #[inline]
696 #[must_use]
697 pub fn class_uses_trait(&self, class: &[u8], trait_name: &[u8]) -> bool {
698 let lowercase_class = ascii_lowercase_word(class);
699 let lowercase_trait = ascii_lowercase_word(trait_name);
700 let Some(metadata) = self.get_class_like_by_word(lowercase_class) else {
701 return false;
702 };
703
704 if metadata.used_traits.contains(&lowercase_trait) {
705 return true;
706 }
707
708 if self.class_like_aliases.is_empty() || self.class_likes.contains_key(&lowercase_trait) {
709 return false;
710 }
711
712 self.class_like_aliases.get(&lowercase_trait).is_some_and(|actual| metadata.used_traits.contains(actual))
713 }
714
715 #[inline]
717 #[must_use]
718 pub fn is_instance_of(&self, child: &[u8], parent: &[u8]) -> bool {
719 if child == parent {
720 return true;
721 }
722
723 let lowercase_child = ascii_lowercase_word(child);
724 let lowercase_parent = ascii_lowercase_word(parent);
725
726 if lowercase_child == lowercase_parent {
727 return true;
728 }
729
730 let Some(metadata) = self.get_class_like_by_word(lowercase_child) else {
731 return false;
732 };
733
734 let matches = |parent| {
735 metadata.name == parent
736 || metadata.all_parent_classes.contains(&parent)
737 || metadata.all_parent_interfaces.contains(&parent)
738 || metadata.used_traits.contains(&parent)
739 || metadata.require_extends.contains(&parent)
740 || metadata.require_implements.contains(&parent)
741 };
742
743 if matches(lowercase_parent) {
744 return true;
745 }
746
747 if self.class_like_aliases.is_empty() || self.class_likes.contains_key(&lowercase_parent) {
748 return false;
749 }
750
751 self.class_like_aliases.get(&lowercase_parent).is_some_and(|actual| matches(*actual))
752 }
753
754 #[inline]
756 #[must_use]
757 pub fn is_enum_or_final_class(&self, name: &[u8]) -> bool {
758 let lowercase_name = ascii_lowercase_word(name);
759 self.get_class_like_by_word(lowercase_name).is_some_and(|meta| meta.kind.is_enum() || meta.flags.is_final())
760 }
761
762 #[inline]
765 #[must_use]
766 pub fn is_inheritable(&self, name: &[u8]) -> bool {
767 let lowercase_name = ascii_lowercase_word(name);
768 match self.symbols.get_kind(lowercase_name) {
769 Some(SymbolKind::Class) => {
770 self.get_class_like_by_word(lowercase_name).is_some_and(|meta| !meta.flags.is_final())
771 }
772 Some(SymbolKind::Enum) => false,
773 Some(SymbolKind::Interface | SymbolKind::Trait) | None => true,
774 }
775 }
776
777 #[inline]
779 #[must_use]
780 pub fn get_class_descendants(&self, class: &[u8]) -> WordSet {
781 let lowercase_class = ascii_lowercase_word(class);
782 let lowercase_class = self.resolve_class_like_word(lowercase_class).unwrap_or(lowercase_class);
783 let mut all_descendants = WordSet::default();
784 let mut queue = vec![&lowercase_class];
785 let mut visited = WordSet::default();
786 visited.insert(lowercase_class);
787
788 while let Some(current_name) = queue.pop() {
789 if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
790 for descendant in direct_descendants {
791 if visited.insert(*descendant) {
792 all_descendants.insert(*descendant);
793 queue.push(descendant);
794 }
795 }
796 }
797 }
798
799 all_descendants
800 }
801
802 #[inline]
804 #[must_use]
805 pub fn get_class_ancestors(&self, class: &[u8]) -> WordSet {
806 let lowercase_class = ascii_lowercase_word(class);
807 let mut ancestors = WordSet::default();
808 if let Some(meta) = self.get_class_like_by_word(lowercase_class) {
809 ancestors.extend(meta.all_parent_classes.iter().copied());
810 ancestors.extend(meta.all_parent_interfaces.iter().copied());
811 }
812 ancestors
813 }
814
815 #[inline]
817 #[must_use]
818 pub fn get_declaring_method_class(&self, class: &[u8], method: &[u8]) -> Option<Word> {
819 let lowercase_class = ascii_lowercase_word(class);
820 let lowercase_method = ascii_lowercase_word(method);
821
822 self.get_class_like_by_word(lowercase_class)?
823 .declaring_method_ids
824 .get(&lowercase_method)
825 .map(|method_id| method_id.get_class_name())
826 }
827
828 #[must_use]
830 pub fn get_declaring_method_identifier(&self, method_id: &MethodIdentifier) -> MethodIdentifier {
831 let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
832 let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
833
834 let Some(class_meta) = self.get_class_like_by_word(lowercase_class) else {
835 return *method_id;
836 };
837
838 if let Some(declaring_method_id) = class_meta.declaring_method_ids.get(&lowercase_method) {
839 return *declaring_method_id;
840 }
841
842 if class_meta.flags.is_abstract()
843 && let Some(overridden_map) = class_meta.overridden_method_ids.get(&lowercase_method)
844 && let Some((_, first_method_id)) = overridden_map.first()
845 {
846 return *first_method_id;
847 }
848
849 *method_id
850 }
851
852 #[inline]
854 #[must_use]
855 pub fn method_is_overriding(&self, class: &[u8], method: &[u8]) -> bool {
856 let lowercase_class = ascii_lowercase_word(class);
857 let lowercase_method = ascii_lowercase_word(method);
858 self.get_class_like_by_word(lowercase_class)
859 .is_some_and(|meta| meta.overridden_method_ids.contains_key(&lowercase_method))
860 }
861
862 #[inline]
864 #[must_use]
865 pub fn method_is_abstract(&self, class: &[u8], method: &[u8]) -> bool {
866 let lowercase_class = ascii_lowercase_word(class);
867 let lowercase_method = ascii_lowercase_word(method);
868 self.get_method_by_words(lowercase_class, lowercase_method)
869 .and_then(|meta| meta.method_metadata.as_ref())
870 .is_some_and(|method_meta| method_meta.is_abstract)
871 }
872
873 #[inline]
875 #[must_use]
876 pub fn method_is_final(&self, class: &[u8], method: &[u8]) -> bool {
877 let lowercase_class = ascii_lowercase_word(class);
878 let lowercase_method = ascii_lowercase_word(method);
879 self.get_method_by_words(lowercase_class, lowercase_method)
880 .and_then(|meta| meta.method_metadata.as_ref())
881 .is_some_and(|method_meta| method_meta.is_final)
882 }
883
884 #[inline]
890 #[must_use]
891 pub fn get_method_visibility(&self, class: &[u8], method: &[u8]) -> Option<Visibility> {
892 let lowercase_class = ascii_lowercase_word(class);
893 let lowercase_method = ascii_lowercase_word(method);
894
895 if let Some(class_meta) = self.get_class_like_by_word(lowercase_class)
897 && let Some(overridden_visibility) = class_meta.trait_visibility_map.get(&lowercase_method)
898 {
899 return Some(*overridden_visibility);
900 }
901
902 let declaring_class = self.get_declaring_method_class(class, method)?;
904 let identifier = (declaring_class, lowercase_method);
905
906 self.function_likes
907 .get(&identifier)
908 .and_then(|meta| meta.method_metadata.as_ref())
909 .map(|method_meta| method_meta.visibility)
910 }
911
912 #[must_use]
914 pub fn get_function_like_thrown_types<'meta>(
915 &'meta self,
916 class_like: Option<&'meta ClassLikeMetadata>,
917 function_like: &'meta FunctionLikeMetadata,
918 ) -> &'meta [TypeMetadata] {
919 if !function_like.thrown_types.is_empty() {
920 return function_like.thrown_types.as_slice();
921 }
922
923 if !function_like.kind.is_method() {
924 return &[];
925 }
926
927 let Some(class_like) = class_like else {
928 return &[];
929 };
930
931 let method_name = &function_like.name;
932
933 if let Some(overridden_map) = class_like.overridden_method_ids.get(method_name) {
934 for (parent_class_name, parent_method_id) in overridden_map {
935 if class_like.name.as_bytes().eq_ignore_ascii_case(parent_class_name.as_bytes()) {
936 continue; }
938
939 let Some(parent_class) = self.class_likes.get(parent_class_name) else {
940 continue;
941 };
942
943 let parent_method_key = (parent_method_id.get_class_name(), parent_method_id.get_method_name());
944 if let Some(parent_method) = self.function_likes.get(&parent_method_key) {
945 let thrown = self.get_function_like_thrown_types(Some(parent_class), parent_method);
946 if !thrown.is_empty() {
947 return thrown;
948 }
949 }
950 }
951 }
952
953 &[]
954 }
955
956 #[inline]
960 #[must_use]
961 pub fn get_declaring_property_class(&self, class: &[u8], property: &[u8]) -> Option<Word> {
962 let lowercase_class = ascii_lowercase_word(class);
963 let property_name = word(property);
964 self.get_class_like_by_word(lowercase_class)?.declaring_property_ids.get(&property_name).copied()
965 }
966
967 #[must_use]
969 pub fn get_all_descendants(&self, class: &[u8]) -> WordSet {
970 let lowercase_class = ascii_lowercase_word(class);
971 let lowercase_class = self.resolve_class_like_word(lowercase_class).unwrap_or(lowercase_class);
972 let mut all_descendants = WordSet::default();
973 let mut queue = vec![&lowercase_class];
974 let mut visited = WordSet::default();
975 visited.insert(lowercase_class);
976
977 while let Some(current_name) = queue.pop() {
978 if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
979 for descendant in direct_descendants {
980 if visited.insert(*descendant) {
981 all_descendants.insert(*descendant);
982 queue.push(descendant);
983 }
984 }
985 }
986 }
987
988 all_descendants
989 }
990
991 #[must_use]
994 pub fn get_anonymous_class_name(file: &File, span: Span) -> Word {
995 crate::get_anonymous_class_name(file, span)
996 }
997
998 #[must_use]
1001 pub fn get_anonymous_class(&self, file: &File, span: Span) -> Option<&ClassLikeMetadata> {
1002 let name = Self::get_anonymous_class_name(file, span);
1003 self.get_class_like(name.as_bytes())
1004 }
1005
1006 #[inline]
1016 #[must_use]
1017 pub fn get_file_signature(&self, file_id: &FileId) -> Option<&FileSignature> {
1018 self.file_signatures.get(file_id)
1019 }
1020
1021 #[inline]
1032 pub fn set_file_signature(&mut self, file_id: FileId, signature: FileSignature) -> Option<FileSignature> {
1033 self.file_signatures.insert(file_id, signature)
1034 }
1035
1036 pub fn mark_safe_symbols(&mut self, diff: &CodebaseDiff, references: &SymbolReferences) -> Option<WordSet> {
1050 let (invalid_symbols, partially_invalid, invalid_files) = references.get_invalid_symbols(diff)?;
1051
1052 for keep_symbol in diff.get_keep() {
1054 if !invalid_symbols.contains(keep_symbol) {
1055 if keep_symbol.1.is_empty() {
1056 if !partially_invalid.contains(&keep_symbol.0) {
1058 self.safe_symbols.insert(keep_symbol.0);
1059 }
1060 } else {
1061 self.safe_symbol_members.insert(*keep_symbol);
1063 }
1064 }
1065 }
1066
1067 Some(invalid_files)
1068 }
1069
1070 pub fn extend(&mut self, other: CodebaseMetadata) {
1075 let class_likes_changed = !other.class_likes.is_empty();
1076 for (k, mut v) in other.class_likes {
1077 match self.class_likes.entry(k) {
1078 Entry::Occupied(mut entry) => {
1079 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1080 v.version_constraint.merge(entry.get().version_constraint.clone());
1081 entry.insert(v);
1082 } else {
1083 entry.get_mut().version_constraint.merge(v.version_constraint);
1084 }
1085 }
1086 Entry::Vacant(entry) => {
1087 entry.insert(v);
1088 }
1089 }
1090 }
1091
1092 if class_likes_changed && !self.class_like_alias_declarations.is_empty() {
1093 self.class_like_aliases_dirty = true;
1094 }
1095
1096 self.merge_class_like_alias_declarations(other.class_like_alias_declarations);
1097
1098 for (k, mut v) in other.function_likes {
1099 match self.function_likes.entry(k) {
1100 Entry::Occupied(mut entry) => {
1101 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1102 v.version_constraint.merge(entry.get().version_constraint.clone());
1103 entry.insert(v);
1104 } else {
1105 entry.get_mut().version_constraint.merge(v.version_constraint);
1106 }
1107 }
1108 Entry::Vacant(entry) => {
1109 entry.insert(v);
1110 }
1111 }
1112 }
1113
1114 for (k, mut v) in other.constants {
1115 match self.constants.entry(k) {
1116 Entry::Occupied(mut entry) => {
1117 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1118 v.version_constraint.merge(entry.get().version_constraint.clone());
1119 entry.insert(v);
1120 } else {
1121 entry.get_mut().version_constraint.merge(v.version_constraint);
1122 }
1123 }
1124 Entry::Vacant(entry) => {
1125 entry.insert(v);
1126 }
1127 }
1128 }
1129
1130 self.symbols.extend(other.symbols);
1131
1132 for (k, v) in other.all_class_like_descendants {
1133 self.all_class_like_descendants.entry(k).or_default().extend(v);
1134 }
1135
1136 for (k, v) in other.direct_classlike_descendants {
1137 self.direct_classlike_descendants.entry(k).or_default().extend(v);
1138 }
1139
1140 self.file_signatures.extend(other.file_signatures);
1141 self.safe_symbols.extend(other.safe_symbols);
1142 self.safe_symbol_members.extend(other.safe_symbol_members);
1143 self.infer_types_from_usage |= other.infer_types_from_usage;
1144 self.merge_patch_class_likes(other.patch_class_likes);
1145 self.merge_patch_function_likes(other.patch_function_likes);
1146 self.merge_patch_constants(other.patch_constants);
1147 }
1148
1149 pub fn extend_ref(&mut self, other: &CodebaseMetadata) {
1155 let class_likes_changed = !other.class_likes.is_empty();
1156 for (k, v) in &other.class_likes {
1157 match self.class_likes.entry(*k) {
1158 Entry::Occupied(mut entry) => {
1159 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1160 let mut new = v.clone();
1161 new.version_constraint.merge(entry.get().version_constraint.clone());
1162 entry.insert(new);
1163 } else {
1164 entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1165 }
1166 }
1167 Entry::Vacant(entry) => {
1168 entry.insert(v.clone());
1169 }
1170 }
1171 }
1172
1173 if class_likes_changed && !self.class_like_alias_declarations.is_empty() {
1174 self.class_like_aliases_dirty = true;
1175 }
1176
1177 self.merge_class_like_alias_declarations(
1178 other.class_like_alias_declarations.iter().map(|(alias, declaration)| (*alias, *declaration)),
1179 );
1180
1181 for (k, v) in &other.function_likes {
1182 match self.function_likes.entry(*k) {
1183 Entry::Occupied(mut entry) => {
1184 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1185 let mut new = v.clone();
1186 new.version_constraint.merge(entry.get().version_constraint.clone());
1187 entry.insert(new);
1188 } else {
1189 entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1190 }
1191 }
1192 Entry::Vacant(entry) => {
1193 entry.insert(v.clone());
1194 }
1195 }
1196 }
1197
1198 for (k, v) in &other.constants {
1199 match self.constants.entry(*k) {
1200 Entry::Occupied(mut entry) => {
1201 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1202 let mut new = v.clone();
1203 new.version_constraint.merge(entry.get().version_constraint.clone());
1204 entry.insert(new);
1205 } else {
1206 entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1207 }
1208 }
1209 Entry::Vacant(entry) => {
1210 entry.insert(v.clone());
1211 }
1212 }
1213 }
1214
1215 self.symbols.extend_ref(&other.symbols);
1216
1217 for (k, v) in &other.all_class_like_descendants {
1218 self.all_class_like_descendants.entry(*k).or_default().extend(v.iter().copied());
1219 }
1220
1221 for (k, v) in &other.direct_classlike_descendants {
1222 self.direct_classlike_descendants.entry(*k).or_default().extend(v.iter().copied());
1223 }
1224
1225 for (k, v) in &other.file_signatures {
1226 self.file_signatures.insert(*k, v.clone());
1227 }
1228 self.safe_symbols.extend(other.safe_symbols.iter().copied());
1229 self.safe_symbol_members.extend(other.safe_symbol_members.iter().copied());
1230 self.infer_types_from_usage |= other.infer_types_from_usage;
1231 self.merge_patch_class_likes(other.patch_class_likes.iter().map(|(k, v)| (*k, v.clone())));
1232 self.merge_patch_function_likes(other.patch_function_likes.iter().map(|(k, v)| (*k, v.clone())));
1233 self.merge_patch_constants(other.patch_constants.iter().map(|(k, v)| (*k, v.clone())));
1234 }
1235
1236 fn merge_class_like_alias_declarations(
1237 &mut self,
1238 declarations: impl IntoIterator<Item = (Word, (Word, Span, MetadataFlags))>,
1239 ) {
1240 for (alias, (target, span, flags)) in declarations {
1241 self.add_class_like_alias(alias, target, span, flags);
1242 }
1243 }
1244
1245 fn merge_patch_class_likes(&mut self, incoming: impl IntoIterator<Item = (Word, ClassLikeMetadata)>) {
1252 for (k, v) in incoming {
1253 match self.patch_class_likes.entry(k) {
1254 Entry::Occupied(mut entry) => {
1255 let diagnostic = duplicate_patch_class_diagnostic(entry.get(), &v);
1256 entry.get_mut().issues.push(diagnostic);
1257 }
1258 Entry::Vacant(entry) => {
1259 entry.insert(v);
1260 }
1261 }
1262 }
1263 }
1264
1265 fn merge_patch_function_likes(&mut self, incoming: impl IntoIterator<Item = ((Word, Word), FunctionLikeMetadata)>) {
1269 for (k, v) in incoming {
1270 match self.patch_function_likes.entry(k) {
1271 Entry::Occupied(mut entry) => {
1272 if k.0.is_empty() {
1273 let diagnostic = duplicate_patch_function_diagnostic(entry.get(), &v);
1274 entry.get_mut().issues.push(diagnostic);
1275 }
1276 }
1277 Entry::Vacant(entry) => {
1278 entry.insert(v);
1279 }
1280 }
1281 }
1282 }
1283
1284 fn merge_patch_constants(&mut self, incoming: impl IntoIterator<Item = (Word, ConstantMetadata)>) {
1286 for (k, v) in incoming {
1287 match self.patch_constants.entry(k) {
1288 Entry::Occupied(mut entry) => {
1289 let diagnostic = duplicate_patch_constant_diagnostic(entry.get(), &v);
1290 entry.get_mut().issues.push(diagnostic);
1291 }
1292 Entry::Vacant(entry) => {
1293 entry.insert(v);
1294 }
1295 }
1296 }
1297 }
1298
1299 pub fn convert_partial_to_patch(&mut self) {
1306 for (k, v) in std::mem::take(&mut self.class_likes) {
1307 self.patch_class_likes.insert(k, v);
1308 }
1309
1310 for (k, v) in std::mem::take(&mut self.function_likes) {
1311 self.patch_function_likes.insert(k, v);
1312 }
1313
1314 for (k, v) in std::mem::take(&mut self.constants) {
1315 self.patch_constants.insert(k, v);
1316 }
1317
1318 self.symbols = Symbols::new();
1319 self.class_like_aliases.clear();
1320 self.class_like_alias_declarations.clear();
1321 self.class_like_aliases_dirty = false;
1322 self.all_class_like_descendants.clear();
1323 self.direct_classlike_descendants.clear();
1324 }
1325
1326 pub fn apply_patches_pass(&mut self) {
1336 let mut inherited_overrides: HashSet<(Word, Word)> = HashSet::default();
1340
1341 let class_keys: Vec<Word> = self.patch_class_likes.keys().copied().collect();
1342 for fqcn in class_keys {
1343 let Some(target) = self.class_likes.get(&fqcn) else {
1344 if let Some(p) = self.patch_class_likes.get_mut(&fqcn) {
1345 let diag = orphan_patch_class_diagnostic(p);
1346 p.issues.push(diag);
1347 }
1348 continue;
1349 };
1350 if target.flags.is_user_defined() {
1353 continue;
1354 }
1355
1356 let mut working = target.clone();
1357 let inherited =
1358 collect_inherited_patch_methods(&working, &self.patch_class_likes[&fqcn], &self.class_likes);
1359 inherited_overrides.extend(inherited.iter().map(|method| (fqcn, *method)));
1360 if let Some(patch_entry) = self.patch_class_likes.get_mut(&fqcn) {
1361 working.apply_patch(patch_entry, &inherited);
1362 }
1363 self.class_likes.insert(fqcn, working);
1364 }
1365
1366 let func_keys: Vec<(Word, Word)> = self.patch_function_likes.keys().copied().collect();
1367 for key in func_keys {
1368 let Some(target) = self.function_likes.get(&key) else {
1369 if inherited_overrides.contains(&key) {
1370 if let Some(p) = self.patch_function_likes.get(&key) {
1375 let materialized = p.clone();
1376 self.function_likes.insert(key, materialized);
1377 }
1378 continue;
1379 }
1380 if key.0.is_empty()
1383 && let Some(p) = self.patch_function_likes.get_mut(&key)
1384 {
1385 let diag = orphan_patch_function_diagnostic(p);
1386 p.issues.push(diag);
1387 }
1388 continue;
1389 };
1390 if target.flags.is_user_defined() {
1391 continue;
1392 }
1393
1394 let mut working = target.clone();
1395 if let Some(patch_entry) = self.patch_function_likes.get_mut(&key) {
1396 working.apply_patch(patch_entry);
1397 }
1398 self.function_likes.insert(key, working);
1399 }
1400
1401 let const_keys: Vec<Word> = self.patch_constants.keys().copied().collect();
1402 for fqcn in const_keys {
1403 let Some(target) = self.constants.get(&fqcn) else {
1404 if let Some(p) = self.patch_constants.get_mut(&fqcn) {
1405 let diag = orphan_patch_constant_diagnostic(p);
1406 p.issues.push(diag);
1407 }
1408 continue;
1409 };
1410 if target.flags.is_user_defined() {
1411 continue;
1412 }
1413
1414 let mut working = target.clone();
1415 if let Some(patch_entry) = self.patch_constants.get(&fqcn) {
1416 working.apply_patch(patch_entry);
1417 }
1418 self.constants.insert(fqcn, working);
1419 }
1420 }
1421
1422 #[must_use]
1438 pub fn extract_owned_keys(&self, merged: &CodebaseMetadata) -> CodebaseEntryKeys {
1439 let class_like_names = self
1440 .class_likes
1441 .iter()
1442 .filter(|(name, meta)| merged.class_likes.get(*name).is_some_and(|m| m.span == meta.span))
1443 .map(|(name, _)| *name)
1444 .collect();
1445
1446 let class_like_aliases = self
1447 .class_like_alias_declarations
1448 .iter()
1449 .filter(|(name, (_, span, _))| {
1450 merged.class_like_alias_declarations.get(*name).is_some_and(|(_, merged_span, _)| merged_span == span)
1451 })
1452 .map(|(alias, (target, span, _))| (*alias, *target, *span))
1453 .collect();
1454
1455 let function_like_keys = self
1456 .function_likes
1457 .iter()
1458 .filter(|(key, meta)| merged.function_likes.get(*key).is_some_and(|m| m.span == meta.span))
1459 .map(|(key, _)| *key)
1460 .collect();
1461
1462 let constant_names = self
1463 .constants
1464 .iter()
1465 .filter(|(name, meta)| merged.constants.get(*name).is_some_and(|m| m.span == meta.span))
1466 .map(|(name, _)| *name)
1467 .collect();
1468
1469 let file_ids = self.file_signatures.keys().copied().collect();
1471
1472 CodebaseEntryKeys { class_like_names, class_like_aliases, function_like_keys, constant_names, file_ids }
1473 }
1474
1475 pub fn remove_entries_by_keys(&mut self, keys: &CodebaseEntryKeys) {
1480 if !keys.class_like_names.is_empty() && !self.class_like_alias_declarations.is_empty() {
1481 self.class_like_aliases_dirty = true;
1482 }
1483
1484 for k in &keys.class_like_names {
1485 self.class_likes.remove(k);
1486 self.symbols.remove(*k);
1487 }
1488
1489 for (alias, _, _) in &keys.class_like_aliases {
1490 self.class_like_alias_declarations.remove(alias);
1491 self.class_like_aliases_dirty = true;
1492 }
1493
1494 for k in &keys.function_like_keys {
1495 self.function_likes.remove(k);
1496 }
1497
1498 for k in &keys.constant_names {
1499 self.constants.remove(k);
1500 }
1501
1502 for k in &keys.file_ids {
1503 self.file_signatures.remove(k);
1504 }
1505
1506 let removed_files: HashSet<FileId> = keys.file_ids.iter().copied().collect();
1509 self.patch_class_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1510 self.patch_function_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1511 self.patch_constants.retain(|_, m| !removed_files.contains(&m.span.file_id));
1512 }
1513
1514 pub fn take_issues(&mut self, user_defined: bool) -> IssueCollection {
1516 let mut issues = IssueCollection::new();
1517
1518 for meta in self.class_likes.values_mut() {
1519 if user_defined && !meta.flags.is_user_defined() {
1520 continue;
1521 }
1522 issues.extend(meta.take_issues());
1523 }
1524
1525 for meta in self.function_likes.values_mut() {
1526 if user_defined && !meta.flags.is_user_defined() {
1527 continue;
1528 }
1529 issues.extend(meta.take_issues());
1530 }
1531
1532 for meta in self.constants.values_mut() {
1533 if user_defined && !meta.flags.is_user_defined() {
1534 continue;
1535 }
1536 issues.extend(meta.take_issues());
1537 }
1538
1539 for meta in self.patch_class_likes.values_mut() {
1543 issues.extend(meta.take_issues());
1544 }
1545
1546 for meta in self.patch_function_likes.values_mut() {
1547 issues.extend(meta.take_issues());
1548 }
1549
1550 for meta in self.patch_constants.values_mut() {
1551 issues.extend(meta.take_issues());
1552 }
1553
1554 issues
1555 }
1556
1557 #[must_use]
1561 pub fn get_all_file_ids(&self) -> Vec<FileId> {
1562 self.file_signatures.keys().copied().collect()
1563 }
1564}
1565
1566fn collect_inherited_patch_methods(
1571 target: &ClassLikeMetadata,
1572 patch: &ClassLikeMetadata,
1573 class_likes: &WordMap<ClassLikeMetadata>,
1574) -> WordSet {
1575 if patch.methods.is_empty() {
1576 return WordSet::default();
1577 }
1578 let ancestor_methods = class_like::collect_ancestor_methods(target, class_likes);
1579 patch.methods.iter().filter(|m| ancestor_methods.contains(*m)).copied().collect()
1580}
1581
1582fn duplicate_patch_class_diagnostic(kept: &ClassLikeMetadata, dropped: &ClassLikeMetadata) -> Issue {
1583 Issue::error(format!(
1584 "Multiple patches target `{}`; at most one patch may target a given symbol.",
1585 kept.original_name
1586 ))
1587 .with_code(ScanningIssueKind::PatchDuplicateTarget)
1588 .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this symbol."))
1589 .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1590 .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1591}
1592
1593fn duplicate_patch_function_diagnostic(kept: &FunctionLikeMetadata, dropped: &FunctionLikeMetadata) -> Issue {
1594 Issue::error(format!(
1595 "Multiple patches target function `{}`; at most one patch may target a given symbol.",
1596 kept.name
1597 ))
1598 .with_code(ScanningIssueKind::PatchDuplicateTarget)
1599 .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this function."))
1600 .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1601 .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1602}
1603
1604fn duplicate_patch_constant_diagnostic(kept: &ConstantMetadata, dropped: &ConstantMetadata) -> Issue {
1605 Issue::error(format!(
1606 "Multiple patches target constant `{}`; at most one patch may target a given symbol.",
1607 kept.name
1608 ))
1609 .with_code(ScanningIssueKind::PatchDuplicateTarget)
1610 .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this constant."))
1611 .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1612 .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1613}
1614
1615fn orphan_patch_class_diagnostic(meta: &ClassLikeMetadata) -> Issue {
1616 Issue::error(format!(
1617 "Patch declares `{}` but no vendored or built-in definition exists to patch.",
1618 meta.original_name,
1619 ))
1620 .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1621 .with_annotation(Annotation::primary(meta.span))
1622 .with_help(
1623 "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1624 check the symbol name and verify the patch still matches the upstream source.",
1625 )
1626}
1627
1628fn orphan_patch_function_diagnostic(meta: &FunctionLikeMetadata) -> Issue {
1629 Issue::error(format!(
1630 "Patch declares function `{}` but no vendored or built-in definition exists to patch.",
1631 meta.name,
1632 ))
1633 .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1634 .with_annotation(Annotation::primary(meta.span))
1635 .with_help(
1636 "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1637 check the function name and verify the patch still matches the upstream source.",
1638 )
1639}
1640
1641fn orphan_patch_constant_diagnostic(meta: &ConstantMetadata) -> Issue {
1642 Issue::error(format!(
1643 "Patch declares constant `{}` but no vendored or built-in definition exists to patch.",
1644 meta.name,
1645 ))
1646 .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1647 .with_annotation(Annotation::primary(meta.span))
1648 .with_help(
1649 "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1650 check the constant name and verify the patch still matches the upstream source.",
1651 )
1652}
1653
1654fn should_replace_metadata(
1665 existing_flags: MetadataFlags,
1666 existing_span: Span,
1667 new_flags: MetadataFlags,
1668 new_span: Span,
1669) -> bool {
1670 let new_is_user_defined = new_flags.is_user_defined();
1671 let existing_is_user_defined = existing_flags.is_user_defined();
1672
1673 if new_is_user_defined != existing_is_user_defined {
1674 return new_is_user_defined;
1675 }
1676
1677 let new_is_patch = new_flags.is_patch();
1678 let existing_is_patch = existing_flags.is_patch();
1679
1680 if new_is_patch != existing_is_patch {
1681 return new_is_patch;
1682 }
1683
1684 let new_is_external = new_flags.is_external();
1685 let existing_is_external = existing_flags.is_external();
1686
1687 if new_is_external != existing_is_external {
1688 return new_is_external;
1689 }
1690
1691 let new_is_built_in = new_flags.is_built_in();
1692 let existing_is_built_in = existing_flags.is_built_in();
1693
1694 if new_is_built_in != existing_is_built_in {
1695 return new_is_built_in;
1696 }
1697
1698 let new_is_polyfill = new_flags.is_polyfill();
1699 let existing_is_polyfill = existing_flags.is_polyfill();
1700
1701 if new_is_polyfill != existing_is_polyfill {
1702 return !new_is_polyfill;
1703 }
1704
1705 new_span < existing_span
1706}
1707
1708#[cfg(test)]
1709mod should_replace_metadata_tests {
1710 use super::*;
1711
1712 #[test]
1713 fn non_polyfill_replaces_polyfill() {
1714 let polyfill = MetadataFlags::POLYFILL;
1715 let real = MetadataFlags::empty();
1716 assert!(should_replace_metadata(polyfill, Span::dummy(0, 100), real, Span::dummy(0, 100)));
1717 assert!(!should_replace_metadata(real, Span::dummy(0, 100), polyfill, Span::dummy(0, 100)));
1718 }
1719
1720 #[test]
1721 fn polyfill_does_not_replace_non_polyfill_even_with_smaller_span() {
1722 let real = MetadataFlags::empty();
1723 let polyfill = MetadataFlags::POLYFILL;
1724 assert!(!should_replace_metadata(real, Span::dummy(500, 600), polyfill, Span::dummy(0, 10)));
1725 }
1726
1727 #[test]
1728 fn user_defined_beats_polyfill_flag() {
1729 let polyfill_user = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1730 let plain = MetadataFlags::empty();
1731 assert!(!should_replace_metadata(polyfill_user, Span::dummy(0, 10), plain, Span::dummy(0, 10)));
1732 assert!(should_replace_metadata(plain, Span::dummy(0, 10), polyfill_user, Span::dummy(0, 10)));
1733 }
1734
1735 #[test]
1736 fn two_user_defined_fall_through_to_polyfill_check() {
1737 let a = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1738 let b = MetadataFlags::USER_DEFINED;
1739 assert!(should_replace_metadata(a, Span::dummy(0, 10), b, Span::dummy(0, 10)));
1740 assert!(!should_replace_metadata(b, Span::dummy(0, 10), a, Span::dummy(0, 10)));
1741 }
1742
1743 #[test]
1744 fn two_non_polyfills_fall_through_to_priority_rules() {
1745 let user = MetadataFlags::USER_DEFINED;
1746 let builtin = MetadataFlags::BUILTIN;
1747 assert!(!should_replace_metadata(user, Span::dummy(0, 10), builtin, Span::dummy(0, 10)));
1748 assert!(should_replace_metadata(builtin, Span::dummy(0, 10), user, Span::dummy(0, 10)));
1749 }
1750
1751 #[test]
1752 fn patch_beats_vendored() {
1753 let vendored = MetadataFlags::empty();
1754 let patch = MetadataFlags::PATCH;
1755 assert!(should_replace_metadata(vendored, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1756 assert!(!should_replace_metadata(patch, Span::dummy(0, 100), vendored, Span::dummy(0, 100)));
1757 }
1758
1759 #[test]
1760 fn patch_beats_builtin() {
1761 let builtin = MetadataFlags::BUILTIN;
1762 let patch = MetadataFlags::PATCH;
1763 assert!(should_replace_metadata(builtin, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1764 assert!(!should_replace_metadata(patch, Span::dummy(0, 100), builtin, Span::dummy(0, 100)));
1765 }
1766
1767 #[test]
1768 fn external_beats_builtin_and_vendored() {
1769 let external = MetadataFlags::EXTERNAL;
1770 let builtin = MetadataFlags::BUILTIN;
1771 let vendored = MetadataFlags::empty();
1772
1773 assert!(should_replace_metadata(builtin, Span::dummy(0, 100), external, Span::dummy(500, 600)));
1774 assert!(!should_replace_metadata(external, Span::dummy(500, 600), builtin, Span::dummy(0, 100)));
1775 assert!(should_replace_metadata(vendored, Span::dummy(0, 100), external, Span::dummy(500, 600)));
1776 assert!(!should_replace_metadata(external, Span::dummy(500, 600), vendored, Span::dummy(0, 100)));
1777 }
1778
1779 #[test]
1780 fn patch_and_user_defined_beat_external() {
1781 let external = MetadataFlags::EXTERNAL;
1782 let patch = MetadataFlags::PATCH;
1783 let user = MetadataFlags::USER_DEFINED;
1784
1785 assert!(should_replace_metadata(external, Span::dummy(0, 100), patch, Span::dummy(500, 600)));
1786 assert!(!should_replace_metadata(patch, Span::dummy(500, 600), external, Span::dummy(0, 100)));
1787 assert!(should_replace_metadata(external, Span::dummy(0, 100), user, Span::dummy(500, 600)));
1788 assert!(!should_replace_metadata(user, Span::dummy(500, 600), external, Span::dummy(0, 100)));
1789 }
1790
1791 #[test]
1792 fn user_defined_beats_patch() {
1793 let user = MetadataFlags::USER_DEFINED;
1794 let patch = MetadataFlags::PATCH;
1795 assert!(!should_replace_metadata(user, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1796 assert!(should_replace_metadata(patch, Span::dummy(0, 100), user, Span::dummy(0, 100)));
1797 }
1798
1799 #[test]
1800 fn patch_does_not_beat_user_defined_even_with_smaller_span() {
1801 let user = MetadataFlags::USER_DEFINED;
1802 let patch = MetadataFlags::PATCH;
1803 assert!(!should_replace_metadata(user, Span::dummy(500, 600), patch, Span::dummy(0, 10)));
1804 }
1805
1806 #[test]
1807 #[allow(clippy::expect_used)]
1808 fn patch_function_like_leaves_vendor_owning_slot() {
1809 use crate::metadata::function_like::FunctionLikeKind;
1814
1815 let name = word("foo");
1816 let key = (empty_word(), name);
1817 let vendor_span = Span::dummy(0, 100);
1818 let patch_span = Span::dummy(500, 600);
1819
1820 let vendor =
1821 FunctionLikeMetadata::new(FunctionLikeKind::Function, name, name, vendor_span, MetadataFlags::empty());
1822 let mut codebase = CodebaseMetadata::new();
1823 codebase.function_likes.insert(key, vendor);
1824
1825 let patch = FunctionLikeMetadata::new(FunctionLikeKind::Function, name, name, patch_span, MetadataFlags::PATCH);
1826 codebase.patch_function_likes.insert(key, patch);
1827
1828 codebase.apply_patches_pass();
1829
1830 let merged = codebase.function_likes.get(&key).expect("function-like must remain after patch");
1831 assert_eq!(merged.span, vendor_span, "patch must not move the slot's span");
1832 assert!(!merged.flags.is_patch(), "patch must not flip the slot's flags");
1833 }
1834
1835 #[test]
1836 fn patch_does_not_apply_to_user_defined_class() {
1837 let class_name = word("MyClass");
1838 let method_existing = word("doIt");
1839
1840 let mut user_class =
1841 ClassLikeMetadata::new(class_name, class_name, Span::dummy(0, 100), None, MetadataFlags::USER_DEFINED);
1842 user_class.methods.insert(method_existing);
1843
1844 let mut codebase = CodebaseMetadata::new();
1845 codebase.class_likes.insert(class_name, user_class);
1846
1847 let mut patch_class =
1848 ClassLikeMetadata::new(class_name, class_name, Span::dummy(0, 50), None, MetadataFlags::PATCH);
1849 let method_new = word("patchedMethod");
1850 patch_class.methods.insert(method_new);
1851
1852 codebase.patch_class_likes.insert(class_name, patch_class);
1853
1854 codebase.apply_patches_pass();
1855
1856 let class = &codebase.class_likes[&class_name];
1857 assert!(!class.methods.contains(&method_new));
1859 assert!(class.methods.contains(&method_existing));
1861 assert!(class.flags.is_user_defined());
1862 assert!(class.issues.is_empty());
1864 }
1865}