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