1use std::borrow::Cow;
2use std::collections::hash_map::Entry;
3
4use foldhash::HashMap;
5use foldhash::HashSet;
6use serde::Deserialize;
7use serde::Serialize;
8
9use mago_database::file::File;
10use mago_database::file::FileId;
11use mago_reporting::Annotation;
12use mago_reporting::Issue;
13use mago_reporting::IssueCollection;
14use mago_span::Span;
15use mago_word::Word;
16use mago_word::WordMap;
17use mago_word::WordSet;
18use mago_word::ascii_lowercase_constant_name_word;
19use mago_word::ascii_lowercase_word;
20use mago_word::empty_word;
21use mago_word::word;
22
23use crate::diff::CodebaseDiff;
24use crate::identifier::method::MethodIdentifier;
25use crate::issue::ScanningIssueKind;
26use crate::metadata::class_like::ClassLikeMetadata;
27use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
28use crate::metadata::constant::ConstantMetadata;
29use crate::metadata::enum_case::EnumCaseMetadata;
30use crate::metadata::flags::MetadataFlags;
31use crate::metadata::function_like::FunctionLikeMetadata;
32use crate::metadata::property::PropertyMetadata;
33use crate::metadata::ttype::TypeMetadata;
34use crate::reference::SymbolReferences;
35use crate::signature::FileSignature;
36use crate::symbol::SymbolKind;
37use crate::symbol::Symbols;
38use crate::ttype::atomic::TAtomic;
39use crate::ttype::atomic::object::TObject;
40use crate::ttype::union::TUnion;
41use crate::visibility::Visibility;
42
43pub mod attribute;
44pub mod class_like;
45pub mod class_like_constant;
46pub mod constant;
47pub mod enum_case;
48pub mod flags;
49pub mod function_like;
50pub mod parameter;
51pub mod property;
52pub mod property_hook;
53pub mod ttype;
54pub mod version_constraint;
55
56#[derive(Debug, Clone)]
62pub struct CodebaseEntryKeys {
63 pub class_like_names: Vec<Word>,
65 pub function_like_keys: Vec<(Word, Word)>,
67 pub constant_names: Vec<Word>,
69 pub file_ids: Vec<FileId>,
71}
72
73#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
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 pub function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
89 pub symbols: Symbols,
91 pub constants: WordMap<ConstantMetadata>,
93 pub all_class_like_descendants: WordMap<WordSet>,
95 pub direct_classlike_descendants: WordMap<WordSet>,
97 pub safe_symbols: WordSet,
99 pub safe_symbol_members: HashSet<(Word, Word)>,
101 pub file_signatures: HashMap<FileId, FileSignature>,
104 pub patch_class_likes: WordMap<ClassLikeMetadata>,
112 pub patch_function_likes: HashMap<(Word, Word), FunctionLikeMetadata>,
117 pub patch_constants: WordMap<ConstantMetadata>,
119}
120
121impl CodebaseMetadata {
122 #[inline]
124 #[must_use]
125 pub fn new() -> Self {
126 Self::default()
127 }
128
129 #[inline]
138 #[must_use]
139 pub fn class_exists(&self, name: &[u8]) -> bool {
140 let lowercase_name = ascii_lowercase_word(name);
141 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class))
142 }
143
144 #[inline]
146 #[must_use]
147 pub fn interface_exists(&self, name: &[u8]) -> bool {
148 let lowercase_name = ascii_lowercase_word(name);
149 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Interface))
150 }
151
152 #[inline]
154 #[must_use]
155 pub fn trait_exists(&self, name: &[u8]) -> bool {
156 let lowercase_name = ascii_lowercase_word(name);
157 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Trait))
158 }
159
160 #[inline]
162 #[must_use]
163 pub fn enum_exists(&self, name: &[u8]) -> bool {
164 let lowercase_name = ascii_lowercase_word(name);
165 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Enum))
166 }
167
168 #[inline]
170 #[must_use]
171 pub fn class_like_exists(&self, name: &[u8]) -> bool {
172 let lowercase_name = ascii_lowercase_word(name);
173 self.symbols.contains(lowercase_name)
174 }
175
176 #[inline]
178 #[must_use]
179 pub fn namespace_exists(&self, name: &[u8]) -> bool {
180 let lowercase_name = ascii_lowercase_word(name);
181 self.symbols.contains_namespace(lowercase_name)
182 }
183
184 #[inline]
186 #[must_use]
187 pub fn class_or_trait_exists(&self, name: &[u8]) -> bool {
188 let lowercase_name = ascii_lowercase_word(name);
189 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class | SymbolKind::Trait))
190 }
191
192 #[inline]
194 #[must_use]
195 pub fn class_or_interface_exists(&self, name: &[u8]) -> bool {
196 let lowercase_name = ascii_lowercase_word(name);
197 matches!(self.symbols.get_kind(lowercase_name), Some(SymbolKind::Class | SymbolKind::Interface))
198 }
199
200 #[inline]
202 #[must_use]
203 pub fn method_identifier_exists(&self, method_id: &MethodIdentifier) -> bool {
204 let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
205 let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
206 let identifier = (lowercase_class, lowercase_method);
207 self.function_likes.contains_key(&identifier)
208 }
209
210 #[inline]
212 #[must_use]
213 pub fn function_exists(&self, name: &[u8]) -> bool {
214 let lowercase_name = ascii_lowercase_word(name);
215 let identifier = (empty_word(), lowercase_name);
216 self.function_likes.contains_key(&identifier)
217 }
218
219 #[inline]
222 #[must_use]
223 pub fn constant_exists(&self, name: &[u8]) -> bool {
224 let lowercase_name = ascii_lowercase_constant_name_word(name);
225 self.constants.contains_key(&lowercase_name)
226 }
227
228 #[inline]
230 #[must_use]
231 pub fn method_exists(&self, class: &[u8], method: &[u8]) -> bool {
232 let lowercase_class = ascii_lowercase_word(class);
233 let lowercase_method = ascii_lowercase_word(method);
234 self.class_likes
235 .get(&lowercase_class)
236 .is_some_and(|meta| meta.appearing_method_ids.contains_key(&lowercase_method))
237 }
238
239 #[inline]
242 #[must_use]
243 pub fn property_exists(&self, class: &[u8], property: &[u8]) -> bool {
244 let lowercase_class = ascii_lowercase_word(class);
245 let property_name = word(property);
246 self.class_likes
247 .get(&lowercase_class)
248 .is_some_and(|meta| meta.appearing_property_ids.contains_key(&property_name))
249 }
250
251 #[inline]
254 #[must_use]
255 pub fn class_constant_exists(&self, class: &[u8], constant: &[u8]) -> bool {
256 let lowercase_class = ascii_lowercase_word(class);
257 let constant_name = word(constant);
258 self.class_likes.get(&lowercase_class).is_some_and(|meta| {
259 meta.constants.contains_key(&constant_name) || meta.enum_cases.contains_key(&constant_name)
260 })
261 }
262
263 #[inline]
265 #[must_use]
266 pub fn method_is_declared_in_class(&self, class: &[u8], method: &[u8]) -> bool {
267 let lowercase_class = ascii_lowercase_word(class);
268 let lowercase_method = ascii_lowercase_word(method);
269 self.class_likes
270 .get(&lowercase_class)
271 .and_then(|meta| meta.declaring_method_ids.get(&lowercase_method))
272 .is_some_and(|method_id| method_id.get_class_name() == lowercase_class)
273 }
274
275 #[inline]
277 #[must_use]
278 pub fn property_is_declared_in_class(&self, class: &[u8], property: &[u8]) -> bool {
279 let lowercase_class = ascii_lowercase_word(class);
280 let property_name = word(property);
281 self.class_likes.get(&lowercase_class).is_some_and(|meta| meta.properties.contains_key(&property_name))
282 }
283
284 #[inline]
287 #[must_use]
288 pub fn get_class(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
289 let lowercase_name = ascii_lowercase_word(name);
290 if self.symbols.contains_class(lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
291 }
292
293 #[inline]
295 #[must_use]
296 pub fn get_interface(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
297 let lowercase_name = ascii_lowercase_word(name);
298 if self.symbols.contains_interface(lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
299 }
300
301 #[inline]
303 #[must_use]
304 pub fn get_trait(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
305 let lowercase_name = ascii_lowercase_word(name);
306 if self.symbols.contains_trait(lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
307 }
308
309 #[inline]
311 #[must_use]
312 pub fn get_enum(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
313 let lowercase_name = ascii_lowercase_word(name);
314 if self.symbols.contains_enum(lowercase_name) { self.class_likes.get(&lowercase_name) } else { None }
315 }
316
317 #[inline]
319 #[must_use]
320 pub fn get_class_like(&self, name: &[u8]) -> Option<&ClassLikeMetadata> {
321 let lowercase_name = ascii_lowercase_word(name);
322 self.class_likes.get(&lowercase_name)
323 }
324
325 #[inline]
327 #[must_use]
328 pub fn get_function(&self, name: &[u8]) -> Option<&FunctionLikeMetadata> {
329 let lowercase_name = ascii_lowercase_word(name);
330 let identifier = (empty_word(), lowercase_name);
331 self.function_likes.get(&identifier)
332 }
333
334 #[inline]
336 #[must_use]
337 pub fn get_method(&self, class: &[u8], method: &[u8]) -> Option<&FunctionLikeMetadata> {
338 let lowercase_class = ascii_lowercase_word(class);
339 let lowercase_method = ascii_lowercase_word(method);
340 let identifier = (lowercase_class, lowercase_method);
341 self.function_likes.get(&identifier)
342 }
343
344 #[inline]
347 #[must_use]
348 pub fn get_closure(&self, synthetic_name: &Word) -> Option<&FunctionLikeMetadata> {
349 self.function_likes.get(&(empty_word(), *synthetic_name))
350 }
351
352 #[inline]
356 #[must_use]
357 pub fn get_closure_at(&self, file: &File, span: Span) -> Option<&FunctionLikeMetadata> {
358 let name = crate::build_synthetic_name("closure", file, span);
359 self.get_closure(&name)
360 }
361
362 #[inline]
364 #[must_use]
365 pub fn get_method_by_id(&self, method_id: &MethodIdentifier) -> Option<&FunctionLikeMetadata> {
366 let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
367 let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
368 let identifier = (lowercase_class, lowercase_method);
369 self.function_likes.get(&identifier)
370 }
371
372 #[inline]
375 #[must_use]
376 pub fn get_declaring_method(&self, class: &[u8], method: &[u8]) -> Option<&FunctionLikeMetadata> {
377 let method_id = MethodIdentifier::new(word(class), word(method));
378 let declaring_method_id = self.get_declaring_method_identifier(&method_id);
379 self.get_method(
380 declaring_method_id.get_class_name().as_bytes(),
381 declaring_method_id.get_method_name().as_bytes(),
382 )
383 }
384
385 #[inline]
388 #[must_use]
389 pub fn get_function_like(
390 &self,
391 identifier: &crate::identifier::function_like::FunctionLikeIdentifier,
392 ) -> Option<&FunctionLikeMetadata> {
393 use crate::identifier::function_like::FunctionLikeIdentifier;
394 match identifier {
395 FunctionLikeIdentifier::Function(name) => self.get_function(name.as_bytes()),
396 FunctionLikeIdentifier::Method(class, method) => self.get_method(class.as_bytes(), method.as_bytes()),
397 FunctionLikeIdentifier::Closure(name) => self.get_closure(name),
398 }
399 }
400
401 #[inline]
404 #[must_use]
405 pub fn get_constant(&self, name: &[u8]) -> Option<&ConstantMetadata> {
406 let lowercase_name = ascii_lowercase_constant_name_word(name);
407 self.constants.get(&lowercase_name)
408 }
409
410 #[inline]
417 #[must_use]
418 pub fn span_of(&self, name: &[u8]) -> Option<Span> {
419 if let Some(meta) = self.get_class_like(name) {
420 return Some(meta.name_span.unwrap_or(meta.span));
421 }
422
423 if let Some(meta) = self.get_function(name) {
424 return Some(meta.name_span.unwrap_or(meta.span));
425 }
426
427 self.get_constant(name).map(|meta| meta.span)
428 }
429
430 #[inline]
433 #[must_use]
434 pub fn get_class_constant(&self, class: &[u8], constant: &[u8]) -> Option<&ClassLikeConstantMetadata> {
435 let lowercase_class = ascii_lowercase_word(class);
436 let constant_name = word(constant);
437 self.class_likes.get(&lowercase_class).and_then(|meta| meta.constants.get(&constant_name))
438 }
439
440 #[inline]
442 #[must_use]
443 pub fn get_enum_case(&self, class: &[u8], case: &[u8]) -> Option<&EnumCaseMetadata> {
444 let lowercase_class = ascii_lowercase_word(class);
445 let case_name = word(case);
446 self.class_likes.get(&lowercase_class).and_then(|meta| meta.enum_cases.get(&case_name))
447 }
448
449 #[inline]
452 #[must_use]
453 pub fn get_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
454 let lowercase_class = ascii_lowercase_word(class);
455 let property_name = word(property);
456 self.class_likes.get(&lowercase_class)?.properties.get(&property_name)
457 }
458
459 #[inline]
461 #[must_use]
462 pub fn get_declaring_property(&self, class: &[u8], property: &[u8]) -> Option<&PropertyMetadata> {
463 let lowercase_class = ascii_lowercase_word(class);
464 let property_name = word(property);
465 let declaring_class = self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name)?;
466 self.class_likes.get(declaring_class)?.properties.get(&property_name)
467 }
468 #[inline]
472 #[must_use]
473 pub fn get_property_type(&self, class: &[u8], property: &[u8]) -> Option<&TUnion> {
474 let lowercase_class = ascii_lowercase_word(class);
475 let property_name = word(property);
476 let declaring_class = self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name)?;
477 let property_meta = self.class_likes.get(declaring_class)?.properties.get(&property_name)?;
478 property_meta.type_metadata.as_ref().map(|tm| &tm.type_union)
479 }
480
481 #[must_use]
483 pub fn get_class_constant_type<'meta>(&'meta self, class: &[u8], constant: &[u8]) -> Option<Cow<'meta, TUnion>> {
484 let lowercase_class = ascii_lowercase_word(class);
485 let constant_name = word(constant);
486 let class_meta = self.class_likes.get(&lowercase_class)?;
487
488 if class_meta.kind.is_enum() && class_meta.enum_cases.contains_key(&constant_name) {
490 let atomic = TAtomic::Object(TObject::new_enum_case(class_meta.original_name, constant_name));
491 return Some(Cow::Owned(TUnion::from_atomic(atomic)));
492 }
493
494 let constant_meta = class_meta.constants.get(&constant_name)?;
496
497 if let Some(type_meta) = constant_meta.type_metadata.as_ref() {
499 return Some(Cow::Borrowed(&type_meta.type_union));
500 }
501
502 constant_meta.inferred_type.as_ref().map(|atomic| Cow::Owned(TUnion::from_atomic(atomic.clone())))
504 }
505
506 #[inline]
508 #[must_use]
509 pub fn get_class_constant_literal_value(&self, class: &[u8], constant: &[u8]) -> Option<&TAtomic> {
510 let lowercase_class = ascii_lowercase_word(class);
511 let constant_name = word(constant);
512 self.class_likes
513 .get(&lowercase_class)
514 .and_then(|meta| meta.constants.get(&constant_name))
515 .and_then(|constant_meta| constant_meta.inferred_type.as_ref())
516 }
517 #[inline]
521 #[must_use]
522 pub fn class_extends(&self, child: &[u8], parent: &[u8]) -> bool {
523 let lowercase_child = ascii_lowercase_word(child);
524 let lowercase_parent = ascii_lowercase_word(parent);
525 self.class_likes.get(&lowercase_child).is_some_and(|meta| meta.all_parent_classes.contains(&lowercase_parent))
526 }
527
528 #[inline]
530 #[must_use]
531 pub fn class_directly_extends(&self, child: &[u8], parent: &[u8]) -> bool {
532 let lowercase_child = ascii_lowercase_word(child);
533 let lowercase_parent = ascii_lowercase_word(parent);
534 self.class_likes
535 .get(&lowercase_child)
536 .is_some_and(|meta| meta.direct_parent_class.as_ref() == Some(&lowercase_parent))
537 }
538
539 #[inline]
541 #[must_use]
542 pub fn class_implements(&self, class: &[u8], interface: &[u8]) -> bool {
543 let lowercase_class = ascii_lowercase_word(class);
544 let lowercase_interface = ascii_lowercase_word(interface);
545 self.class_likes
546 .get(&lowercase_class)
547 .is_some_and(|meta| meta.all_parent_interfaces.contains(&lowercase_interface))
548 }
549
550 #[inline]
552 #[must_use]
553 pub fn class_directly_implements(&self, class: &[u8], interface: &[u8]) -> bool {
554 let lowercase_class = ascii_lowercase_word(class);
555 let lowercase_interface = ascii_lowercase_word(interface);
556 self.class_likes
557 .get(&lowercase_class)
558 .is_some_and(|meta| meta.direct_parent_interfaces.contains(&lowercase_interface))
559 }
560
561 #[inline]
563 #[must_use]
564 pub fn class_uses_trait(&self, class: &[u8], trait_name: &[u8]) -> bool {
565 let lowercase_class = ascii_lowercase_word(class);
566 let lowercase_trait = ascii_lowercase_word(trait_name);
567 self.class_likes.get(&lowercase_class).is_some_and(|meta| meta.used_traits.contains(&lowercase_trait))
568 }
569
570 #[inline]
573 #[must_use]
574 pub fn trait_requires_extends(&self, trait_name: &[u8], class_name: &[u8]) -> bool {
575 let lowercase_trait = ascii_lowercase_word(trait_name);
576
577 self.class_likes.get(&lowercase_trait).is_some_and(|meta| {
578 meta.require_extends.iter().any(|required| self.is_instance_of(class_name, required.as_bytes()))
579 })
580 }
581
582 #[inline]
584 #[must_use]
585 pub fn is_instance_of(&self, child: &[u8], parent: &[u8]) -> bool {
586 if child == parent {
587 return true;
588 }
589
590 let lowercase_child = ascii_lowercase_word(child);
591 let lowercase_parent = ascii_lowercase_word(parent);
592
593 if lowercase_child == lowercase_parent {
594 return true;
595 }
596
597 self.class_likes.get(&lowercase_child).is_some_and(|meta| {
598 meta.all_parent_classes.contains(&lowercase_parent)
599 || meta.all_parent_interfaces.contains(&lowercase_parent)
600 || meta.used_traits.contains(&lowercase_parent)
601 || meta.require_extends.contains(&lowercase_parent)
602 || meta.require_implements.contains(&lowercase_parent)
603 })
604 }
605
606 #[inline]
608 #[must_use]
609 pub fn is_enum_or_final_class(&self, name: &[u8]) -> bool {
610 let lowercase_name = ascii_lowercase_word(name);
611 self.class_likes.get(&lowercase_name).is_some_and(|meta| meta.kind.is_enum() || meta.flags.is_final())
612 }
613
614 #[inline]
617 #[must_use]
618 pub fn is_inheritable(&self, name: &[u8]) -> bool {
619 let lowercase_name = ascii_lowercase_word(name);
620 match self.symbols.get_kind(lowercase_name) {
621 Some(SymbolKind::Class) => self.class_likes.get(&lowercase_name).is_some_and(|meta| !meta.flags.is_final()),
622 Some(SymbolKind::Enum) => false,
623 Some(SymbolKind::Interface | SymbolKind::Trait) | None => true,
624 }
625 }
626
627 #[inline]
629 #[must_use]
630 pub fn get_class_descendants(&self, class: &[u8]) -> WordSet {
631 let lowercase_class = ascii_lowercase_word(class);
632 let mut all_descendants = WordSet::default();
633 let mut queue = vec![&lowercase_class];
634 let mut visited = WordSet::default();
635 visited.insert(lowercase_class);
636
637 while let Some(current_name) = queue.pop() {
638 if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
639 for descendant in direct_descendants {
640 if visited.insert(*descendant) {
641 all_descendants.insert(*descendant);
642 queue.push(descendant);
643 }
644 }
645 }
646 }
647
648 all_descendants
649 }
650
651 #[inline]
653 #[must_use]
654 pub fn get_class_ancestors(&self, class: &[u8]) -> WordSet {
655 let lowercase_class = ascii_lowercase_word(class);
656 let mut ancestors = WordSet::default();
657 if let Some(meta) = self.class_likes.get(&lowercase_class) {
658 ancestors.extend(meta.all_parent_classes.iter().copied());
659 ancestors.extend(meta.all_parent_interfaces.iter().copied());
660 }
661 ancestors
662 }
663
664 #[inline]
666 #[must_use]
667 pub fn get_declaring_method_class(&self, class: &[u8], method: &[u8]) -> Option<Word> {
668 let lowercase_class = ascii_lowercase_word(class);
669 let lowercase_method = ascii_lowercase_word(method);
670
671 self.class_likes
672 .get(&lowercase_class)?
673 .declaring_method_ids
674 .get(&lowercase_method)
675 .map(|method_id| method_id.get_class_name())
676 }
677
678 #[inline]
680 #[must_use]
681 pub fn get_appearing_method_class(&self, class: &[u8], method: &[u8]) -> Option<Word> {
682 let lowercase_class = ascii_lowercase_word(class);
683 let lowercase_method = ascii_lowercase_word(method);
684 self.class_likes
685 .get(&lowercase_class)?
686 .appearing_method_ids
687 .get(&lowercase_method)
688 .map(|method_id| method_id.get_class_name())
689 }
690
691 #[must_use]
693 pub fn get_declaring_method_identifier(&self, method_id: &MethodIdentifier) -> MethodIdentifier {
694 let lowercase_class = ascii_lowercase_word(method_id.get_class_name().as_bytes());
695 let lowercase_method = ascii_lowercase_word(method_id.get_method_name().as_bytes());
696
697 let Some(class_meta) = self.class_likes.get(&lowercase_class) else {
698 return *method_id;
699 };
700
701 if let Some(declaring_method_id) = class_meta.declaring_method_ids.get(&lowercase_method) {
702 return *declaring_method_id;
703 }
704
705 if class_meta.flags.is_abstract()
706 && let Some(overridden_map) = class_meta.overridden_method_ids.get(&lowercase_method)
707 && let Some((_, first_method_id)) = overridden_map.first()
708 {
709 return *first_method_id;
710 }
711
712 *method_id
713 }
714
715 #[inline]
717 #[must_use]
718 pub fn method_is_overriding(&self, class: &[u8], method: &[u8]) -> bool {
719 let lowercase_class = ascii_lowercase_word(class);
720 let lowercase_method = ascii_lowercase_word(method);
721 self.class_likes
722 .get(&lowercase_class)
723 .is_some_and(|meta| meta.overridden_method_ids.contains_key(&lowercase_method))
724 }
725
726 #[inline]
728 #[must_use]
729 pub fn method_is_abstract(&self, class: &[u8], method: &[u8]) -> bool {
730 let lowercase_class = ascii_lowercase_word(class);
731 let lowercase_method = ascii_lowercase_word(method);
732 let identifier = (lowercase_class, lowercase_method);
733 self.function_likes
734 .get(&identifier)
735 .and_then(|meta| meta.method_metadata.as_ref())
736 .is_some_and(|method_meta| method_meta.is_abstract)
737 }
738
739 #[inline]
741 #[must_use]
742 pub fn method_is_static(&self, class: &[u8], method: &[u8]) -> bool {
743 let lowercase_class = ascii_lowercase_word(class);
744 let lowercase_method = ascii_lowercase_word(method);
745 let identifier = (lowercase_class, lowercase_method);
746 self.function_likes
747 .get(&identifier)
748 .and_then(|meta| meta.method_metadata.as_ref())
749 .is_some_and(|method_meta| method_meta.is_static)
750 }
751
752 #[inline]
754 #[must_use]
755 pub fn method_is_final(&self, class: &[u8], method: &[u8]) -> bool {
756 let lowercase_class = ascii_lowercase_word(class);
757 let lowercase_method = ascii_lowercase_word(method);
758 let identifier = (lowercase_class, lowercase_method);
759 self.function_likes
760 .get(&identifier)
761 .and_then(|meta| meta.method_metadata.as_ref())
762 .is_some_and(|method_meta| method_meta.is_final)
763 }
764
765 #[inline]
771 #[must_use]
772 pub fn get_method_visibility(&self, class: &[u8], method: &[u8]) -> Option<Visibility> {
773 let lowercase_class = ascii_lowercase_word(class);
774 let lowercase_method = ascii_lowercase_word(method);
775
776 if let Some(class_meta) = self.class_likes.get(&lowercase_class)
778 && let Some(overridden_visibility) = class_meta.trait_visibility_map.get(&lowercase_method)
779 {
780 return Some(*overridden_visibility);
781 }
782
783 let declaring_class = self.get_declaring_method_class(class, method)?;
785 let identifier = (declaring_class, lowercase_method);
786
787 self.function_likes
788 .get(&identifier)
789 .and_then(|meta| meta.method_metadata.as_ref())
790 .map(|method_meta| method_meta.visibility)
791 }
792
793 #[must_use]
795 pub fn get_function_like_thrown_types<'meta>(
796 &'meta self,
797 class_like: Option<&'meta ClassLikeMetadata>,
798 function_like: &'meta FunctionLikeMetadata,
799 ) -> &'meta [TypeMetadata] {
800 if !function_like.thrown_types.is_empty() {
801 return function_like.thrown_types.as_slice();
802 }
803
804 if !function_like.kind.is_method() {
805 return &[];
806 }
807
808 let Some(class_like) = class_like else {
809 return &[];
810 };
811
812 let method_name = &function_like.name;
813
814 if let Some(overridden_map) = class_like.overridden_method_ids.get(method_name) {
815 for (parent_class_name, parent_method_id) in overridden_map {
816 if class_like.name.as_bytes().eq_ignore_ascii_case(parent_class_name.as_bytes()) {
817 continue; }
819
820 let Some(parent_class) = self.class_likes.get(parent_class_name) else {
821 continue;
822 };
823
824 let parent_method_key = (parent_method_id.get_class_name(), parent_method_id.get_method_name());
825 if let Some(parent_method) = self.function_likes.get(&parent_method_key) {
826 let thrown = self.get_function_like_thrown_types(Some(parent_class), parent_method);
827 if !thrown.is_empty() {
828 return thrown;
829 }
830 }
831 }
832 }
833
834 &[]
835 }
836
837 #[inline]
839 #[must_use]
840 pub fn get_declaring_property_class(&self, class: &[u8], property: &[u8]) -> Option<Word> {
841 let lowercase_class = ascii_lowercase_word(class);
842 let property_name = word(property);
843 self.class_likes.get(&lowercase_class)?.declaring_property_ids.get(&property_name).copied()
844 }
845
846 #[inline]
848 #[must_use]
849 pub fn get_appearing_property_class(&self, class: &[u8], property: &[u8]) -> Option<Word> {
850 let lowercase_class = ascii_lowercase_word(class);
851 let property_name = word(property);
852 self.class_likes.get(&lowercase_class)?.appearing_property_ids.get(&property_name).copied()
853 }
854
855 #[must_use]
857 pub fn get_all_descendants(&self, class: &[u8]) -> WordSet {
858 let lowercase_class = ascii_lowercase_word(class);
859 let mut all_descendants = WordSet::default();
860 let mut queue = vec![&lowercase_class];
861 let mut visited = WordSet::default();
862 visited.insert(lowercase_class);
863
864 while let Some(current_name) = queue.pop() {
865 if let Some(direct_descendants) = self.direct_classlike_descendants.get(current_name) {
866 for descendant in direct_descendants {
867 if visited.insert(*descendant) {
868 all_descendants.insert(*descendant);
869 queue.push(descendant);
870 }
871 }
872 }
873 }
874
875 all_descendants
876 }
877
878 #[must_use]
881 pub fn get_anonymous_class_name(file: &File, span: Span) -> Word {
882 crate::get_anonymous_class_name(file, span)
883 }
884
885 #[must_use]
888 pub fn get_anonymous_class(&self, file: &File, span: Span) -> Option<&ClassLikeMetadata> {
889 let name = Self::get_anonymous_class_name(file, span);
890 self.get_class_like(name.as_bytes())
891 }
892
893 #[inline]
903 #[must_use]
904 pub fn get_file_signature(&self, file_id: &FileId) -> Option<&FileSignature> {
905 self.file_signatures.get(file_id)
906 }
907
908 #[inline]
919 pub fn set_file_signature(&mut self, file_id: FileId, signature: FileSignature) -> Option<FileSignature> {
920 self.file_signatures.insert(file_id, signature)
921 }
922
923 #[inline]
933 pub fn remove_file_signature(&mut self, file_id: &FileId) -> Option<FileSignature> {
934 self.file_signatures.remove(file_id)
935 }
936
937 pub fn mark_safe_symbols(&mut self, diff: &CodebaseDiff, references: &SymbolReferences) -> Option<bool> {
952 let (invalid_symbols, partially_invalid) = references.get_invalid_symbols(diff)?;
953
954 for keep_symbol in diff.get_keep() {
956 if !invalid_symbols.contains(keep_symbol) {
957 if keep_symbol.1.is_empty() {
958 if !partially_invalid.contains(&keep_symbol.0) {
960 self.safe_symbols.insert(keep_symbol.0);
961 }
962 } else {
963 self.safe_symbol_members.insert(*keep_symbol);
965 }
966 }
967 }
968
969 Some(invalid_symbols.contains(&(empty_word(), empty_word())))
970 }
971
972 pub fn extend(&mut self, other: CodebaseMetadata) {
977 for (k, mut v) in other.class_likes {
978 match self.class_likes.entry(k) {
979 Entry::Occupied(mut entry) => {
980 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
981 v.version_constraint.merge(entry.get().version_constraint.clone());
982 entry.insert(v);
983 } else {
984 entry.get_mut().version_constraint.merge(v.version_constraint);
985 }
986 }
987 Entry::Vacant(entry) => {
988 entry.insert(v);
989 }
990 }
991 }
992
993 for (k, mut v) in other.function_likes {
994 match self.function_likes.entry(k) {
995 Entry::Occupied(mut entry) => {
996 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
997 v.version_constraint.merge(entry.get().version_constraint.clone());
998 entry.insert(v);
999 } else {
1000 entry.get_mut().version_constraint.merge(v.version_constraint);
1001 }
1002 }
1003 Entry::Vacant(entry) => {
1004 entry.insert(v);
1005 }
1006 }
1007 }
1008
1009 for (k, mut v) in other.constants {
1010 match self.constants.entry(k) {
1011 Entry::Occupied(mut entry) => {
1012 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1013 v.version_constraint.merge(entry.get().version_constraint.clone());
1014 entry.insert(v);
1015 } else {
1016 entry.get_mut().version_constraint.merge(v.version_constraint);
1017 }
1018 }
1019 Entry::Vacant(entry) => {
1020 entry.insert(v);
1021 }
1022 }
1023 }
1024
1025 self.symbols.extend(other.symbols);
1026
1027 for (k, v) in other.all_class_like_descendants {
1028 self.all_class_like_descendants.entry(k).or_default().extend(v);
1029 }
1030
1031 for (k, v) in other.direct_classlike_descendants {
1032 self.direct_classlike_descendants.entry(k).or_default().extend(v);
1033 }
1034
1035 self.file_signatures.extend(other.file_signatures);
1036 self.safe_symbols.extend(other.safe_symbols);
1037 self.safe_symbol_members.extend(other.safe_symbol_members);
1038 self.infer_types_from_usage |= other.infer_types_from_usage;
1039 self.merge_patch_class_likes(other.patch_class_likes);
1040 self.merge_patch_function_likes(other.patch_function_likes);
1041 self.merge_patch_constants(other.patch_constants);
1042 }
1043
1044 pub fn extend_ref(&mut self, other: &CodebaseMetadata) {
1050 for (k, v) in &other.class_likes {
1051 match self.class_likes.entry(*k) {
1052 Entry::Occupied(mut entry) => {
1053 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1054 let mut new = v.clone();
1055 new.version_constraint.merge(entry.get().version_constraint.clone());
1056 entry.insert(new);
1057 } else {
1058 entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1059 }
1060 }
1061 Entry::Vacant(entry) => {
1062 entry.insert(v.clone());
1063 }
1064 }
1065 }
1066
1067 for (k, v) in &other.function_likes {
1068 match self.function_likes.entry(*k) {
1069 Entry::Occupied(mut entry) => {
1070 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1071 let mut new = v.clone();
1072 new.version_constraint.merge(entry.get().version_constraint.clone());
1073 entry.insert(new);
1074 } else {
1075 entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1076 }
1077 }
1078 Entry::Vacant(entry) => {
1079 entry.insert(v.clone());
1080 }
1081 }
1082 }
1083
1084 for (k, v) in &other.constants {
1085 match self.constants.entry(*k) {
1086 Entry::Occupied(mut entry) => {
1087 if should_replace_metadata(entry.get().flags, entry.get().span, v.flags, v.span) {
1088 let mut new = v.clone();
1089 new.version_constraint.merge(entry.get().version_constraint.clone());
1090 entry.insert(new);
1091 } else {
1092 entry.get_mut().version_constraint.merge(v.version_constraint.clone());
1093 }
1094 }
1095 Entry::Vacant(entry) => {
1096 entry.insert(v.clone());
1097 }
1098 }
1099 }
1100
1101 self.symbols.extend_ref(&other.symbols);
1102
1103 for (k, v) in &other.all_class_like_descendants {
1104 self.all_class_like_descendants.entry(*k).or_default().extend(v.iter().copied());
1105 }
1106
1107 for (k, v) in &other.direct_classlike_descendants {
1108 self.direct_classlike_descendants.entry(*k).or_default().extend(v.iter().copied());
1109 }
1110
1111 for (k, v) in &other.file_signatures {
1112 self.file_signatures.insert(*k, v.clone());
1113 }
1114 self.safe_symbols.extend(other.safe_symbols.iter().copied());
1115 self.safe_symbol_members.extend(other.safe_symbol_members.iter().copied());
1116 self.infer_types_from_usage |= other.infer_types_from_usage;
1117 self.merge_patch_class_likes(other.patch_class_likes.iter().map(|(k, v)| (*k, v.clone())));
1118 self.merge_patch_function_likes(other.patch_function_likes.iter().map(|(k, v)| (*k, v.clone())));
1119 self.merge_patch_constants(other.patch_constants.iter().map(|(k, v)| (*k, v.clone())));
1120 }
1121
1122 fn merge_patch_class_likes(&mut self, incoming: impl IntoIterator<Item = (Word, ClassLikeMetadata)>) {
1129 for (k, v) in incoming {
1130 match self.patch_class_likes.entry(k) {
1131 Entry::Occupied(mut entry) => {
1132 let diagnostic = duplicate_patch_class_diagnostic(entry.get(), &v);
1133 entry.get_mut().issues.push(diagnostic);
1134 }
1135 Entry::Vacant(entry) => {
1136 entry.insert(v);
1137 }
1138 }
1139 }
1140 }
1141
1142 fn merge_patch_function_likes(&mut self, incoming: impl IntoIterator<Item = ((Word, Word), FunctionLikeMetadata)>) {
1146 for (k, v) in incoming {
1147 match self.patch_function_likes.entry(k) {
1148 Entry::Occupied(mut entry) => {
1149 if k.0.is_empty() {
1150 let diagnostic = duplicate_patch_function_diagnostic(entry.get(), &v);
1151 entry.get_mut().issues.push(diagnostic);
1152 }
1153 }
1154 Entry::Vacant(entry) => {
1155 entry.insert(v);
1156 }
1157 }
1158 }
1159 }
1160
1161 fn merge_patch_constants(&mut self, incoming: impl IntoIterator<Item = (Word, ConstantMetadata)>) {
1163 for (k, v) in incoming {
1164 match self.patch_constants.entry(k) {
1165 Entry::Occupied(mut entry) => {
1166 let diagnostic = duplicate_patch_constant_diagnostic(entry.get(), &v);
1167 entry.get_mut().issues.push(diagnostic);
1168 }
1169 Entry::Vacant(entry) => {
1170 entry.insert(v);
1171 }
1172 }
1173 }
1174 }
1175
1176 pub fn convert_partial_to_patch(&mut self) {
1183 for (k, v) in std::mem::take(&mut self.class_likes) {
1184 self.patch_class_likes.insert(k, v);
1185 }
1186
1187 for (k, v) in std::mem::take(&mut self.function_likes) {
1188 self.patch_function_likes.insert(k, v);
1189 }
1190
1191 for (k, v) in std::mem::take(&mut self.constants) {
1192 self.patch_constants.insert(k, v);
1193 }
1194
1195 self.symbols = Symbols::new();
1196 self.all_class_like_descendants.clear();
1197 self.direct_classlike_descendants.clear();
1198 }
1199
1200 pub fn apply_patches_pass(&mut self) {
1210 let mut inherited_overrides: HashSet<(Word, Word)> = HashSet::default();
1214
1215 let class_keys: Vec<Word> = self.patch_class_likes.keys().copied().collect();
1216 for fqcn in class_keys {
1217 let Some(target) = self.class_likes.get(&fqcn) else {
1218 if let Some(p) = self.patch_class_likes.get_mut(&fqcn) {
1219 let diag = orphan_patch_class_diagnostic(p);
1220 p.issues.push(diag);
1221 }
1222 continue;
1223 };
1224 if target.flags.is_user_defined() {
1227 continue;
1228 }
1229
1230 let mut working = target.clone();
1231 let inherited =
1232 collect_inherited_patch_methods(&working, &self.patch_class_likes[&fqcn], &self.class_likes);
1233 inherited_overrides.extend(inherited.iter().map(|method| (fqcn, *method)));
1234 if let Some(patch_entry) = self.patch_class_likes.get_mut(&fqcn) {
1235 working.apply_patch(patch_entry, &inherited);
1236 }
1237 self.class_likes.insert(fqcn, working);
1238 }
1239
1240 let func_keys: Vec<(Word, Word)> = self.patch_function_likes.keys().copied().collect();
1241 for key in func_keys {
1242 let Some(target) = self.function_likes.get(&key) else {
1243 if inherited_overrides.contains(&key) {
1244 if let Some(p) = self.patch_function_likes.get(&key) {
1249 let materialized = p.clone();
1250 self.function_likes.insert(key, materialized);
1251 }
1252 continue;
1253 }
1254 if key.0.is_empty()
1257 && let Some(p) = self.patch_function_likes.get_mut(&key)
1258 {
1259 let diag = orphan_patch_function_diagnostic(p);
1260 p.issues.push(diag);
1261 }
1262 continue;
1263 };
1264 if target.flags.is_user_defined() {
1265 continue;
1266 }
1267
1268 let mut working = target.clone();
1269 if let Some(patch_entry) = self.patch_function_likes.get_mut(&key) {
1270 working.apply_patch(patch_entry);
1271 }
1272 self.function_likes.insert(key, working);
1273 }
1274
1275 let const_keys: Vec<Word> = self.patch_constants.keys().copied().collect();
1276 for fqcn in const_keys {
1277 let Some(target) = self.constants.get(&fqcn) else {
1278 if let Some(p) = self.patch_constants.get_mut(&fqcn) {
1279 let diag = orphan_patch_constant_diagnostic(p);
1280 p.issues.push(diag);
1281 }
1282 continue;
1283 };
1284 if target.flags.is_user_defined() {
1285 continue;
1286 }
1287
1288 let mut working = target.clone();
1289 if let Some(patch_entry) = self.patch_constants.get(&fqcn) {
1290 working.apply_patch(patch_entry);
1291 }
1292 self.constants.insert(fqcn, working);
1293 }
1294 }
1295
1296 pub fn remove_entries_of(&mut self, file_metadata: &CodebaseMetadata) {
1307 for k in file_metadata.class_likes.keys() {
1308 self.class_likes.remove(k);
1309 }
1310
1311 for k in file_metadata.function_likes.keys() {
1312 self.function_likes.remove(k);
1313 }
1314
1315 for k in file_metadata.constants.keys() {
1316 self.constants.remove(k);
1317 }
1318
1319 for k in file_metadata.class_likes.keys() {
1322 self.symbols.remove(*k);
1323 }
1324
1325 for k in file_metadata.file_signatures.keys() {
1326 self.file_signatures.remove(k);
1327 }
1328
1329 let removed_files: HashSet<FileId> = file_metadata.file_signatures.keys().copied().collect();
1332 self.patch_class_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1333 self.patch_function_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1334 self.patch_constants.retain(|_, m| !removed_files.contains(&m.span.file_id));
1335 }
1336
1337 #[must_use]
1342 pub fn extract_keys(&self) -> CodebaseEntryKeys {
1343 CodebaseEntryKeys {
1344 class_like_names: self.class_likes.keys().copied().collect(),
1345 function_like_keys: self.function_likes.keys().copied().collect(),
1346 constant_names: self.constants.keys().copied().collect(),
1347 file_ids: self.file_signatures.keys().copied().collect(),
1348 }
1349 }
1350
1351 #[must_use]
1367 pub fn extract_owned_keys(&self, merged: &CodebaseMetadata) -> CodebaseEntryKeys {
1368 let class_like_names = self
1369 .class_likes
1370 .iter()
1371 .filter(|(name, meta)| merged.class_likes.get(*name).is_some_and(|m| m.span == meta.span))
1372 .map(|(name, _)| *name)
1373 .collect();
1374
1375 let function_like_keys = self
1376 .function_likes
1377 .iter()
1378 .filter(|(key, meta)| merged.function_likes.get(*key).is_some_and(|m| m.span == meta.span))
1379 .map(|(key, _)| *key)
1380 .collect();
1381
1382 let constant_names = self
1383 .constants
1384 .iter()
1385 .filter(|(name, meta)| merged.constants.get(*name).is_some_and(|m| m.span == meta.span))
1386 .map(|(name, _)| *name)
1387 .collect();
1388
1389 let file_ids = self.file_signatures.keys().copied().collect();
1391
1392 CodebaseEntryKeys { class_like_names, function_like_keys, constant_names, file_ids }
1393 }
1394
1395 pub fn remove_entries_by_keys(&mut self, keys: &CodebaseEntryKeys) {
1400 for k in &keys.class_like_names {
1401 self.class_likes.remove(k);
1402 self.symbols.remove(*k);
1403 }
1404
1405 for k in &keys.function_like_keys {
1406 self.function_likes.remove(k);
1407 }
1408
1409 for k in &keys.constant_names {
1410 self.constants.remove(k);
1411 }
1412
1413 for k in &keys.file_ids {
1414 self.file_signatures.remove(k);
1415 }
1416
1417 let removed_files: HashSet<FileId> = keys.file_ids.iter().copied().collect();
1420 self.patch_class_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1421 self.patch_function_likes.retain(|_, m| !removed_files.contains(&m.span.file_id));
1422 self.patch_constants.retain(|_, m| !removed_files.contains(&m.span.file_id));
1423 }
1424
1425 pub fn take_issues(&mut self, user_defined: bool) -> IssueCollection {
1427 let mut issues = IssueCollection::new();
1428
1429 for meta in self.class_likes.values_mut() {
1430 if user_defined && !meta.flags.is_user_defined() {
1431 continue;
1432 }
1433 issues.extend(meta.take_issues());
1434 }
1435
1436 for meta in self.function_likes.values_mut() {
1437 if user_defined && !meta.flags.is_user_defined() {
1438 continue;
1439 }
1440 issues.extend(meta.take_issues());
1441 }
1442
1443 for meta in self.constants.values_mut() {
1444 if user_defined && !meta.flags.is_user_defined() {
1445 continue;
1446 }
1447 issues.extend(meta.take_issues());
1448 }
1449
1450 for meta in self.patch_class_likes.values_mut() {
1454 issues.extend(meta.take_issues());
1455 }
1456
1457 for meta in self.patch_function_likes.values_mut() {
1458 issues.extend(meta.take_issues());
1459 }
1460
1461 for meta in self.patch_constants.values_mut() {
1462 issues.extend(meta.take_issues());
1463 }
1464
1465 issues
1466 }
1467
1468 #[must_use]
1472 pub fn get_all_file_ids(&self) -> Vec<FileId> {
1473 self.file_signatures.keys().copied().collect()
1474 }
1475}
1476
1477impl Default for CodebaseMetadata {
1478 #[inline]
1479 fn default() -> Self {
1480 Self {
1481 class_likes: WordMap::default(),
1482 function_likes: HashMap::default(),
1483 symbols: Symbols::new(),
1484 infer_types_from_usage: false,
1485 constants: WordMap::default(),
1486 all_class_like_descendants: WordMap::default(),
1487 direct_classlike_descendants: WordMap::default(),
1488 safe_symbols: WordSet::default(),
1489 safe_symbol_members: HashSet::default(),
1490 file_signatures: HashMap::default(),
1491 patch_class_likes: WordMap::default(),
1492 patch_function_likes: HashMap::default(),
1493 patch_constants: WordMap::default(),
1494 }
1495 }
1496}
1497
1498fn collect_inherited_patch_methods(
1503 target: &ClassLikeMetadata,
1504 patch: &ClassLikeMetadata,
1505 class_likes: &WordMap<ClassLikeMetadata>,
1506) -> WordSet {
1507 if patch.methods.is_empty() {
1508 return WordSet::default();
1509 }
1510 let ancestor_methods = class_like::collect_ancestor_methods(target, class_likes);
1511 patch.methods.iter().filter(|m| ancestor_methods.contains(*m)).copied().collect()
1512}
1513
1514fn duplicate_patch_class_diagnostic(kept: &ClassLikeMetadata, dropped: &ClassLikeMetadata) -> Issue {
1515 Issue::error(format!(
1516 "Multiple patches target `{}`; at most one patch may target a given symbol.",
1517 kept.original_name
1518 ))
1519 .with_code(ScanningIssueKind::PatchDuplicateTarget)
1520 .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this symbol."))
1521 .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1522 .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1523}
1524
1525fn duplicate_patch_function_diagnostic(kept: &FunctionLikeMetadata, dropped: &FunctionLikeMetadata) -> Issue {
1526 Issue::error(format!(
1527 "Multiple patches target function `{}`; at most one patch may target a given symbol.",
1528 kept.name
1529 ))
1530 .with_code(ScanningIssueKind::PatchDuplicateTarget)
1531 .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this function."))
1532 .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1533 .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1534}
1535
1536fn duplicate_patch_constant_diagnostic(kept: &ConstantMetadata, dropped: &ConstantMetadata) -> Issue {
1537 Issue::error(format!(
1538 "Multiple patches target constant `{}`; at most one patch may target a given symbol.",
1539 kept.name
1540 ))
1541 .with_code(ScanningIssueKind::PatchDuplicateTarget)
1542 .with_annotation(Annotation::primary(dropped.span).with_message("Duplicate patch for this constant."))
1543 .with_annotation(Annotation::secondary(kept.span).with_message("Already patched here."))
1544 .with_help("Merge the conflicting declarations into a single patch, or remove all but one.")
1545}
1546
1547fn orphan_patch_class_diagnostic(meta: &ClassLikeMetadata) -> Issue {
1548 Issue::error(format!(
1549 "Patch declares `{}` but no vendored or built-in definition exists to patch.",
1550 meta.original_name,
1551 ))
1552 .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1553 .with_annotation(Annotation::primary(meta.span))
1554 .with_help(
1555 "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1556 check the symbol name and verify the patch still matches the upstream source.",
1557 )
1558}
1559
1560fn orphan_patch_function_diagnostic(meta: &FunctionLikeMetadata) -> Issue {
1561 Issue::error(format!(
1562 "Patch declares function `{}` but no vendored or built-in definition exists to patch.",
1563 meta.name,
1564 ))
1565 .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1566 .with_annotation(Annotation::primary(meta.span))
1567 .with_help(
1568 "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1569 check the function name and verify the patch still matches the upstream source.",
1570 )
1571}
1572
1573fn orphan_patch_constant_diagnostic(meta: &ConstantMetadata) -> Issue {
1574 Issue::error(format!(
1575 "Patch declares constant `{}` but no vendored or built-in definition exists to patch.",
1576 meta.name,
1577 ))
1578 .with_code(ScanningIssueKind::PatchIntroducesNewSymbol)
1579 .with_annotation(Annotation::primary(meta.span))
1580 .with_help(
1581 "The patch may be misnamed or out-of-date relative to the vendored or built-in definition; \
1582 check the constant name and verify the patch still matches the upstream source.",
1583 )
1584}
1585
1586fn should_replace_metadata(
1597 existing_flags: MetadataFlags,
1598 existing_span: Span,
1599 new_flags: MetadataFlags,
1600 new_span: Span,
1601) -> bool {
1602 let new_is_user_defined = new_flags.is_user_defined();
1603 let existing_is_user_defined = existing_flags.is_user_defined();
1604
1605 if new_is_user_defined != existing_is_user_defined {
1606 return new_is_user_defined;
1607 }
1608
1609 let new_is_patch = new_flags.is_patch();
1610 let existing_is_patch = existing_flags.is_patch();
1611
1612 if new_is_patch != existing_is_patch {
1613 return new_is_patch;
1614 }
1615
1616 let new_is_built_in = new_flags.is_built_in();
1617 let existing_is_built_in = existing_flags.is_built_in();
1618
1619 if new_is_built_in != existing_is_built_in {
1620 return new_is_built_in;
1621 }
1622
1623 let new_is_polyfill = new_flags.is_polyfill();
1624 let existing_is_polyfill = existing_flags.is_polyfill();
1625
1626 if new_is_polyfill != existing_is_polyfill {
1627 return !new_is_polyfill;
1628 }
1629
1630 new_span < existing_span
1631}
1632
1633#[cfg(test)]
1634mod should_replace_metadata_tests {
1635 use super::*;
1636
1637 #[test]
1638 fn non_polyfill_replaces_polyfill() {
1639 let polyfill = MetadataFlags::POLYFILL;
1640 let real = MetadataFlags::empty();
1641 assert!(should_replace_metadata(polyfill, Span::dummy(0, 100), real, Span::dummy(0, 100)));
1642 assert!(!should_replace_metadata(real, Span::dummy(0, 100), polyfill, Span::dummy(0, 100)));
1643 }
1644
1645 #[test]
1646 fn polyfill_does_not_replace_non_polyfill_even_with_smaller_span() {
1647 let real = MetadataFlags::empty();
1648 let polyfill = MetadataFlags::POLYFILL;
1649 assert!(!should_replace_metadata(real, Span::dummy(500, 600), polyfill, Span::dummy(0, 10)));
1650 }
1651
1652 #[test]
1653 fn user_defined_beats_polyfill_flag() {
1654 let polyfill_user = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1655 let plain = MetadataFlags::empty();
1656 assert!(!should_replace_metadata(polyfill_user, Span::dummy(0, 10), plain, Span::dummy(0, 10)));
1657 assert!(should_replace_metadata(plain, Span::dummy(0, 10), polyfill_user, Span::dummy(0, 10)));
1658 }
1659
1660 #[test]
1661 fn two_user_defined_fall_through_to_polyfill_check() {
1662 let a = MetadataFlags::POLYFILL | MetadataFlags::USER_DEFINED;
1663 let b = MetadataFlags::USER_DEFINED;
1664 assert!(should_replace_metadata(a, Span::dummy(0, 10), b, Span::dummy(0, 10)));
1665 assert!(!should_replace_metadata(b, Span::dummy(0, 10), a, Span::dummy(0, 10)));
1666 }
1667
1668 #[test]
1669 fn two_non_polyfills_fall_through_to_priority_rules() {
1670 let user = MetadataFlags::USER_DEFINED;
1671 let builtin = MetadataFlags::BUILTIN;
1672 assert!(!should_replace_metadata(user, Span::dummy(0, 10), builtin, Span::dummy(0, 10)));
1673 assert!(should_replace_metadata(builtin, Span::dummy(0, 10), user, Span::dummy(0, 10)));
1674 }
1675
1676 #[test]
1677 fn patch_beats_vendored() {
1678 let vendored = MetadataFlags::empty();
1679 let patch = MetadataFlags::PATCH;
1680 assert!(should_replace_metadata(vendored, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1681 assert!(!should_replace_metadata(patch, Span::dummy(0, 100), vendored, Span::dummy(0, 100)));
1682 }
1683
1684 #[test]
1685 fn patch_beats_builtin() {
1686 let builtin = MetadataFlags::BUILTIN;
1687 let patch = MetadataFlags::PATCH;
1688 assert!(should_replace_metadata(builtin, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1689 assert!(!should_replace_metadata(patch, Span::dummy(0, 100), builtin, Span::dummy(0, 100)));
1690 }
1691
1692 #[test]
1693 fn user_defined_beats_patch() {
1694 let user = MetadataFlags::USER_DEFINED;
1695 let patch = MetadataFlags::PATCH;
1696 assert!(!should_replace_metadata(user, Span::dummy(0, 100), patch, Span::dummy(0, 100)));
1697 assert!(should_replace_metadata(patch, Span::dummy(0, 100), user, Span::dummy(0, 100)));
1698 }
1699
1700 #[test]
1701 fn patch_does_not_beat_user_defined_even_with_smaller_span() {
1702 let user = MetadataFlags::USER_DEFINED;
1703 let patch = MetadataFlags::PATCH;
1704 assert!(!should_replace_metadata(user, Span::dummy(500, 600), patch, Span::dummy(0, 10)));
1705 }
1706
1707 #[test]
1708 #[allow(clippy::expect_used)]
1709 fn patch_function_like_leaves_vendor_owning_slot() {
1710 use crate::metadata::function_like::FunctionLikeKind;
1715
1716 let name = word("foo");
1717 let key = (empty_word(), name);
1718 let vendor_span = Span::dummy(0, 100);
1719 let patch_span = Span::dummy(500, 600);
1720
1721 let vendor =
1722 FunctionLikeMetadata::new(FunctionLikeKind::Function, name, name, vendor_span, MetadataFlags::empty());
1723 let mut codebase = CodebaseMetadata::new();
1724 codebase.function_likes.insert(key, vendor);
1725
1726 let patch = FunctionLikeMetadata::new(FunctionLikeKind::Function, name, name, patch_span, MetadataFlags::PATCH);
1727 codebase.patch_function_likes.insert(key, patch);
1728
1729 codebase.apply_patches_pass();
1730
1731 let merged = codebase.function_likes.get(&key).expect("function-like must remain after patch");
1732 assert_eq!(merged.span, vendor_span, "patch must not move the slot's span");
1733 assert!(!merged.flags.is_patch(), "patch must not flip the slot's flags");
1734 }
1735
1736 #[test]
1737 fn patch_does_not_apply_to_user_defined_class() {
1738 let class_name = word("MyClass");
1739 let method_existing = word("doIt");
1740
1741 let mut user_class =
1742 ClassLikeMetadata::new(class_name, class_name, Span::dummy(0, 100), None, MetadataFlags::USER_DEFINED);
1743 user_class.methods.insert(method_existing);
1744
1745 let mut codebase = CodebaseMetadata::new();
1746 codebase.class_likes.insert(class_name, user_class);
1747
1748 let mut patch_class =
1749 ClassLikeMetadata::new(class_name, class_name, Span::dummy(0, 50), None, MetadataFlags::PATCH);
1750 let method_new = word("patchedMethod");
1751 patch_class.methods.insert(method_new);
1752
1753 codebase.patch_class_likes.insert(class_name, patch_class);
1754
1755 codebase.apply_patches_pass();
1756
1757 let class = &codebase.class_likes[&class_name];
1758 assert!(!class.methods.contains(&method_new));
1760 assert!(class.methods.contains(&method_existing));
1762 assert!(class.flags.is_user_defined());
1763 assert!(class.issues.is_empty());
1765 }
1766}