1use std::sync::Arc;
2
3use dashmap::{DashMap, DashSet};
4
5use crate::interner::Interner;
6
7type ReferenceLocations = DashMap<u32, Vec<(u32, u32, u16, u16)>>;
16type FinalizationCache = DashMap<Arc<str>, std::sync::OnceLock<Arc<[Arc<str>]>>>;
17
18use crate::storage::{
19 ClassStorage, EnumStorage, FunctionStorage, InterfaceStorage, MethodStorage, TraitStorage,
20};
21use mir_types::Union;
22
23#[inline]
32fn lookup_method<'a>(
33 map: &'a indexmap::IndexMap<Arc<str>, Arc<MethodStorage>>,
34 name: &str,
35) -> Option<&'a Arc<MethodStorage>> {
36 map.get(name).or_else(|| {
37 map.iter()
38 .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
39 .map(|(_, v)| v)
40 })
41}
42
43#[inline]
49fn record_ref(
50 sym_locs: &ReferenceLocations,
51 file_refs: &DashMap<u32, Vec<u32>>,
52 sym_id: u32,
53 file_id: u32,
54 line: u32,
55 col_start: u16,
56 col_end: u16,
57) {
58 {
59 let mut entries = sym_locs.entry(sym_id).or_default();
60 let span = (file_id, line, col_start, col_end);
61 if !entries.contains(&span) {
62 entries.push(span);
63 }
64 }
65 {
66 let mut refs = file_refs.entry(file_id).or_default();
67 if !refs.contains(&sym_id) {
68 refs.push(sym_id);
69 }
70 }
71}
72
73#[derive(Debug, Default)]
87struct CompactRefIndex {
88 entries: Vec<(u32, u32, u32, u16, u16)>,
91 sym_offsets: Vec<u32>,
93 by_file: Vec<u32>,
96 file_offsets: Vec<u32>,
98}
99
100struct ClassInheritance {
105 parent: Option<Arc<str>>,
106 interfaces: Vec<Arc<str>>, traits: Vec<Arc<str>>, all_parents: Vec<Arc<str>>,
109}
110
111struct InterfaceInheritance {
112 extends: Vec<Arc<str>>, all_parents: Vec<Arc<str>>,
114}
115
116pub struct StructuralSnapshot {
124 classes: std::collections::HashMap<Arc<str>, ClassInheritance>,
125 interfaces: std::collections::HashMap<Arc<str>, InterfaceInheritance>,
126}
127
128#[derive(Debug, Default)]
133pub struct Codebase {
134 pub classes: DashMap<Arc<str>, ClassStorage>,
135 pub interfaces: DashMap<Arc<str>, InterfaceStorage>,
136 pub traits: DashMap<Arc<str>, TraitStorage>,
137 pub enums: DashMap<Arc<str>, EnumStorage>,
138 pub functions: DashMap<Arc<str>, FunctionStorage>,
139 pub constants: DashMap<Arc<str>, Union>,
140
141 pub global_vars: DashMap<Arc<str>, Union>,
144 file_global_vars: DashMap<Arc<str>, Vec<Arc<str>>>,
147
148 referenced_methods: DashSet<u32>,
151 referenced_properties: DashSet<u32>,
153 referenced_functions: DashSet<u32>,
155
156 pub symbol_interner: Interner,
159 pub file_interner: Interner,
161
162 symbol_reference_locations: ReferenceLocations,
165 file_symbol_references: DashMap<u32, Vec<u32>>,
170
171 compact_ref_index: std::sync::RwLock<Option<CompactRefIndex>>,
175 is_compacted: std::sync::atomic::AtomicBool,
178
179 pub symbol_to_file: DashMap<Arc<str>, Arc<str>>,
182
183 pub known_symbols: DashSet<Arc<str>>,
187
188 pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
196 pub file_namespaces: DashMap<Arc<str>, String>,
204
205 finalized: std::sync::atomic::AtomicBool,
207
208 finalization_cache: FinalizationCache,
215}
216
217impl Codebase {
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn inject_stub_slice(&self, slice: crate::storage::StubSlice) {
241 let file = slice.file.clone();
242 for cls in slice.classes {
243 if let Some(f) = &file {
244 self.symbol_to_file.insert(cls.fqcn.clone(), f.clone());
245 }
246 self.classes.insert(cls.fqcn.clone(), cls);
247 }
248 for iface in slice.interfaces {
249 if let Some(f) = &file {
250 self.symbol_to_file.insert(iface.fqcn.clone(), f.clone());
251 }
252 self.interfaces.insert(iface.fqcn.clone(), iface);
253 }
254 for tr in slice.traits {
255 if let Some(f) = &file {
256 self.symbol_to_file.insert(tr.fqcn.clone(), f.clone());
257 }
258 self.traits.insert(tr.fqcn.clone(), tr);
259 }
260 for en in slice.enums {
261 if let Some(f) = &file {
262 self.symbol_to_file.insert(en.fqcn.clone(), f.clone());
263 }
264 self.enums.insert(en.fqcn.clone(), en);
265 }
266 for func in slice.functions {
267 if let Some(f) = &file {
268 self.symbol_to_file.insert(func.fqn.clone(), f.clone());
269 }
270 self.functions.insert(func.fqn.clone(), func);
271 }
272 for (name, ty) in slice.constants {
273 self.constants.insert(name, ty);
274 }
275 if let Some(f) = &file {
276 for (name, ty) in slice.global_vars {
277 self.register_global_var(f, name, ty);
278 }
279 if let Some(ns) = slice.namespace {
280 self.file_namespaces.insert(f.clone(), ns.to_string());
281 }
282 if !slice.imports.is_empty() {
283 self.file_imports.insert(f.clone(), slice.imports);
284 }
285 }
286 }
287
288 pub fn compact_reference_index(&self) {
305 let mut entries: Vec<(u32, u32, u32, u16, u16)> = self
307 .symbol_reference_locations
308 .iter()
309 .flat_map(|entry| {
310 let sym_id = *entry.key();
311 entry
312 .value()
313 .iter()
314 .map(move |&(file_id, line, col_start, col_end)| {
315 (sym_id, file_id, line, col_start, col_end)
316 })
317 .collect::<Vec<_>>()
318 })
319 .collect();
320
321 if entries.is_empty() {
322 return;
323 }
324
325 entries.sort_unstable();
327 entries.dedup();
328
329 let n = entries.len();
330
331 let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
333 let mut sym_offsets = vec![0u32; max_sym + 2];
334 for &(sym_id, ..) in &entries {
335 sym_offsets[sym_id as usize + 1] += 1;
336 }
337 for i in 1..sym_offsets.len() {
338 sym_offsets[i] += sym_offsets[i - 1];
339 }
340
341 let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
345 let mut by_file: Vec<u32> = (0..n as u32).collect();
346 by_file.sort_unstable_by_key(|&i| {
347 let (sym_id, file_id, line, col_start, col_end) = entries[i as usize];
348 (file_id, sym_id, line, col_start, col_end)
349 });
350
351 let mut file_offsets = vec![0u32; max_file + 2];
352 for &idx in &by_file {
353 let file_id = entries[idx as usize].1;
354 file_offsets[file_id as usize + 1] += 1;
355 }
356 for i in 1..file_offsets.len() {
357 file_offsets[i] += file_offsets[i - 1];
358 }
359
360 *self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
361 entries,
362 sym_offsets,
363 by_file,
364 file_offsets,
365 });
366 self.is_compacted
367 .store(true, std::sync::atomic::Ordering::Release);
368
369 self.symbol_reference_locations.clear();
371 self.file_symbol_references.clear();
372 }
373
374 fn ensure_expanded(&self) {
380 if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
382 return;
383 }
384 let mut guard = self.compact_ref_index.write().unwrap();
386 if let Some(ci) = guard.take() {
387 for &(sym_id, file_id, line, col_start, col_end) in &ci.entries {
388 record_ref(
389 &self.symbol_reference_locations,
390 &self.file_symbol_references,
391 sym_id,
392 file_id,
393 line,
394 col_start,
395 col_end,
396 );
397 }
398 self.is_compacted
399 .store(false, std::sync::atomic::Ordering::Release);
400 }
401 }
403
404 pub fn invalidate_finalization(&self) {
411 self.finalized
412 .store(false, std::sync::atomic::Ordering::SeqCst);
413 self.finalization_cache.clear();
414 }
415
416 pub fn remove_file_definitions(&self, file_path: &str) {
427 let symbols: Vec<Arc<str>> = self
429 .symbol_to_file
430 .iter()
431 .filter(|entry| entry.value().as_ref() == file_path)
432 .map(|entry| entry.key().clone())
433 .collect();
434
435 for sym in &symbols {
439 self.classes.remove(sym.as_ref());
440 self.interfaces.remove(sym.as_ref());
441 self.traits.remove(sym.as_ref());
442 self.enums.remove(sym.as_ref());
443 self.functions.remove(sym.as_ref());
444 self.constants.remove(sym.as_ref());
445 self.symbol_to_file.remove(sym.as_ref());
446 self.known_symbols.remove(sym.as_ref());
447 self.finalization_cache.remove(sym.as_ref());
448 }
449
450 self.file_imports.remove(file_path);
452 self.file_namespaces.remove(file_path);
453
454 if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
456 for name in var_names {
457 self.global_vars.remove(name.as_ref());
458 }
459 }
460
461 self.ensure_expanded();
463
464 if let Some(file_id) = self.file_interner.get_id(file_path) {
467 if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
468 for sym_id in sym_ids {
469 if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
470 entries.retain(|&(fid, ..)| fid != file_id);
471 }
472 }
473 }
474 }
475
476 self.invalidate_finalization();
477 }
478
479 pub fn file_structural_snapshot(&self, file_path: &str) -> StructuralSnapshot {
491 let symbols: Vec<Arc<str>> = self
492 .symbol_to_file
493 .iter()
494 .filter(|e| e.value().as_ref() == file_path)
495 .map(|e| e.key().clone())
496 .collect();
497
498 let mut classes = std::collections::HashMap::new();
499 let mut interfaces = std::collections::HashMap::new();
500
501 for sym in symbols {
502 self.ensure_finalized(sym.as_ref());
503 if let Some(cls) = self.classes.get(sym.as_ref()) {
504 let mut ifaces = cls.interfaces.clone();
505 ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
506 let mut traits = cls.traits.clone();
507 traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
508 classes.insert(
509 sym,
510 ClassInheritance {
511 parent: cls.parent.clone(),
512 interfaces: ifaces,
513 traits,
514 all_parents: cls.all_parents.clone(),
515 },
516 );
517 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
518 let mut extends = iface.extends.clone();
519 extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
520 interfaces.insert(
521 sym,
522 InterfaceInheritance {
523 extends,
524 all_parents: iface.all_parents.clone(),
525 },
526 );
527 }
528 }
529
530 StructuralSnapshot {
531 classes,
532 interfaces,
533 }
534 }
535
536 pub fn structural_unchanged_after_pass1(
542 &self,
543 file_path: &str,
544 old: &StructuralSnapshot,
545 ) -> bool {
546 let symbols: Vec<Arc<str>> = self
547 .symbol_to_file
548 .iter()
549 .filter(|e| e.value().as_ref() == file_path)
550 .map(|e| e.key().clone())
551 .collect();
552
553 let mut seen_classes = 0usize;
554 let mut seen_interfaces = 0usize;
555
556 for sym in &symbols {
557 if let Some(cls) = self.classes.get(sym.as_ref()) {
558 seen_classes += 1;
559 let Some(old_cls) = old.classes.get(sym.as_ref()) else {
560 return false; };
562 if old_cls.parent != cls.parent {
563 return false;
564 }
565 let mut new_ifaces = cls.interfaces.clone();
566 new_ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
567 if old_cls.interfaces != new_ifaces {
568 return false;
569 }
570 let mut new_traits = cls.traits.clone();
571 new_traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
572 if old_cls.traits != new_traits {
573 return false;
574 }
575 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
576 seen_interfaces += 1;
577 let Some(old_iface) = old.interfaces.get(sym.as_ref()) else {
578 return false; };
580 let mut new_extends = iface.extends.clone();
581 new_extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
582 if old_iface.extends != new_extends {
583 return false;
584 }
585 }
586 }
588
589 seen_classes == old.classes.len() && seen_interfaces == old.interfaces.len()
591 }
592
593 pub fn restore_all_parents(&self, file_path: &str, snapshot: &StructuralSnapshot) {
600 let symbols: Vec<Arc<str>> = self
601 .symbol_to_file
602 .iter()
603 .filter(|e| e.value().as_ref() == file_path)
604 .map(|e| e.key().clone())
605 .collect();
606
607 for sym in &symbols {
608 if let Some(old_cls) = snapshot.classes.get(sym.as_ref()) {
609 if let Some(mut cls) = self.classes.get_mut(sym.as_ref()) {
610 cls.all_parents = old_cls.all_parents.clone();
611 }
612 let arc: Arc<[Arc<str>]> = Arc::from(old_cls.all_parents.as_slice());
615 self.finalization_cache
616 .entry(sym.clone())
617 .or_default()
618 .get_or_init(|| arc);
619 } else if let Some(old_iface) = snapshot.interfaces.get(sym.as_ref()) {
620 if let Some(mut iface) = self.interfaces.get_mut(sym.as_ref()) {
621 iface.all_parents = old_iface.all_parents.clone();
622 }
623 let arc: Arc<[Arc<str>]> = Arc::from(old_iface.all_parents.as_slice());
624 self.finalization_cache
625 .entry(sym.clone())
626 .or_default()
627 .get_or_init(|| arc);
628 }
629 }
630
631 self.finalized
632 .store(true, std::sync::atomic::Ordering::SeqCst);
633 }
634
635 pub fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
642 self.file_global_vars
643 .entry(file.clone())
644 .or_default()
645 .push(name.clone());
646 self.global_vars.insert(name, ty);
647 }
648
649 pub fn get_property(
655 &self,
656 fqcn: &str,
657 prop_name: &str,
658 ) -> Option<crate::storage::PropertyStorage> {
659 self.get_property_inner(fqcn, prop_name, &mut std::collections::HashSet::new())
660 }
661
662 fn get_property_inner(
663 &self,
664 fqcn: &str,
665 prop_name: &str,
666 visited: &mut std::collections::HashSet<String>,
667 ) -> Option<crate::storage::PropertyStorage> {
668 if !visited.insert(fqcn.to_string()) {
669 return None;
670 }
671 self.ensure_finalized(fqcn);
672 if let Some(cls) = self.classes.get(fqcn) {
674 if let Some(p) = cls.own_properties.get(prop_name) {
675 return Some(p.clone());
676 }
677 let mixins = cls.mixins.clone();
678 drop(cls);
679 for mixin in &mixins {
680 if let Some(p) = self.get_property_inner(mixin.as_ref(), prop_name, visited) {
681 return Some(p);
682 }
683 }
684 }
685
686 let all_parents = {
688 if let Some(cls) = self.classes.get(fqcn) {
689 cls.all_parents.clone()
690 } else {
691 return None;
692 }
693 };
694
695 for ancestor_fqcn in &all_parents {
696 if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
697 if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
698 return Some(p.clone());
699 }
700 let anc_mixins = ancestor_cls.mixins.clone();
701 drop(ancestor_cls);
702 for mixin_fqcn in &anc_mixins {
703 if let Some(p) = self.get_property_inner(mixin_fqcn, prop_name, visited) {
704 return Some(p);
705 }
706 }
707 }
708 }
709
710 let trait_list = {
712 if let Some(cls) = self.classes.get(fqcn) {
713 cls.traits.clone()
714 } else {
715 vec![]
716 }
717 };
718 for trait_fqcn in &trait_list {
719 if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
720 if let Some(p) = tr.own_properties.get(prop_name) {
721 return Some(p.clone());
722 }
723 }
724 }
725
726 None
727 }
728
729 pub fn get_class_constant(
731 &self,
732 fqcn: &str,
733 const_name: &str,
734 ) -> Option<crate::storage::ConstantStorage> {
735 self.ensure_finalized(fqcn);
736 if let Some(cls) = self.classes.get(fqcn) {
738 if let Some(c) = cls.own_constants.get(const_name) {
739 return Some(c.clone());
740 }
741 let all_parents = cls.all_parents.clone();
742 let interfaces = cls.interfaces.clone();
743 let traits = cls.traits.clone();
744 drop(cls);
745
746 for tr_fqcn in &traits {
747 if let Some(tr) = self.traits.get(tr_fqcn.as_ref()) {
748 if let Some(c) = tr.own_constants.get(const_name) {
749 return Some(c.clone());
750 }
751 }
752 }
753
754 for ancestor_fqcn in &all_parents {
755 if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
756 if let Some(c) = ancestor.own_constants.get(const_name) {
757 return Some(c.clone());
758 }
759 }
760 if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
761 if let Some(c) = iface.own_constants.get(const_name) {
762 return Some(c.clone());
763 }
764 }
765 }
766
767 for iface_fqcn in &interfaces {
768 if let Some(iface) = self.interfaces.get(iface_fqcn.as_ref()) {
769 if let Some(c) = iface.own_constants.get(const_name) {
770 return Some(c.clone());
771 }
772 }
773 }
774
775 return None;
776 }
777
778 if let Some(iface) = self.interfaces.get(fqcn) {
780 if let Some(c) = iface.own_constants.get(const_name) {
781 return Some(c.clone());
782 }
783 let parents = iface.all_parents.clone();
784 drop(iface);
785 for p in &parents {
786 if let Some(parent_iface) = self.interfaces.get(p.as_ref()) {
787 if let Some(c) = parent_iface.own_constants.get(const_name) {
788 return Some(c.clone());
789 }
790 }
791 }
792 return None;
793 }
794
795 if let Some(en) = self.enums.get(fqcn) {
797 if let Some(c) = en.own_constants.get(const_name) {
798 return Some(c.clone());
799 }
800 if en.cases.contains_key(const_name) {
801 return Some(crate::storage::ConstantStorage {
802 name: Arc::from(const_name),
803 ty: mir_types::Union::mixed(),
804 visibility: None,
805 is_final: false,
806 location: None,
807 });
808 }
809 return None;
810 }
811
812 if let Some(tr) = self.traits.get(fqcn) {
814 if let Some(c) = tr.own_constants.get(const_name) {
815 return Some(c.clone());
816 }
817 return None;
818 }
819
820 None
821 }
822
823 pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
825 self.get_method_inner(fqcn, method_name, &mut std::collections::HashSet::new())
826 }
827
828 fn get_method_inner(
829 &self,
830 fqcn: &str,
831 method_name: &str,
832 visited: &mut std::collections::HashSet<String>,
833 ) -> Option<Arc<MethodStorage>> {
834 if !visited.insert(fqcn.to_string()) {
835 return None;
836 }
837 self.ensure_finalized(fqcn);
838 let method_lower = method_name.to_lowercase();
840 let method_name = method_lower.as_str();
841
842 if let Some(cls) = self.classes.get(fqcn) {
844 if let Some(m) = lookup_method(&cls.own_methods, method_name) {
846 return Some(Arc::clone(m));
847 }
848 let own_traits = cls.traits.clone();
850 let ancestors = cls.all_parents.clone();
851 let mixins = cls.mixins.clone();
852 drop(cls);
853
854 for mixin_fqcn in &mixins {
856 if let Some(m) = self.get_method_inner(mixin_fqcn, method_name, visited) {
857 return Some(m);
858 }
859 }
860
861 for tr_fqcn in &own_traits {
863 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
864 return Some(m);
865 }
866 }
867
868 for ancestor_fqcn in &ancestors {
870 if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
871 if let Some(m) = lookup_method(&anc.own_methods, method_name) {
872 return Some(Arc::clone(m));
873 }
874 let anc_traits = anc.traits.clone();
875 let anc_mixins = anc.mixins.clone();
876 drop(anc);
877 for tr_fqcn in &anc_traits {
878 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
879 return Some(m);
880 }
881 }
882 for mixin_fqcn in &anc_mixins {
883 if let Some(m) = self.get_method_inner(mixin_fqcn, method_name, visited) {
884 return Some(m);
885 }
886 }
887 } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
888 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
889 let mut ms = (**m).clone();
890 ms.is_abstract = true;
891 return Some(Arc::new(ms));
892 }
893 }
894 }
896 return None;
897 }
898
899 if let Some(iface) = self.interfaces.get(fqcn) {
901 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
902 return Some(Arc::clone(m));
903 }
904 let parents = iface.all_parents.clone();
905 drop(iface);
906 for parent_fqcn in &parents {
907 if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
908 if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
909 return Some(Arc::clone(m));
910 }
911 }
912 }
913 return None;
914 }
915
916 if let Some(tr) = self.traits.get(fqcn) {
918 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
919 return Some(Arc::clone(m));
920 }
921 return None;
922 }
923
924 if let Some(e) = self.enums.get(fqcn) {
926 if let Some(m) = lookup_method(&e.own_methods, method_name) {
927 return Some(Arc::clone(m));
928 }
929 if matches!(method_name, "cases" | "from" | "tryfrom") {
931 return Some(Arc::new(crate::storage::MethodStorage {
932 fqcn: Arc::from(fqcn),
933 name: Arc::from(method_name),
934 params: vec![],
935 return_type: Some(mir_types::Union::mixed()),
936 inferred_return_type: None,
937 visibility: crate::storage::Visibility::Public,
938 is_static: true,
939 is_abstract: false,
940 is_constructor: false,
941 template_params: vec![],
942 assertions: vec![],
943 throws: vec![],
944 is_final: false,
945 is_internal: false,
946 is_pure: false,
947 deprecated: None,
948 location: None,
949 }));
950 }
951 }
952
953 None
954 }
955
956 pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
958 self.ensure_finalized(child);
959 if child == ancestor {
960 return true;
961 }
962 if let Some(cls) = self.classes.get(child) {
963 return cls.implements_or_extends(ancestor);
964 }
965 if let Some(iface) = self.interfaces.get(child) {
966 return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
967 }
968 if let Some(en) = self.enums.get(child) {
971 if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
973 return true;
974 }
975 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
977 return true;
978 }
979 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
981 {
982 return true;
983 }
984 }
985 false
986 }
987
988 pub fn type_exists(&self, fqcn: &str) -> bool {
990 self.classes.contains_key(fqcn)
991 || self.interfaces.contains_key(fqcn)
992 || self.traits.contains_key(fqcn)
993 || self.enums.contains_key(fqcn)
994 }
995
996 pub fn function_exists(&self, fqn: &str) -> bool {
997 self.functions.contains_key(fqn)
998 }
999
1000 pub fn is_abstract_class(&self, fqcn: &str) -> bool {
1004 self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
1005 }
1006
1007 pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
1010 if let Some(cls) = self.classes.get(fqcn) {
1011 return cls.template_params.clone();
1012 }
1013 if let Some(iface) = self.interfaces.get(fqcn) {
1014 return iface.template_params.clone();
1015 }
1016 if let Some(tr) = self.traits.get(fqcn) {
1017 return tr.template_params.clone();
1018 }
1019 vec![]
1020 }
1021
1022 pub fn get_inherited_template_bindings(
1027 &self,
1028 fqcn: &str,
1029 ) -> std::collections::HashMap<Arc<str>, Union> {
1030 let mut bindings = std::collections::HashMap::new();
1031 let mut current = fqcn.to_string();
1032
1033 loop {
1034 let (parent_fqcn, extends_type_args) = {
1035 let cls = match self.classes.get(current.as_str()) {
1036 Some(c) => c,
1037 None => break,
1038 };
1039 let parent = match &cls.parent {
1040 Some(p) => p.clone(),
1041 None => break,
1042 };
1043 let args = cls.extends_type_args.clone();
1044 (parent, args)
1045 };
1046
1047 if !extends_type_args.is_empty() {
1048 let parent_tps = self.get_class_template_params(&parent_fqcn);
1049 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
1050 bindings
1051 .entry(tp.name.clone())
1052 .or_insert_with(|| ty.clone());
1053 }
1054 }
1055
1056 current = parent_fqcn.to_string();
1057 }
1058
1059 bindings
1060 }
1061
1062 pub fn has_magic_get(&self, fqcn: &str) -> bool {
1065 self.get_method(fqcn, "__get").is_some()
1066 }
1067
1068 pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
1076 self.ensure_finalized(fqcn);
1077 if let Some(iface) = self.interfaces.get(fqcn) {
1079 let parents = iface.all_parents.clone();
1080 drop(iface);
1081 for p in &parents {
1082 if !self.type_exists(p.as_ref()) {
1083 return true;
1084 }
1085 }
1086 return false;
1087 }
1088
1089 let (parent, interfaces, traits, all_parents) = {
1091 let Some(cls) = self.classes.get(fqcn) else {
1092 return false;
1093 };
1094 (
1095 cls.parent.clone(),
1096 cls.interfaces.clone(),
1097 cls.traits.clone(),
1098 cls.all_parents.clone(),
1099 )
1100 };
1101
1102 if let Some(ref p) = parent {
1104 if !self.type_exists(p.as_ref()) {
1105 return true;
1106 }
1107 }
1108 for iface in &interfaces {
1109 if !self.type_exists(iface.as_ref()) {
1110 return true;
1111 }
1112 }
1113 for tr in &traits {
1114 if !self.type_exists(tr.as_ref()) {
1115 return true;
1116 }
1117 }
1118
1119 for ancestor in &all_parents {
1121 if !self.type_exists(ancestor.as_ref()) {
1122 return true;
1123 }
1124 }
1125
1126 false
1127 }
1128
1129 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
1136 let name = name.trim_start_matches('\\');
1137 if name.is_empty() {
1138 return name.to_string();
1139 }
1140 if name.contains('\\') {
1145 let first_segment = name.split('\\').next().unwrap_or(name);
1147 if let Some(imports) = self.file_imports.get(file) {
1148 if let Some(resolved_prefix) = imports.get(first_segment) {
1149 let rest = &name[first_segment.len()..]; return format!("{resolved_prefix}{rest}");
1151 }
1152 }
1153 if self.type_exists(name) {
1155 return name.to_string();
1156 }
1157 if let Some(ns) = self.file_namespaces.get(file) {
1159 let qualified = format!("{}\\{}", *ns, name);
1160 if self.type_exists(&qualified) {
1161 return qualified;
1162 }
1163 }
1164 return name.to_string();
1165 }
1166 match name {
1168 "self" | "parent" | "static" | "this" => return name.to_string(),
1169 _ => {}
1170 }
1171 if let Some(imports) = self.file_imports.get(file) {
1173 if let Some(resolved) = imports.get(name) {
1174 return resolved.clone();
1175 }
1176 let name_lower = name.to_lowercase();
1178 for (alias, resolved) in imports.iter() {
1179 if alias.to_lowercase() == name_lower {
1180 return resolved.clone();
1181 }
1182 }
1183 }
1184 if let Some(ns) = self.file_namespaces.get(file) {
1186 let qualified = format!("{}\\{}", *ns, name);
1187 if self.type_exists(&qualified) {
1192 return qualified;
1193 }
1194 if self.type_exists(name) {
1195 return name.to_string();
1196 }
1197 return qualified;
1198 }
1199 name.to_string()
1200 }
1201
1202 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
1209 if let Some(cls) = self.classes.get(fqcn) {
1210 return cls.location.clone();
1211 }
1212 if let Some(iface) = self.interfaces.get(fqcn) {
1213 return iface.location.clone();
1214 }
1215 if let Some(tr) = self.traits.get(fqcn) {
1216 return tr.location.clone();
1217 }
1218 if let Some(en) = self.enums.get(fqcn) {
1219 return en.location.clone();
1220 }
1221 if let Some(func) = self.functions.get(fqcn) {
1222 return func.location.clone();
1223 }
1224 None
1225 }
1226
1227 pub fn get_member_location(
1229 &self,
1230 fqcn: &str,
1231 member_name: &str,
1232 ) -> Option<crate::storage::Location> {
1233 if let Some(method) = self.get_method(fqcn, member_name) {
1235 return method.location.clone();
1236 }
1237 if let Some(prop) = self.get_property(fqcn, member_name) {
1239 return prop.location.clone();
1240 }
1241 if let Some(cls) = self.classes.get(fqcn) {
1243 if let Some(c) = cls.own_constants.get(member_name) {
1244 return c.location.clone();
1245 }
1246 }
1247 if let Some(iface) = self.interfaces.get(fqcn) {
1249 if let Some(c) = iface.own_constants.get(member_name) {
1250 return c.location.clone();
1251 }
1252 }
1253 if let Some(tr) = self.traits.get(fqcn) {
1255 if let Some(c) = tr.own_constants.get(member_name) {
1256 return c.location.clone();
1257 }
1258 }
1259 if let Some(en) = self.enums.get(fqcn) {
1261 if let Some(c) = en.own_constants.get(member_name) {
1262 return c.location.clone();
1263 }
1264 if let Some(case) = en.cases.get(member_name) {
1265 return case.location.clone();
1266 }
1267 }
1268 None
1269 }
1270
1271 pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
1277 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1278 let id = self.symbol_interner.intern_str(&key);
1279 self.referenced_methods.insert(id);
1280 }
1281
1282 pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
1284 let key = format!("{fqcn}::{prop_name}");
1285 let id = self.symbol_interner.intern_str(&key);
1286 self.referenced_properties.insert(id);
1287 }
1288
1289 pub fn mark_function_referenced(&self, fqn: &str) {
1291 let id = self.symbol_interner.intern_str(fqn);
1292 self.referenced_functions.insert(id);
1293 }
1294
1295 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
1296 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1297 match self.symbol_interner.get_id(&key) {
1298 Some(id) => self.referenced_methods.contains(&id),
1299 None => false,
1300 }
1301 }
1302
1303 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
1304 let key = format!("{fqcn}::{prop_name}");
1305 match self.symbol_interner.get_id(&key) {
1306 Some(id) => self.referenced_properties.contains(&id),
1307 None => false,
1308 }
1309 }
1310
1311 pub fn is_function_referenced(&self, fqn: &str) -> bool {
1312 match self.symbol_interner.get_id(fqn) {
1313 Some(id) => self.referenced_functions.contains(&id),
1314 None => false,
1315 }
1316 }
1317
1318 pub fn mark_method_referenced_at(
1321 &self,
1322 fqcn: &str,
1323 method_name: &str,
1324 file: Arc<str>,
1325 line: u32,
1326 col_start: u16,
1327 col_end: u16,
1328 ) {
1329 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1330 self.ensure_expanded();
1331 let sym_id = self.symbol_interner.intern_str(&key);
1332 let file_id = self.file_interner.intern(file);
1333 self.referenced_methods.insert(sym_id);
1334 record_ref(
1335 &self.symbol_reference_locations,
1336 &self.file_symbol_references,
1337 sym_id,
1338 file_id,
1339 line,
1340 col_start,
1341 col_end,
1342 );
1343 }
1344
1345 pub fn mark_property_referenced_at(
1348 &self,
1349 fqcn: &str,
1350 prop_name: &str,
1351 file: Arc<str>,
1352 line: u32,
1353 col_start: u16,
1354 col_end: u16,
1355 ) {
1356 let key = format!("{fqcn}::{prop_name}");
1357 self.ensure_expanded();
1358 let sym_id = self.symbol_interner.intern_str(&key);
1359 let file_id = self.file_interner.intern(file);
1360 self.referenced_properties.insert(sym_id);
1361 record_ref(
1362 &self.symbol_reference_locations,
1363 &self.file_symbol_references,
1364 sym_id,
1365 file_id,
1366 line,
1367 col_start,
1368 col_end,
1369 );
1370 }
1371
1372 pub fn mark_function_referenced_at(
1375 &self,
1376 fqn: &str,
1377 file: Arc<str>,
1378 line: u32,
1379 col_start: u16,
1380 col_end: u16,
1381 ) {
1382 self.ensure_expanded();
1383 let sym_id = self.symbol_interner.intern_str(fqn);
1384 let file_id = self.file_interner.intern(file);
1385 self.referenced_functions.insert(sym_id);
1386 record_ref(
1387 &self.symbol_reference_locations,
1388 &self.file_symbol_references,
1389 sym_id,
1390 file_id,
1391 line,
1392 col_start,
1393 col_end,
1394 );
1395 }
1396
1397 pub fn mark_class_referenced_at(
1401 &self,
1402 fqcn: &str,
1403 file: Arc<str>,
1404 line: u32,
1405 col_start: u16,
1406 col_end: u16,
1407 ) {
1408 self.ensure_expanded();
1409 let sym_id = self.symbol_interner.intern_str(fqcn);
1410 let file_id = self.file_interner.intern(file);
1411 record_ref(
1412 &self.symbol_reference_locations,
1413 &self.file_symbol_references,
1414 sym_id,
1415 file_id,
1416 line,
1417 col_start,
1418 col_end,
1419 );
1420 }
1421
1422 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
1426 if locs.is_empty() {
1427 return;
1428 }
1429 self.ensure_expanded();
1430 let file_id = self.file_interner.intern(file);
1431 for (symbol_key, line, col_start, col_end) in locs {
1432 let sym_id = self.symbol_interner.intern_str(symbol_key);
1433 record_ref(
1434 &self.symbol_reference_locations,
1435 &self.file_symbol_references,
1436 sym_id,
1437 file_id,
1438 *line,
1439 *col_start,
1440 *col_end,
1441 );
1442 }
1443 }
1444
1445 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1448 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
1449 return Vec::new();
1450 };
1451 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1453 let id = sym_id as usize;
1454 if id + 1 >= ci.sym_offsets.len() {
1455 return Vec::new();
1456 }
1457 let start = ci.sym_offsets[id] as usize;
1458 let end = ci.sym_offsets[id + 1] as usize;
1459 return ci.entries[start..end]
1460 .iter()
1461 .map(|&(_, file_id, line, col_start, col_end)| {
1462 (self.file_interner.get(file_id), line, col_start, col_end)
1463 })
1464 .collect();
1465 }
1466 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1468 return Vec::new();
1469 };
1470 entries
1471 .iter()
1472 .map(|&(file_id, line, col_start, col_end)| {
1473 (self.file_interner.get(file_id), line, col_start, col_end)
1474 })
1475 .collect()
1476 }
1477
1478 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1482 let Some(file_id) = self.file_interner.get_id(file) else {
1483 return Vec::new();
1484 };
1485 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1487 let id = file_id as usize;
1488 if id + 1 >= ci.file_offsets.len() {
1489 return Vec::new();
1490 }
1491 let start = ci.file_offsets[id] as usize;
1492 let end = ci.file_offsets[id + 1] as usize;
1493 return ci.by_file[start..end]
1494 .iter()
1495 .map(|&entry_idx| {
1496 let (sym_id, _, line, col_start, col_end) = ci.entries[entry_idx as usize];
1497 (self.symbol_interner.get(sym_id), line, col_start, col_end)
1498 })
1499 .collect();
1500 }
1501 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
1503 return Vec::new();
1504 };
1505 let mut out = Vec::new();
1506 for &sym_id in sym_ids.iter() {
1507 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1508 continue;
1509 };
1510 let sym_key = self.symbol_interner.get(sym_id);
1511 for &(entry_file_id, line, col_start, col_end) in entries.iter() {
1512 if entry_file_id == file_id {
1513 out.push((sym_key.clone(), line, col_start, col_end));
1514 }
1515 }
1516 }
1517 out
1518 }
1519
1520 pub fn file_has_symbol_references(&self, file: &str) -> bool {
1522 let Some(file_id) = self.file_interner.get_id(file) else {
1523 return false;
1524 };
1525 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1527 let id = file_id as usize;
1528 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
1529 }
1530 self.file_symbol_references.contains_key(&file_id)
1531 }
1532
1533 pub fn ensure_finalized(&self, fqcn: &str) -> Arc<[Arc<str>]> {
1548 if let Some(entry) = self.finalization_cache.get(fqcn) {
1550 if let Some(v) = entry.get() {
1551 return Arc::clone(v);
1552 }
1553 }
1554
1555 thread_local! {
1558 static IN_PROGRESS: std::cell::RefCell<std::collections::HashSet<String>> =
1559 std::cell::RefCell::new(std::collections::HashSet::new());
1560 }
1561 let is_cycle = IN_PROGRESS.with(|s| !s.borrow_mut().insert(fqcn.to_string()));
1562 if is_cycle {
1563 return Arc::from([]);
1564 }
1565
1566 let parents = if self.classes.contains_key(fqcn) {
1567 self.compute_class_ancestors(fqcn)
1568 } else if self.interfaces.contains_key(fqcn) {
1569 self.compute_interface_ancestors(fqcn)
1570 } else {
1571 vec![]
1572 };
1573
1574 if let Some(mut cls) = self.classes.get_mut(fqcn) {
1576 cls.all_parents = parents.clone();
1577 } else if let Some(mut iface) = self.interfaces.get_mut(fqcn) {
1578 iface.all_parents = parents.clone();
1579 }
1580
1581 let arc: Arc<[Arc<str>]> = Arc::from(parents.as_slice());
1582
1583 let entry = self.finalization_cache.entry(Arc::from(fqcn)).or_default();
1586 let stored = entry.get_or_init(|| Arc::clone(&arc));
1587
1588 IN_PROGRESS.with(|s| s.borrow_mut().remove(fqcn));
1589
1590 Arc::clone(stored)
1591 }
1592
1593 pub fn finalize(&self) {
1600 if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
1601 return;
1602 }
1603
1604 let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
1606 for fqcn in &class_keys {
1607 self.ensure_finalized(fqcn);
1608 }
1609
1610 let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
1612 for fqcn in &iface_keys {
1613 self.ensure_finalized(fqcn);
1614 }
1615
1616 type PendingImports = Vec<(Arc<str>, Vec<(Arc<str>, Arc<str>, Arc<str>)>)>;
1619 let pending: PendingImports = self
1620 .classes
1621 .iter()
1622 .filter(|e| !e.pending_import_types.is_empty())
1623 .map(|e| (e.key().clone(), e.pending_import_types.clone()))
1624 .collect();
1625 for (dst_fqcn, imports) in pending {
1626 let mut resolved: std::collections::HashMap<Arc<str>, mir_types::Union> =
1627 std::collections::HashMap::new();
1628 for (local, original, from_class) in &imports {
1629 if let Some(src_cls) = self.classes.get(from_class.as_ref()) {
1630 if let Some(ty) = src_cls.type_aliases.get(original.as_ref()) {
1631 resolved.insert(local.clone(), ty.clone());
1632 }
1633 }
1634 }
1635 if !resolved.is_empty() {
1636 if let Some(mut dst_cls) = self.classes.get_mut(dst_fqcn.as_ref()) {
1637 for (k, v) in resolved {
1638 dst_cls.type_aliases.insert(k, v);
1639 }
1640 }
1641 }
1642 }
1643
1644 self.finalized
1645 .store(true, std::sync::atomic::Ordering::SeqCst);
1646 }
1647
1648 fn get_method_in_trait(
1656 &self,
1657 tr_fqcn: &Arc<str>,
1658 method_name: &str,
1659 ) -> Option<Arc<MethodStorage>> {
1660 let mut visited = std::collections::HashSet::new();
1661 self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
1662 }
1663
1664 fn get_method_in_trait_inner(
1665 &self,
1666 tr_fqcn: &Arc<str>,
1667 method_name: &str,
1668 visited: &mut std::collections::HashSet<String>,
1669 ) -> Option<Arc<MethodStorage>> {
1670 if !visited.insert(tr_fqcn.to_string()) {
1671 return None; }
1673 let tr = self.traits.get(tr_fqcn.as_ref())?;
1674 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
1675 return Some(Arc::clone(m));
1676 }
1677 let used_traits = tr.traits.clone();
1678 drop(tr);
1679 for used_fqcn in &used_traits {
1680 if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
1681 return Some(m);
1682 }
1683 }
1684 None
1685 }
1686
1687 fn compute_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1693 let (parent, interfaces, traits) = match self.classes.get(fqcn) {
1694 Some(cls) => (
1695 cls.parent.clone(),
1696 cls.interfaces.clone(),
1697 cls.traits.clone(),
1698 ),
1699 None => return vec![],
1700 };
1701
1702 let mut result: Vec<Arc<str>> = Vec::new();
1703 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1704
1705 if let Some(ref p) = parent {
1706 if seen.insert(p.to_string()) {
1707 result.push(Arc::clone(p));
1708 }
1709 for a in self.ensure_finalized(p).iter() {
1710 if seen.insert(a.to_string()) {
1711 result.push(Arc::clone(a));
1712 }
1713 }
1714 }
1715 for iface in &interfaces {
1716 if seen.insert(iface.to_string()) {
1717 result.push(Arc::clone(iface));
1718 }
1719 for a in self.ensure_finalized(iface).iter() {
1720 if seen.insert(a.to_string()) {
1721 result.push(Arc::clone(a));
1722 }
1723 }
1724 }
1725 for t in traits {
1726 if seen.insert(t.to_string()) {
1727 result.push(t);
1728 }
1729 }
1730
1731 result
1732 }
1733
1734 fn compute_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1737 let extends = match self.interfaces.get(fqcn) {
1738 Some(iface) => iface.extends.clone(),
1739 None => return vec![],
1740 };
1741
1742 let mut result: Vec<Arc<str>> = Vec::new();
1743 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1744
1745 for e in &extends {
1746 if seen.insert(e.to_string()) {
1747 result.push(Arc::clone(e));
1748 }
1749 for a in self.ensure_finalized(e).iter() {
1750 if seen.insert(a.to_string()) {
1751 result.push(Arc::clone(a));
1752 }
1753 }
1754 }
1755
1756 result
1757 }
1758}
1759
1760pub struct CodebaseBuilder {
1772 cb: Codebase,
1773}
1774
1775impl CodebaseBuilder {
1776 pub fn new() -> Self {
1777 Self {
1778 cb: Codebase::new(),
1779 }
1780 }
1781
1782 pub fn add(&mut self, slice: crate::storage::StubSlice) {
1785 self.cb.inject_stub_slice(slice);
1786 }
1787
1788 pub fn finalize(self) -> Codebase {
1790 self.cb.finalize();
1791 self.cb
1792 }
1793
1794 pub fn codebase(&self) -> &Codebase {
1796 &self.cb
1797 }
1798}
1799
1800impl Default for CodebaseBuilder {
1801 fn default() -> Self {
1802 Self::new()
1803 }
1804}
1805
1806pub fn codebase_from_parts(parts: Vec<crate::storage::StubSlice>) -> Codebase {
1808 let mut b = CodebaseBuilder::new();
1809 for p in parts {
1810 b.add(p);
1811 }
1812 b.finalize()
1813}
1814
1815#[cfg(test)]
1816mod tests {
1817 use super::*;
1818
1819 fn arc(s: &str) -> Arc<str> {
1820 Arc::from(s)
1821 }
1822
1823 #[test]
1824 fn method_referenced_at_groups_spans_by_file() {
1825 let cb = Codebase::new();
1826 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
1827 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 10, 15);
1828 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 2, 0, 5);
1829
1830 let locs = cb.get_reference_locations("Foo::bar");
1831 let files: std::collections::HashSet<&str> =
1832 locs.iter().map(|(f, ..)| f.as_ref()).collect();
1833 assert_eq!(files.len(), 2, "two files, not three spans");
1834 assert!(locs.contains(&(arc("a.php"), 1, 0, 5)));
1835 assert!(locs.contains(&(arc("a.php"), 1, 10, 15)));
1836 assert_eq!(
1837 locs.iter().filter(|(f, ..)| f.as_ref() == "a.php").count(),
1838 2
1839 );
1840 assert!(locs.contains(&(arc("b.php"), 2, 0, 5)));
1841 assert!(
1842 cb.is_method_referenced("Foo", "bar"),
1843 "DashSet also updated"
1844 );
1845 }
1846
1847 #[test]
1848 fn duplicate_spans_are_deduplicated() {
1849 let cb = Codebase::new();
1850 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
1852 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
1853
1854 let count = cb
1855 .get_reference_locations("Foo::bar")
1856 .iter()
1857 .filter(|(f, ..)| f.as_ref() == "a.php")
1858 .count();
1859 assert_eq!(count, 1, "duplicate span deduplicated");
1860 }
1861
1862 #[test]
1863 fn method_key_is_lowercased() {
1864 let cb = Codebase::new();
1865 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 1, 0, 3);
1866 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
1867 }
1868
1869 #[test]
1870 fn property_referenced_at_records_location() {
1871 let cb = Codebase::new();
1872 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 1, 5, 10);
1873
1874 assert!(cb
1875 .get_reference_locations("Bar::count")
1876 .contains(&(arc("x.php"), 1, 5, 10)));
1877 assert!(cb.is_property_referenced("Bar", "count"));
1878 }
1879
1880 #[test]
1881 fn function_referenced_at_records_location() {
1882 let cb = Codebase::new();
1883 cb.mark_function_referenced_at("my_fn", arc("a.php"), 1, 10, 15);
1884
1885 assert!(cb
1886 .get_reference_locations("my_fn")
1887 .contains(&(arc("a.php"), 1, 10, 15)));
1888 assert!(cb.is_function_referenced("my_fn"));
1889 }
1890
1891 #[test]
1892 fn class_referenced_at_records_location() {
1893 let cb = Codebase::new();
1894 cb.mark_class_referenced_at("Foo", arc("a.php"), 1, 5, 8);
1895
1896 assert!(cb
1897 .get_reference_locations("Foo")
1898 .contains(&(arc("a.php"), 1, 5, 8)));
1899 }
1900
1901 #[test]
1902 fn get_reference_locations_flattens_all_files() {
1903 let cb = Codebase::new();
1904 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1905 cb.mark_function_referenced_at("fn1", arc("b.php"), 2, 0, 5);
1906
1907 let mut locs = cb.get_reference_locations("fn1");
1908 locs.sort_by_key(|&(_, line, col, _)| (line, col));
1909 assert_eq!(locs.len(), 2);
1910 assert_eq!(locs[0], (arc("a.php"), 1, 0, 5));
1911 assert_eq!(locs[1], (arc("b.php"), 2, 0, 5));
1912 }
1913
1914 #[test]
1915 fn replay_reference_locations_restores_index() {
1916 let cb = Codebase::new();
1917 let locs = vec![
1918 ("Foo::bar".to_string(), 1u32, 0u16, 5u16),
1919 ("Foo::bar".to_string(), 1, 10, 15),
1920 ("greet".to_string(), 2, 0, 5),
1921 ];
1922 cb.replay_reference_locations(arc("a.php"), &locs);
1923
1924 let bar_locs = cb.get_reference_locations("Foo::bar");
1925 assert!(bar_locs.contains(&(arc("a.php"), 1, 0, 5)));
1926 assert!(bar_locs.contains(&(arc("a.php"), 1, 10, 15)));
1927
1928 assert!(cb
1929 .get_reference_locations("greet")
1930 .contains(&(arc("a.php"), 2, 0, 5)));
1931
1932 assert!(cb.file_has_symbol_references("a.php"));
1933 }
1934
1935 #[test]
1936 fn remove_file_clears_its_spans_only() {
1937 let cb = Codebase::new();
1938 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1939 cb.mark_function_referenced_at("fn1", arc("b.php"), 1, 10, 15);
1940
1941 cb.remove_file_definitions("a.php");
1942
1943 let locs = cb.get_reference_locations("fn1");
1944 assert!(
1945 !locs.iter().any(|(f, ..)| f.as_ref() == "a.php"),
1946 "a.php spans removed"
1947 );
1948 assert!(
1949 locs.contains(&(arc("b.php"), 1, 10, 15)),
1950 "b.php spans untouched"
1951 );
1952 assert!(!cb.file_has_symbol_references("a.php"));
1953 }
1954
1955 #[test]
1956 fn remove_file_does_not_affect_other_files() {
1957 let cb = Codebase::new();
1958 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 1, 4);
1959 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 1, 7, 10);
1960
1961 cb.remove_file_definitions("x.php");
1962
1963 let locs = cb.get_reference_locations("Cls::prop");
1964 assert!(!locs.iter().any(|(f, ..)| f.as_ref() == "x.php"));
1965 assert!(locs.contains(&(arc("y.php"), 1, 7, 10)));
1966 }
1967
1968 #[test]
1969 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1970 let cb = Codebase::new();
1971 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1972
1973 cb.remove_file_definitions("ghost.php");
1975
1976 assert!(cb
1978 .get_reference_locations("fn1")
1979 .contains(&(arc("a.php"), 1, 0, 5)));
1980 assert!(!cb.file_has_symbol_references("ghost.php"));
1981 }
1982
1983 #[test]
1984 fn replay_reference_locations_with_empty_list_is_noop() {
1985 let cb = Codebase::new();
1986 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1987
1988 cb.replay_reference_locations(arc("b.php"), &[]);
1990
1991 assert!(
1992 !cb.file_has_symbol_references("b.php"),
1993 "empty replay must not create a file entry"
1994 );
1995 assert!(
1996 cb.get_reference_locations("fn1")
1997 .contains(&(arc("a.php"), 1, 0, 5)),
1998 "existing spans untouched"
1999 );
2000 }
2001
2002 #[test]
2003 fn replay_reference_locations_twice_does_not_duplicate_spans() {
2004 let cb = Codebase::new();
2005 let locs = vec![("fn1".to_string(), 1u32, 0u16, 5u16)];
2006
2007 cb.replay_reference_locations(arc("a.php"), &locs);
2008 cb.replay_reference_locations(arc("a.php"), &locs);
2009
2010 let count = cb
2011 .get_reference_locations("fn1")
2012 .iter()
2013 .filter(|(f, ..)| f.as_ref() == "a.php")
2014 .count();
2015 assert_eq!(
2016 count, 1,
2017 "replaying the same location twice must not create duplicate spans"
2018 );
2019 }
2020
2021 fn make_fn(fqn: &str, short_name: &str) -> crate::storage::FunctionStorage {
2026 crate::storage::FunctionStorage {
2027 fqn: Arc::from(fqn),
2028 short_name: Arc::from(short_name),
2029 params: vec![],
2030 return_type: None,
2031 inferred_return_type: None,
2032 template_params: vec![],
2033 assertions: vec![],
2034 throws: vec![],
2035 deprecated: None,
2036 is_pure: false,
2037 location: None,
2038 }
2039 }
2040
2041 #[test]
2042 fn inject_stub_slice_later_injection_overwrites_earlier() {
2043 let cb = Codebase::new();
2044
2045 cb.inject_stub_slice(crate::storage::StubSlice {
2046 functions: vec![make_fn("strlen", "phpstorm_version")],
2047 file: Some(Arc::from("phpstorm/standard.php")),
2048 ..Default::default()
2049 });
2050 assert_eq!(
2051 cb.functions.get("strlen").unwrap().short_name.as_ref(),
2052 "phpstorm_version"
2053 );
2054
2055 cb.inject_stub_slice(crate::storage::StubSlice {
2056 functions: vec![make_fn("strlen", "custom_version")],
2057 file: Some(Arc::from("stubs/standard/basic.php")),
2058 ..Default::default()
2059 });
2060
2061 assert_eq!(
2062 cb.functions.get("strlen").unwrap().short_name.as_ref(),
2063 "custom_version",
2064 "custom stub must overwrite phpstorm stub"
2065 );
2066 assert_eq!(
2067 cb.symbol_to_file.get("strlen").unwrap().as_ref(),
2068 "stubs/standard/basic.php",
2069 "symbol_to_file must point to the overriding file"
2070 );
2071 }
2072
2073 #[test]
2074 fn inject_stub_slice_constants_not_added_to_symbol_to_file() {
2075 let cb = Codebase::new();
2076
2077 cb.inject_stub_slice(crate::storage::StubSlice {
2078 constants: vec![(Arc::from("PHP_EOL"), mir_types::Union::empty())],
2079 file: Some(Arc::from("stubs/core/constants.php")),
2080 ..Default::default()
2081 });
2082
2083 assert!(
2084 cb.constants.contains_key("PHP_EOL"),
2085 "constant must be registered in constants map"
2086 );
2087 assert!(
2088 !cb.symbol_to_file.contains_key("PHP_EOL"),
2089 "constants must not appear in symbol_to_file — go-to-definition is not supported for them"
2090 );
2091 }
2092
2093 #[test]
2094 fn remove_file_definitions_purges_injected_global_vars() {
2095 let cb = Codebase::new();
2096
2097 cb.inject_stub_slice(crate::storage::StubSlice {
2098 global_vars: vec![(Arc::from("db_connection"), mir_types::Union::empty())],
2099 file: Some(Arc::from("src/bootstrap.php")),
2100 ..Default::default()
2101 });
2102 assert!(
2103 cb.global_vars.contains_key("db_connection"),
2104 "global var must be registered after injection"
2105 );
2106
2107 cb.remove_file_definitions("src/bootstrap.php");
2108
2109 assert!(
2110 !cb.global_vars.contains_key("db_connection"),
2111 "global var must be removed when its defining file is removed"
2112 );
2113 }
2114
2115 #[test]
2116 fn inject_stub_slice_without_file_discards_global_vars() {
2117 let cb = Codebase::new();
2118
2119 cb.inject_stub_slice(crate::storage::StubSlice {
2120 global_vars: vec![(Arc::from("orphan_var"), mir_types::Union::empty())],
2121 file: None,
2122 ..Default::default()
2123 });
2124
2125 assert!(
2126 !cb.global_vars.contains_key("orphan_var"),
2127 "global_vars must not be registered when slice.file is None"
2128 );
2129 }
2130
2131 #[test]
2142 fn inject_stub_slice_populates_file_namespace() {
2143 let cb = Codebase::new();
2147 cb.inject_stub_slice(crate::storage::StubSlice {
2148 file: Some(Arc::from("src/Service.php")),
2149 namespace: Some(Arc::from("App\\Service")),
2150 ..Default::default()
2151 });
2152 assert_eq!(
2153 cb.file_namespaces
2154 .get("src/Service.php")
2155 .as_deref()
2156 .map(|s| s.as_str()),
2157 Some("App\\Service"),
2158 "file_namespaces must be populated when slice carries a namespace"
2159 );
2160
2161 let cb2 = Codebase::new();
2163 cb2.inject_stub_slice(crate::storage::StubSlice {
2164 file: Some(Arc::from("src/global.php")),
2165 namespace: None,
2166 ..Default::default()
2167 });
2168 assert!(
2169 cb2.file_namespaces.is_empty(),
2170 "file_namespaces must not be written when slice.namespace is None"
2171 );
2172 }
2173
2174 #[test]
2175 fn inject_stub_slice_populates_file_imports() {
2176 let cb = Codebase::new();
2180 let mut imports = std::collections::HashMap::new();
2181 imports.insert("Entity".to_string(), "App\\Model\\Entity".to_string());
2182 imports.insert(
2183 "Repo".to_string(),
2184 "App\\Repository\\EntityRepo".to_string(),
2185 );
2186 cb.inject_stub_slice(crate::storage::StubSlice {
2187 file: Some(Arc::from("src/Handler.php")),
2188 imports,
2189 ..Default::default()
2190 });
2191 let stored = cb.file_imports.get("src/Handler.php").unwrap();
2192 assert_eq!(
2193 stored.get("Entity").map(|s| s.as_str()),
2194 Some("App\\Model\\Entity")
2195 );
2196 assert_eq!(
2197 stored.get("Repo").map(|s| s.as_str()),
2198 Some("App\\Repository\\EntityRepo")
2199 );
2200
2201 let cb2 = Codebase::new();
2203 cb2.inject_stub_slice(crate::storage::StubSlice {
2204 file: Some(Arc::from("src/no_imports.php")),
2205 imports: std::collections::HashMap::new(),
2206 ..Default::default()
2207 });
2208 assert!(
2209 cb2.file_imports.is_empty(),
2210 "file_imports must not be written when slice.imports is empty"
2211 );
2212 }
2213
2214 #[test]
2215 fn inject_stub_slice_skips_namespace_and_imports_when_no_file() {
2216 let cb = Codebase::new();
2220 let mut imports = std::collections::HashMap::new();
2221 imports.insert("Foo".to_string(), "Bar\\Foo".to_string());
2222 cb.inject_stub_slice(crate::storage::StubSlice {
2223 file: None,
2224 namespace: Some(Arc::from("Bar")),
2225 imports,
2226 ..Default::default()
2227 });
2228 assert!(
2229 cb.file_namespaces.is_empty(),
2230 "file_namespaces must not be written when slice.file is None"
2231 );
2232 assert!(
2233 cb.file_imports.is_empty(),
2234 "file_imports must not be written when slice.file is None"
2235 );
2236 }
2237
2238 #[test]
2239 fn remove_file_definitions_purges_file_namespaces_and_imports() {
2240 let cb = Codebase::new();
2246 let mut imports = std::collections::HashMap::new();
2247 imports.insert("Entity".to_string(), "App\\Model\\Entity".to_string());
2248 cb.inject_stub_slice(crate::storage::StubSlice {
2249 file: Some(Arc::from("src/Handler.php")),
2250 namespace: Some(Arc::from("App\\Service")),
2251 imports,
2252 ..Default::default()
2253 });
2254 assert!(
2255 cb.file_namespaces.contains_key("src/Handler.php"),
2256 "setup: namespace must be present"
2257 );
2258 assert!(
2259 cb.file_imports.contains_key("src/Handler.php"),
2260 "setup: imports must be present"
2261 );
2262
2263 cb.remove_file_definitions("src/Handler.php");
2264
2265 assert!(
2266 !cb.file_namespaces.contains_key("src/Handler.php"),
2267 "file_namespaces entry must be removed when its defining file is removed"
2268 );
2269 assert!(
2270 !cb.file_imports.contains_key("src/Handler.php"),
2271 "file_imports entry must be removed when its defining file is removed"
2272 );
2273 }
2274
2275 fn bare_class(fqcn: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2280 use indexmap::IndexMap;
2281 ClassStorage {
2282 fqcn: arc(fqcn),
2283 short_name: arc(fqcn),
2284 parent: None,
2285 interfaces: vec![],
2286 traits: vec![],
2287 own_methods: IndexMap::new(),
2288 own_properties: IndexMap::new(),
2289 own_constants: IndexMap::new(),
2290 mixins,
2291 template_params: vec![],
2292 extends_type_args: vec![],
2293 implements_type_args: vec![],
2294 is_abstract: false,
2295 is_final: false,
2296 is_readonly: false,
2297 all_parents: vec![],
2298 deprecated: None,
2299 is_internal: false,
2300 location: None,
2301 type_aliases: std::collections::HashMap::new(),
2302 pending_import_types: vec![],
2303 }
2304 }
2305
2306 fn class_with_method(fqcn: &str, method_name: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2307 use crate::storage::{MethodStorage, Visibility};
2308 use indexmap::IndexMap;
2309 let mut methods = IndexMap::new();
2310 methods.insert(
2311 arc(method_name),
2312 Arc::new(MethodStorage {
2313 name: arc(method_name),
2314 fqcn: arc(fqcn),
2315 params: vec![],
2316 return_type: None,
2317 inferred_return_type: None,
2318 visibility: Visibility::Public,
2319 is_static: false,
2320 is_abstract: false,
2321 is_final: false,
2322 is_constructor: false,
2323 template_params: vec![],
2324 assertions: vec![],
2325 throws: vec![],
2326 deprecated: None,
2327 is_internal: false,
2328 is_pure: false,
2329 location: None,
2330 }),
2331 );
2332 let mut cls = bare_class(fqcn, mixins);
2333 cls.own_methods = methods;
2334 cls
2335 }
2336
2337 fn class_with_property(fqcn: &str, prop_name: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2338 use crate::storage::{PropertyStorage, Visibility};
2339 use indexmap::IndexMap;
2340 let mut props = IndexMap::new();
2341 props.insert(
2342 arc(prop_name),
2343 PropertyStorage {
2344 name: arc(prop_name),
2345 ty: None,
2346 inferred_ty: None,
2347 visibility: Visibility::Public,
2348 is_static: false,
2349 is_readonly: false,
2350 default: None,
2351 location: None,
2352 },
2353 );
2354 let mut cls = bare_class(fqcn, mixins);
2355 cls.own_properties = props;
2356 cls
2357 }
2358
2359 #[test]
2360 fn get_method_two_way_mixin_cycle_returns_none() {
2361 let cb = Codebase::new();
2362 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2363 cb.classes.insert(arc("B"), bare_class("B", vec![arc("A")]));
2364 assert!(cb.get_method("A", "missing").is_none());
2365 }
2366
2367 #[test]
2368 fn get_method_self_mixin_returns_none() {
2369 let cb = Codebase::new();
2370 cb.classes.insert(arc("A"), bare_class("A", vec![arc("A")]));
2371 assert!(cb.get_method("A", "missing").is_none());
2372 }
2373
2374 #[test]
2375 fn get_method_three_way_cycle_returns_none() {
2376 let cb = Codebase::new();
2377 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2378 cb.classes.insert(arc("B"), bare_class("B", vec![arc("C")]));
2379 cb.classes.insert(arc("C"), bare_class("C", vec![arc("A")]));
2380 assert!(cb.get_method("A", "missing").is_none());
2381 }
2382
2383 #[test]
2384 fn get_method_resolves_through_mixin_when_no_cycle() {
2385 let cb = Codebase::new();
2386 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2387 cb.classes
2388 .insert(arc("B"), class_with_method("B", "fromB", vec![]));
2389 assert!(cb.get_method("A", "fromB").is_some());
2390 }
2391
2392 #[test]
2393 fn get_method_own_method_shadows_mixin() {
2394 let cb = Codebase::new();
2395 cb.classes
2396 .insert(arc("A"), class_with_method("A", "foo", vec![arc("B")]));
2397 cb.classes
2398 .insert(arc("B"), class_with_method("B", "foo", vec![]));
2399 let m = cb.get_method("A", "foo").unwrap();
2400 assert_eq!(m.fqcn.as_ref(), "A");
2401 }
2402
2403 #[test]
2404 fn get_method_mixin_nonexistent_class_returns_none() {
2405 let cb = Codebase::new();
2406 cb.classes
2407 .insert(arc("A"), bare_class("A", vec![arc("Ghost")]));
2408 assert!(cb.get_method("A", "foo").is_none());
2409 }
2410
2411 #[test]
2412 fn get_property_two_way_mixin_cycle_returns_none() {
2413 let cb = Codebase::new();
2414 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2415 cb.classes.insert(arc("B"), bare_class("B", vec![arc("A")]));
2416 assert!(cb.get_property("A", "missing").is_none());
2417 }
2418
2419 #[test]
2420 fn get_method_diamond_mixin_finds_method_via_first_path() {
2421 let cb = Codebase::new();
2425 cb.classes
2426 .insert(arc("A"), bare_class("A", vec![arc("B"), arc("C")]));
2427 cb.classes.insert(arc("B"), bare_class("B", vec![arc("D")]));
2428 cb.classes.insert(arc("C"), bare_class("C", vec![arc("D")]));
2429 cb.classes
2430 .insert(arc("D"), class_with_method("D", "foo", vec![]));
2431 assert!(cb.get_method("A", "foo").is_some());
2432 }
2433
2434 #[test]
2435 fn get_method_mixin_on_ancestor_is_followed() {
2436 let cb = Codebase::new();
2437 cb.classes.insert(
2438 arc("Child"),
2439 ClassStorage {
2440 parent: Some(arc("Parent")),
2441 ..bare_class("Child", vec![])
2442 },
2443 );
2444 cb.classes
2445 .insert(arc("Parent"), bare_class("Parent", vec![arc("Mixin")]));
2446 cb.classes.insert(
2447 arc("Mixin"),
2448 class_with_method("Mixin", "fromMixin", vec![]),
2449 );
2450 assert!(cb.get_method("Child", "fromMixin").is_some());
2451 assert!(cb.get_method("Parent", "fromMixin").is_some());
2452 }
2453
2454 #[test]
2455 fn get_method_mixin_on_transitive_ancestor_is_followed() {
2456 let cb = Codebase::new();
2457 cb.classes.insert(
2459 arc("A"),
2460 ClassStorage {
2461 parent: Some(arc("B")),
2462 ..bare_class("A", vec![])
2463 },
2464 );
2465 cb.classes.insert(
2466 arc("B"),
2467 ClassStorage {
2468 parent: Some(arc("C")),
2469 ..bare_class("B", vec![])
2470 },
2471 );
2472 cb.classes.insert(arc("C"), bare_class("C", vec![arc("D")]));
2473 cb.classes
2474 .insert(arc("D"), class_with_method("D", "foo", vec![]));
2475 assert!(cb.get_method("A", "foo").is_some());
2476 }
2477
2478 #[test]
2479 fn get_property_resolves_through_mixin_when_no_cycle() {
2480 let cb = Codebase::new();
2481 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2482 cb.classes
2483 .insert(arc("B"), class_with_property("B", "title", vec![]));
2484 assert!(cb.get_property("A", "title").is_some());
2485 }
2486
2487 fn class_with_parent(fqcn: &str, parent: &str) -> ClassStorage {
2492 ClassStorage {
2493 parent: Some(arc(parent)),
2494 ..bare_class(fqcn, vec![])
2495 }
2496 }
2497
2498 #[test]
2499 fn ensure_finalized_returns_ancestors_for_known_class() {
2500 let cb = Codebase::new();
2501 cb.classes.insert(arc("A"), bare_class("A", vec![]));
2502 cb.classes.insert(arc("B"), class_with_parent("B", "A"));
2503
2504 let parents = cb.ensure_finalized("B");
2505 assert_eq!(parents.as_ref(), &[arc("A")]);
2506 }
2507
2508 #[test]
2509 fn ensure_finalized_memoizes_result() {
2510 let cb = Codebase::new();
2511 cb.classes.insert(arc("A"), bare_class("A", vec![]));
2512 cb.classes.insert(arc("B"), class_with_parent("B", "A"));
2513
2514 let first = cb.ensure_finalized("B");
2515 let second = cb.ensure_finalized("B");
2516 assert!(Arc::ptr_eq(&first, &second));
2518 }
2519
2520 #[test]
2521 fn ensure_finalized_returns_empty_for_unknown_fqcn() {
2522 let cb = Codebase::new();
2523 let parents = cb.ensure_finalized("NoSuchClass");
2524 assert!(parents.is_empty());
2525 }
2526
2527 #[test]
2528 fn ensure_finalized_populates_all_parents_on_storage() {
2529 let cb = Codebase::new();
2530 cb.classes.insert(arc("A"), bare_class("A", vec![]));
2531 cb.classes.insert(arc("B"), class_with_parent("B", "A"));
2532
2533 cb.ensure_finalized("B");
2534 let stored = cb.classes.get("B").unwrap().all_parents.clone();
2535 assert_eq!(stored, vec![arc("A")]);
2536 }
2537
2538 #[test]
2539 fn ensure_finalized_cycle_does_not_loop() {
2540 let cb = Codebase::new();
2542 cb.classes.insert(
2543 arc("A"),
2544 ClassStorage {
2545 parent: Some(arc("B")),
2546 ..bare_class("A", vec![])
2547 },
2548 );
2549 cb.classes.insert(
2550 arc("B"),
2551 ClassStorage {
2552 parent: Some(arc("A")),
2553 ..bare_class("B", vec![])
2554 },
2555 );
2556
2557 let a_parents = cb.ensure_finalized("A");
2559 let b_parents = cb.ensure_finalized("B");
2560
2561 assert!(
2564 a_parents.iter().any(|p| p.as_ref() == "B"),
2565 "A should have B as ancestor"
2566 );
2567 assert!(
2568 b_parents.iter().any(|p| p.as_ref() == "A"),
2569 "B should have A as ancestor"
2570 );
2571 }
2572
2573 #[test]
2574 fn ensure_finalized_evicted_by_remove_file_definitions() {
2575 let cb = Codebase::new();
2576 cb.classes.insert(arc("A"), bare_class("A", vec![]));
2577 cb.classes.insert(arc("B"), class_with_parent("B", "A"));
2578 let file: Arc<str> = arc("test.php");
2580 cb.symbol_to_file.insert(arc("A"), file.clone());
2581 cb.symbol_to_file.insert(arc("B"), file.clone());
2582
2583 let before = cb.ensure_finalized("B");
2585 assert!(!before.is_empty());
2586
2587 cb.remove_file_definitions("test.php");
2589
2590 let after = cb.ensure_finalized("B");
2592 assert!(after.is_empty());
2593 }
2594}