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 function_like_keys: Vec<(Word, Word)>,
65 pub constant_names: Vec<Word>,
67 pub file_ids: Vec<FileId>,
69}
70
71#[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 pub infer_types_from_usage: bool,
83 pub class_likes: WordMap<ClassLikeMetadata>,
85 pub function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
88 pub symbols: Symbols,
90 pub constants: WordMap<ConstantMetadata>,
92 pub all_class_like_descendants: WordMap<WordSet>,
94 pub direct_classlike_descendants: WordMap<WordSet>,
96 pub safe_symbols: WordSet,
98 pub safe_symbol_members: HashSet<(Word, Word)>,
100 pub file_signatures: HashMap<FileId, FileSignature>,
103 pub patch_class_likes: WordMap<ClassLikeMetadata>,
111 pub patch_function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
116 pub patch_constants: WordMap<ConstantMetadata>,
118}
119
120impl CodebaseMetadata {
121 #[inline]
123 #[must_use]
124 pub fn new() -> Self {
125 Self::default()
126 }
127
128 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 let constant_meta = class_meta.constants.get(&constant_name)?;
499
500 if let Some(type_meta) = constant_meta.type_metadata.as_ref() {
502 return Some(Cow::Borrowed(&type_meta.type_union));
503 }
504
505 constant_meta.inferred_type.as_ref().map(|atomic| Cow::Owned(TUnion::from_atomic(atomic.clone())))
507 }
508
509 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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; }
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
938 pub fn remove_file_signature(&mut self, file_id: &FileId) -> Option<FileSignature> {
939 self.file_signatures.remove(file_id)
940 }
941
942 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 for keep_symbol in diff.get_keep() {
961 if !invalid_symbols.contains(keep_symbol) {
962 if keep_symbol.1.is_empty() {
963 if !partially_invalid.contains(&keep_symbol.0) {
965 self.safe_symbols.insert(keep_symbol.0);
966 }
967 } else {
968 self.safe_symbol_members.insert(*keep_symbol);
970 }
971 }
972 }
973
974 Some(invalid_symbols.contains(&(empty_word(), empty_word())))
975 }
976
977 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 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 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 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 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 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 pub fn apply_patches_pass(&mut self) {
1215 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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 #[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
1503fn 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
1591fn 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 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 assert!(!class.methods.contains(&method_new));
1765 assert!(class.methods.contains(&method_existing));
1767 assert!(class.flags.is_user_defined());
1768 assert!(class.issues.is_empty());
1770 }
1771}