1use std::sync::Arc;
2
3use dashmap::{DashMap, DashSet};
4
5use crate::interner::Interner;
6
7type ReferenceLocations = DashMap<u32, Vec<(u32, u32, u16, u16)>>;
16
17use crate::storage::{
18 ClassStorage, EnumStorage, FunctionStorage, InterfaceStorage, MethodStorage, TraitStorage,
19};
20use mir_types::Union;
21
22#[inline]
31fn lookup_method<'a>(
32 map: &'a indexmap::IndexMap<Arc<str>, Arc<MethodStorage>>,
33 name: &str,
34) -> Option<&'a Arc<MethodStorage>> {
35 map.get(name).or_else(|| {
36 map.iter()
37 .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
38 .map(|(_, v)| v)
39 })
40}
41
42#[inline]
48fn record_ref(
49 sym_locs: &ReferenceLocations,
50 file_refs: &DashMap<u32, Vec<u32>>,
51 sym_id: u32,
52 file_id: u32,
53 line: u32,
54 col_start: u16,
55 col_end: u16,
56) {
57 {
58 let mut entries = sym_locs.entry(sym_id).or_default();
59 let span = (file_id, line, col_start, col_end);
60 if !entries.contains(&span) {
61 entries.push(span);
62 }
63 }
64 {
65 let mut refs = file_refs.entry(file_id).or_default();
66 if !refs.contains(&sym_id) {
67 refs.push(sym_id);
68 }
69 }
70}
71
72#[derive(Debug, Default)]
86struct CompactRefIndex {
87 entries: Vec<(u32, u32, u32, u16, u16)>,
90 sym_offsets: Vec<u32>,
92 by_file: Vec<u32>,
95 file_offsets: Vec<u32>,
97}
98
99struct ClassInheritance {
104 parent: Option<Arc<str>>,
105 interfaces: Vec<Arc<str>>, traits: Vec<Arc<str>>, all_parents: Vec<Arc<str>>,
108}
109
110struct InterfaceInheritance {
111 extends: Vec<Arc<str>>, all_parents: Vec<Arc<str>>,
113}
114
115pub struct StructuralSnapshot {
123 classes: std::collections::HashMap<Arc<str>, ClassInheritance>,
124 interfaces: std::collections::HashMap<Arc<str>, InterfaceInheritance>,
125}
126
127#[derive(Debug, Default)]
132pub struct Codebase {
133 pub classes: DashMap<Arc<str>, ClassStorage>,
134 pub interfaces: DashMap<Arc<str>, InterfaceStorage>,
135 pub traits: DashMap<Arc<str>, TraitStorage>,
136 pub enums: DashMap<Arc<str>, EnumStorage>,
137 pub functions: DashMap<Arc<str>, FunctionStorage>,
138 pub constants: DashMap<Arc<str>, Union>,
139
140 pub global_vars: DashMap<Arc<str>, Union>,
143 file_global_vars: DashMap<Arc<str>, Vec<Arc<str>>>,
146
147 referenced_methods: DashSet<u32>,
150 referenced_properties: DashSet<u32>,
152 referenced_functions: DashSet<u32>,
154
155 pub symbol_interner: Interner,
158 pub file_interner: Interner,
160
161 symbol_reference_locations: ReferenceLocations,
164 file_symbol_references: DashMap<u32, Vec<u32>>,
169
170 compact_ref_index: std::sync::RwLock<Option<CompactRefIndex>>,
174 is_compacted: std::sync::atomic::AtomicBool,
177
178 pub symbol_to_file: DashMap<Arc<str>, Arc<str>>,
181
182 pub known_symbols: DashSet<Arc<str>>,
186
187 pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
195 pub file_namespaces: DashMap<Arc<str>, String>,
203
204 finalized: std::sync::atomic::AtomicBool,
206}
207
208impl Codebase {
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 pub fn inject_stub_slice(&self, slice: crate::storage::StubSlice) {
232 let file = slice.file.clone();
233 for cls in slice.classes {
234 if let Some(f) = &file {
235 self.symbol_to_file.insert(cls.fqcn.clone(), f.clone());
236 }
237 self.classes.insert(cls.fqcn.clone(), cls);
238 }
239 for iface in slice.interfaces {
240 if let Some(f) = &file {
241 self.symbol_to_file.insert(iface.fqcn.clone(), f.clone());
242 }
243 self.interfaces.insert(iface.fqcn.clone(), iface);
244 }
245 for tr in slice.traits {
246 if let Some(f) = &file {
247 self.symbol_to_file.insert(tr.fqcn.clone(), f.clone());
248 }
249 self.traits.insert(tr.fqcn.clone(), tr);
250 }
251 for en in slice.enums {
252 if let Some(f) = &file {
253 self.symbol_to_file.insert(en.fqcn.clone(), f.clone());
254 }
255 self.enums.insert(en.fqcn.clone(), en);
256 }
257 for func in slice.functions {
258 if let Some(f) = &file {
259 self.symbol_to_file.insert(func.fqn.clone(), f.clone());
260 }
261 self.functions.insert(func.fqn.clone(), func);
262 }
263 for (name, ty) in slice.constants {
264 self.constants.insert(name, ty);
265 }
266 if let Some(f) = &file {
267 for (name, ty) in slice.global_vars {
268 self.register_global_var(f, name, ty);
269 }
270 if let Some(ns) = slice.namespace {
271 self.file_namespaces.insert(f.clone(), ns.to_string());
272 }
273 if !slice.imports.is_empty() {
274 self.file_imports.insert(f.clone(), slice.imports);
275 }
276 }
277 }
278
279 pub fn compact_reference_index(&self) {
296 let mut entries: Vec<(u32, u32, u32, u16, u16)> = self
298 .symbol_reference_locations
299 .iter()
300 .flat_map(|entry| {
301 let sym_id = *entry.key();
302 entry
303 .value()
304 .iter()
305 .map(move |&(file_id, line, col_start, col_end)| {
306 (sym_id, file_id, line, col_start, col_end)
307 })
308 .collect::<Vec<_>>()
309 })
310 .collect();
311
312 if entries.is_empty() {
313 return;
314 }
315
316 entries.sort_unstable();
318 entries.dedup();
319
320 let n = entries.len();
321
322 let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
324 let mut sym_offsets = vec![0u32; max_sym + 2];
325 for &(sym_id, ..) in &entries {
326 sym_offsets[sym_id as usize + 1] += 1;
327 }
328 for i in 1..sym_offsets.len() {
329 sym_offsets[i] += sym_offsets[i - 1];
330 }
331
332 let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
336 let mut by_file: Vec<u32> = (0..n as u32).collect();
337 by_file.sort_unstable_by_key(|&i| {
338 let (sym_id, file_id, line, col_start, col_end) = entries[i as usize];
339 (file_id, sym_id, line, col_start, col_end)
340 });
341
342 let mut file_offsets = vec![0u32; max_file + 2];
343 for &idx in &by_file {
344 let file_id = entries[idx as usize].1;
345 file_offsets[file_id as usize + 1] += 1;
346 }
347 for i in 1..file_offsets.len() {
348 file_offsets[i] += file_offsets[i - 1];
349 }
350
351 *self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
352 entries,
353 sym_offsets,
354 by_file,
355 file_offsets,
356 });
357 self.is_compacted
358 .store(true, std::sync::atomic::Ordering::Release);
359
360 self.symbol_reference_locations.clear();
362 self.file_symbol_references.clear();
363 }
364
365 fn ensure_expanded(&self) {
371 if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
373 return;
374 }
375 let mut guard = self.compact_ref_index.write().unwrap();
377 if let Some(ci) = guard.take() {
378 for &(sym_id, file_id, line, col_start, col_end) in &ci.entries {
379 record_ref(
380 &self.symbol_reference_locations,
381 &self.file_symbol_references,
382 sym_id,
383 file_id,
384 line,
385 col_start,
386 col_end,
387 );
388 }
389 self.is_compacted
390 .store(false, std::sync::atomic::Ordering::Release);
391 }
392 }
394
395 pub fn invalidate_finalization(&self) {
401 self.finalized
402 .store(false, std::sync::atomic::Ordering::SeqCst);
403 }
404
405 pub fn remove_file_definitions(&self, file_path: &str) {
416 let symbols: Vec<Arc<str>> = self
418 .symbol_to_file
419 .iter()
420 .filter(|entry| entry.value().as_ref() == file_path)
421 .map(|entry| entry.key().clone())
422 .collect();
423
424 for sym in &symbols {
426 self.classes.remove(sym.as_ref());
427 self.interfaces.remove(sym.as_ref());
428 self.traits.remove(sym.as_ref());
429 self.enums.remove(sym.as_ref());
430 self.functions.remove(sym.as_ref());
431 self.constants.remove(sym.as_ref());
432 self.symbol_to_file.remove(sym.as_ref());
433 self.known_symbols.remove(sym.as_ref());
434 }
435
436 self.file_imports.remove(file_path);
438 self.file_namespaces.remove(file_path);
439
440 if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
442 for name in var_names {
443 self.global_vars.remove(name.as_ref());
444 }
445 }
446
447 self.ensure_expanded();
449
450 if let Some(file_id) = self.file_interner.get_id(file_path) {
453 if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
454 for sym_id in sym_ids {
455 if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
456 entries.retain(|&(fid, ..)| fid != file_id);
457 }
458 }
459 }
460 }
461
462 self.invalidate_finalization();
463 }
464
465 pub fn file_structural_snapshot(&self, file_path: &str) -> StructuralSnapshot {
477 let symbols: Vec<Arc<str>> = self
478 .symbol_to_file
479 .iter()
480 .filter(|e| e.value().as_ref() == file_path)
481 .map(|e| e.key().clone())
482 .collect();
483
484 let mut classes = std::collections::HashMap::new();
485 let mut interfaces = std::collections::HashMap::new();
486
487 for sym in symbols {
488 if let Some(cls) = self.classes.get(sym.as_ref()) {
489 let mut ifaces = cls.interfaces.clone();
490 ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
491 let mut traits = cls.traits.clone();
492 traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
493 classes.insert(
494 sym,
495 ClassInheritance {
496 parent: cls.parent.clone(),
497 interfaces: ifaces,
498 traits,
499 all_parents: cls.all_parents.clone(),
500 },
501 );
502 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
503 let mut extends = iface.extends.clone();
504 extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
505 interfaces.insert(
506 sym,
507 InterfaceInheritance {
508 extends,
509 all_parents: iface.all_parents.clone(),
510 },
511 );
512 }
513 }
514
515 StructuralSnapshot {
516 classes,
517 interfaces,
518 }
519 }
520
521 pub fn structural_unchanged_after_pass1(
527 &self,
528 file_path: &str,
529 old: &StructuralSnapshot,
530 ) -> bool {
531 let symbols: Vec<Arc<str>> = self
532 .symbol_to_file
533 .iter()
534 .filter(|e| e.value().as_ref() == file_path)
535 .map(|e| e.key().clone())
536 .collect();
537
538 let mut seen_classes = 0usize;
539 let mut seen_interfaces = 0usize;
540
541 for sym in &symbols {
542 if let Some(cls) = self.classes.get(sym.as_ref()) {
543 seen_classes += 1;
544 let Some(old_cls) = old.classes.get(sym.as_ref()) else {
545 return false; };
547 if old_cls.parent != cls.parent {
548 return false;
549 }
550 let mut new_ifaces = cls.interfaces.clone();
551 new_ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
552 if old_cls.interfaces != new_ifaces {
553 return false;
554 }
555 let mut new_traits = cls.traits.clone();
556 new_traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
557 if old_cls.traits != new_traits {
558 return false;
559 }
560 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
561 seen_interfaces += 1;
562 let Some(old_iface) = old.interfaces.get(sym.as_ref()) else {
563 return false; };
565 let mut new_extends = iface.extends.clone();
566 new_extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
567 if old_iface.extends != new_extends {
568 return false;
569 }
570 }
571 }
573
574 seen_classes == old.classes.len() && seen_interfaces == old.interfaces.len()
576 }
577
578 pub fn restore_all_parents(&self, file_path: &str, snapshot: &StructuralSnapshot) {
585 let symbols: Vec<Arc<str>> = self
586 .symbol_to_file
587 .iter()
588 .filter(|e| e.value().as_ref() == file_path)
589 .map(|e| e.key().clone())
590 .collect();
591
592 for sym in &symbols {
593 if let Some(old_cls) = snapshot.classes.get(sym.as_ref()) {
594 if let Some(mut cls) = self.classes.get_mut(sym.as_ref()) {
595 cls.all_parents = old_cls.all_parents.clone();
596 }
597 } else if let Some(old_iface) = snapshot.interfaces.get(sym.as_ref()) {
598 if let Some(mut iface) = self.interfaces.get_mut(sym.as_ref()) {
599 iface.all_parents = old_iface.all_parents.clone();
600 }
601 }
602 }
603
604 self.finalized
605 .store(true, std::sync::atomic::Ordering::SeqCst);
606 }
607
608 pub fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
615 self.file_global_vars
616 .entry(file.clone())
617 .or_default()
618 .push(name.clone());
619 self.global_vars.insert(name, ty);
620 }
621
622 pub fn get_property(
628 &self,
629 fqcn: &str,
630 prop_name: &str,
631 ) -> Option<crate::storage::PropertyStorage> {
632 self.get_property_inner(fqcn, prop_name, &mut std::collections::HashSet::new())
633 }
634
635 fn get_property_inner(
636 &self,
637 fqcn: &str,
638 prop_name: &str,
639 visited: &mut std::collections::HashSet<String>,
640 ) -> Option<crate::storage::PropertyStorage> {
641 if !visited.insert(fqcn.to_string()) {
642 return None;
643 }
644 if let Some(cls) = self.classes.get(fqcn) {
646 if let Some(p) = cls.own_properties.get(prop_name) {
647 return Some(p.clone());
648 }
649 let mixins = cls.mixins.clone();
650 drop(cls);
651 for mixin in &mixins {
652 if let Some(p) = self.get_property_inner(mixin.as_ref(), prop_name, visited) {
653 return Some(p);
654 }
655 }
656 }
657
658 let all_parents = {
660 if let Some(cls) = self.classes.get(fqcn) {
661 cls.all_parents.clone()
662 } else {
663 return None;
664 }
665 };
666
667 for ancestor_fqcn in &all_parents {
668 if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
669 if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
670 return Some(p.clone());
671 }
672 let anc_mixins = ancestor_cls.mixins.clone();
673 drop(ancestor_cls);
674 for mixin_fqcn in &anc_mixins {
675 if let Some(p) = self.get_property_inner(mixin_fqcn, prop_name, visited) {
676 return Some(p);
677 }
678 }
679 }
680 }
681
682 let trait_list = {
684 if let Some(cls) = self.classes.get(fqcn) {
685 cls.traits.clone()
686 } else {
687 vec![]
688 }
689 };
690 for trait_fqcn in &trait_list {
691 if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
692 if let Some(p) = tr.own_properties.get(prop_name) {
693 return Some(p.clone());
694 }
695 }
696 }
697
698 None
699 }
700
701 pub fn get_class_constant(
703 &self,
704 fqcn: &str,
705 const_name: &str,
706 ) -> Option<crate::storage::ConstantStorage> {
707 if let Some(cls) = self.classes.get(fqcn) {
709 if let Some(c) = cls.own_constants.get(const_name) {
710 return Some(c.clone());
711 }
712 let all_parents = cls.all_parents.clone();
713 let interfaces = cls.interfaces.clone();
714 let traits = cls.traits.clone();
715 drop(cls);
716
717 for tr_fqcn in &traits {
718 if let Some(tr) = self.traits.get(tr_fqcn.as_ref()) {
719 if let Some(c) = tr.own_constants.get(const_name) {
720 return Some(c.clone());
721 }
722 }
723 }
724
725 for ancestor_fqcn in &all_parents {
726 if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
727 if let Some(c) = ancestor.own_constants.get(const_name) {
728 return Some(c.clone());
729 }
730 }
731 if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
732 if let Some(c) = iface.own_constants.get(const_name) {
733 return Some(c.clone());
734 }
735 }
736 }
737
738 for iface_fqcn in &interfaces {
739 if let Some(iface) = self.interfaces.get(iface_fqcn.as_ref()) {
740 if let Some(c) = iface.own_constants.get(const_name) {
741 return Some(c.clone());
742 }
743 }
744 }
745
746 return None;
747 }
748
749 if let Some(iface) = self.interfaces.get(fqcn) {
751 if let Some(c) = iface.own_constants.get(const_name) {
752 return Some(c.clone());
753 }
754 let parents = iface.all_parents.clone();
755 drop(iface);
756 for p in &parents {
757 if let Some(parent_iface) = self.interfaces.get(p.as_ref()) {
758 if let Some(c) = parent_iface.own_constants.get(const_name) {
759 return Some(c.clone());
760 }
761 }
762 }
763 return None;
764 }
765
766 if let Some(en) = self.enums.get(fqcn) {
768 if let Some(c) = en.own_constants.get(const_name) {
769 return Some(c.clone());
770 }
771 if en.cases.contains_key(const_name) {
772 return Some(crate::storage::ConstantStorage {
773 name: Arc::from(const_name),
774 ty: mir_types::Union::mixed(),
775 visibility: None,
776 is_final: false,
777 location: None,
778 });
779 }
780 return None;
781 }
782
783 if let Some(tr) = self.traits.get(fqcn) {
785 if let Some(c) = tr.own_constants.get(const_name) {
786 return Some(c.clone());
787 }
788 return None;
789 }
790
791 None
792 }
793
794 pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
796 self.get_method_inner(fqcn, method_name, &mut std::collections::HashSet::new())
797 }
798
799 fn get_method_inner(
800 &self,
801 fqcn: &str,
802 method_name: &str,
803 visited: &mut std::collections::HashSet<String>,
804 ) -> Option<Arc<MethodStorage>> {
805 if !visited.insert(fqcn.to_string()) {
806 return None;
807 }
808 let method_lower = method_name.to_lowercase();
810 let method_name = method_lower.as_str();
811
812 if let Some(cls) = self.classes.get(fqcn) {
814 if let Some(m) = lookup_method(&cls.own_methods, method_name) {
816 return Some(Arc::clone(m));
817 }
818 let own_traits = cls.traits.clone();
820 let ancestors = cls.all_parents.clone();
821 let mixins = cls.mixins.clone();
822 drop(cls);
823
824 for mixin_fqcn in &mixins {
826 if let Some(m) = self.get_method_inner(mixin_fqcn, method_name, visited) {
827 return Some(m);
828 }
829 }
830
831 for tr_fqcn in &own_traits {
833 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
834 return Some(m);
835 }
836 }
837
838 for ancestor_fqcn in &ancestors {
840 if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
841 if let Some(m) = lookup_method(&anc.own_methods, method_name) {
842 return Some(Arc::clone(m));
843 }
844 let anc_traits = anc.traits.clone();
845 let anc_mixins = anc.mixins.clone();
846 drop(anc);
847 for tr_fqcn in &anc_traits {
848 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
849 return Some(m);
850 }
851 }
852 for mixin_fqcn in &anc_mixins {
853 if let Some(m) = self.get_method_inner(mixin_fqcn, method_name, visited) {
854 return Some(m);
855 }
856 }
857 } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
858 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
859 let mut ms = (**m).clone();
860 ms.is_abstract = true;
861 return Some(Arc::new(ms));
862 }
863 }
864 }
866 return None;
867 }
868
869 if let Some(iface) = self.interfaces.get(fqcn) {
871 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
872 return Some(Arc::clone(m));
873 }
874 let parents = iface.all_parents.clone();
875 drop(iface);
876 for parent_fqcn in &parents {
877 if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
878 if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
879 return Some(Arc::clone(m));
880 }
881 }
882 }
883 return None;
884 }
885
886 if let Some(tr) = self.traits.get(fqcn) {
888 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
889 return Some(Arc::clone(m));
890 }
891 return None;
892 }
893
894 if let Some(e) = self.enums.get(fqcn) {
896 if let Some(m) = lookup_method(&e.own_methods, method_name) {
897 return Some(Arc::clone(m));
898 }
899 if matches!(method_name, "cases" | "from" | "tryfrom") {
901 return Some(Arc::new(crate::storage::MethodStorage {
902 fqcn: Arc::from(fqcn),
903 name: Arc::from(method_name),
904 params: vec![],
905 return_type: Some(mir_types::Union::mixed()),
906 inferred_return_type: None,
907 visibility: crate::storage::Visibility::Public,
908 is_static: true,
909 is_abstract: false,
910 is_constructor: false,
911 template_params: vec![],
912 assertions: vec![],
913 throws: vec![],
914 is_final: false,
915 is_internal: false,
916 is_pure: false,
917 deprecated: None,
918 location: None,
919 }));
920 }
921 }
922
923 None
924 }
925
926 pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
928 if child == ancestor {
929 return true;
930 }
931 if let Some(cls) = self.classes.get(child) {
932 return cls.implements_or_extends(ancestor);
933 }
934 if let Some(iface) = self.interfaces.get(child) {
935 return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
936 }
937 if let Some(en) = self.enums.get(child) {
940 if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
942 return true;
943 }
944 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
946 return true;
947 }
948 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
950 {
951 return true;
952 }
953 }
954 false
955 }
956
957 pub fn type_exists(&self, fqcn: &str) -> bool {
959 self.classes.contains_key(fqcn)
960 || self.interfaces.contains_key(fqcn)
961 || self.traits.contains_key(fqcn)
962 || self.enums.contains_key(fqcn)
963 }
964
965 pub fn function_exists(&self, fqn: &str) -> bool {
966 self.functions.contains_key(fqn)
967 }
968
969 pub fn is_abstract_class(&self, fqcn: &str) -> bool {
973 self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
974 }
975
976 pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
979 if let Some(cls) = self.classes.get(fqcn) {
980 return cls.template_params.clone();
981 }
982 if let Some(iface) = self.interfaces.get(fqcn) {
983 return iface.template_params.clone();
984 }
985 if let Some(tr) = self.traits.get(fqcn) {
986 return tr.template_params.clone();
987 }
988 vec![]
989 }
990
991 pub fn get_inherited_template_bindings(
996 &self,
997 fqcn: &str,
998 ) -> std::collections::HashMap<Arc<str>, Union> {
999 let mut bindings = std::collections::HashMap::new();
1000 let mut current = fqcn.to_string();
1001
1002 loop {
1003 let (parent_fqcn, extends_type_args) = {
1004 let cls = match self.classes.get(current.as_str()) {
1005 Some(c) => c,
1006 None => break,
1007 };
1008 let parent = match &cls.parent {
1009 Some(p) => p.clone(),
1010 None => break,
1011 };
1012 let args = cls.extends_type_args.clone();
1013 (parent, args)
1014 };
1015
1016 if !extends_type_args.is_empty() {
1017 let parent_tps = self.get_class_template_params(&parent_fqcn);
1018 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
1019 bindings
1020 .entry(tp.name.clone())
1021 .or_insert_with(|| ty.clone());
1022 }
1023 }
1024
1025 current = parent_fqcn.to_string();
1026 }
1027
1028 bindings
1029 }
1030
1031 pub fn has_magic_get(&self, fqcn: &str) -> bool {
1034 self.get_method(fqcn, "__get").is_some()
1035 }
1036
1037 pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
1045 if let Some(iface) = self.interfaces.get(fqcn) {
1047 let parents = iface.all_parents.clone();
1048 drop(iface);
1049 for p in &parents {
1050 if !self.type_exists(p.as_ref()) {
1051 return true;
1052 }
1053 }
1054 return false;
1055 }
1056
1057 let (parent, interfaces, traits, all_parents) = {
1059 let Some(cls) = self.classes.get(fqcn) else {
1060 return false;
1061 };
1062 (
1063 cls.parent.clone(),
1064 cls.interfaces.clone(),
1065 cls.traits.clone(),
1066 cls.all_parents.clone(),
1067 )
1068 };
1069
1070 if let Some(ref p) = parent {
1072 if !self.type_exists(p.as_ref()) {
1073 return true;
1074 }
1075 }
1076 for iface in &interfaces {
1077 if !self.type_exists(iface.as_ref()) {
1078 return true;
1079 }
1080 }
1081 for tr in &traits {
1082 if !self.type_exists(tr.as_ref()) {
1083 return true;
1084 }
1085 }
1086
1087 for ancestor in &all_parents {
1089 if !self.type_exists(ancestor.as_ref()) {
1090 return true;
1091 }
1092 }
1093
1094 false
1095 }
1096
1097 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
1104 let name = name.trim_start_matches('\\');
1105 if name.is_empty() {
1106 return name.to_string();
1107 }
1108 if name.contains('\\') {
1113 let first_segment = name.split('\\').next().unwrap_or(name);
1115 if let Some(imports) = self.file_imports.get(file) {
1116 if let Some(resolved_prefix) = imports.get(first_segment) {
1117 let rest = &name[first_segment.len()..]; return format!("{resolved_prefix}{rest}");
1119 }
1120 }
1121 if self.type_exists(name) {
1123 return name.to_string();
1124 }
1125 if let Some(ns) = self.file_namespaces.get(file) {
1127 let qualified = format!("{}\\{}", *ns, name);
1128 if self.type_exists(&qualified) {
1129 return qualified;
1130 }
1131 }
1132 return name.to_string();
1133 }
1134 match name {
1136 "self" | "parent" | "static" | "this" => return name.to_string(),
1137 _ => {}
1138 }
1139 if let Some(imports) = self.file_imports.get(file) {
1141 if let Some(resolved) = imports.get(name) {
1142 return resolved.clone();
1143 }
1144 let name_lower = name.to_lowercase();
1146 for (alias, resolved) in imports.iter() {
1147 if alias.to_lowercase() == name_lower {
1148 return resolved.clone();
1149 }
1150 }
1151 }
1152 if let Some(ns) = self.file_namespaces.get(file) {
1154 let qualified = format!("{}\\{}", *ns, name);
1155 if self.type_exists(&qualified) {
1160 return qualified;
1161 }
1162 if self.type_exists(name) {
1163 return name.to_string();
1164 }
1165 return qualified;
1166 }
1167 name.to_string()
1168 }
1169
1170 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
1177 if let Some(cls) = self.classes.get(fqcn) {
1178 return cls.location.clone();
1179 }
1180 if let Some(iface) = self.interfaces.get(fqcn) {
1181 return iface.location.clone();
1182 }
1183 if let Some(tr) = self.traits.get(fqcn) {
1184 return tr.location.clone();
1185 }
1186 if let Some(en) = self.enums.get(fqcn) {
1187 return en.location.clone();
1188 }
1189 if let Some(func) = self.functions.get(fqcn) {
1190 return func.location.clone();
1191 }
1192 None
1193 }
1194
1195 pub fn get_member_location(
1197 &self,
1198 fqcn: &str,
1199 member_name: &str,
1200 ) -> Option<crate::storage::Location> {
1201 if let Some(method) = self.get_method(fqcn, member_name) {
1203 return method.location.clone();
1204 }
1205 if let Some(prop) = self.get_property(fqcn, member_name) {
1207 return prop.location.clone();
1208 }
1209 if let Some(cls) = self.classes.get(fqcn) {
1211 if let Some(c) = cls.own_constants.get(member_name) {
1212 return c.location.clone();
1213 }
1214 }
1215 if let Some(iface) = self.interfaces.get(fqcn) {
1217 if let Some(c) = iface.own_constants.get(member_name) {
1218 return c.location.clone();
1219 }
1220 }
1221 if let Some(tr) = self.traits.get(fqcn) {
1223 if let Some(c) = tr.own_constants.get(member_name) {
1224 return c.location.clone();
1225 }
1226 }
1227 if let Some(en) = self.enums.get(fqcn) {
1229 if let Some(c) = en.own_constants.get(member_name) {
1230 return c.location.clone();
1231 }
1232 if let Some(case) = en.cases.get(member_name) {
1233 return case.location.clone();
1234 }
1235 }
1236 None
1237 }
1238
1239 pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
1245 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1246 let id = self.symbol_interner.intern_str(&key);
1247 self.referenced_methods.insert(id);
1248 }
1249
1250 pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
1252 let key = format!("{fqcn}::{prop_name}");
1253 let id = self.symbol_interner.intern_str(&key);
1254 self.referenced_properties.insert(id);
1255 }
1256
1257 pub fn mark_function_referenced(&self, fqn: &str) {
1259 let id = self.symbol_interner.intern_str(fqn);
1260 self.referenced_functions.insert(id);
1261 }
1262
1263 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
1264 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1265 match self.symbol_interner.get_id(&key) {
1266 Some(id) => self.referenced_methods.contains(&id),
1267 None => false,
1268 }
1269 }
1270
1271 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
1272 let key = format!("{fqcn}::{prop_name}");
1273 match self.symbol_interner.get_id(&key) {
1274 Some(id) => self.referenced_properties.contains(&id),
1275 None => false,
1276 }
1277 }
1278
1279 pub fn is_function_referenced(&self, fqn: &str) -> bool {
1280 match self.symbol_interner.get_id(fqn) {
1281 Some(id) => self.referenced_functions.contains(&id),
1282 None => false,
1283 }
1284 }
1285
1286 pub fn mark_method_referenced_at(
1289 &self,
1290 fqcn: &str,
1291 method_name: &str,
1292 file: Arc<str>,
1293 line: u32,
1294 col_start: u16,
1295 col_end: u16,
1296 ) {
1297 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1298 self.ensure_expanded();
1299 let sym_id = self.symbol_interner.intern_str(&key);
1300 let file_id = self.file_interner.intern(file);
1301 self.referenced_methods.insert(sym_id);
1302 record_ref(
1303 &self.symbol_reference_locations,
1304 &self.file_symbol_references,
1305 sym_id,
1306 file_id,
1307 line,
1308 col_start,
1309 col_end,
1310 );
1311 }
1312
1313 pub fn mark_property_referenced_at(
1316 &self,
1317 fqcn: &str,
1318 prop_name: &str,
1319 file: Arc<str>,
1320 line: u32,
1321 col_start: u16,
1322 col_end: u16,
1323 ) {
1324 let key = format!("{fqcn}::{prop_name}");
1325 self.ensure_expanded();
1326 let sym_id = self.symbol_interner.intern_str(&key);
1327 let file_id = self.file_interner.intern(file);
1328 self.referenced_properties.insert(sym_id);
1329 record_ref(
1330 &self.symbol_reference_locations,
1331 &self.file_symbol_references,
1332 sym_id,
1333 file_id,
1334 line,
1335 col_start,
1336 col_end,
1337 );
1338 }
1339
1340 pub fn mark_function_referenced_at(
1343 &self,
1344 fqn: &str,
1345 file: Arc<str>,
1346 line: u32,
1347 col_start: u16,
1348 col_end: u16,
1349 ) {
1350 self.ensure_expanded();
1351 let sym_id = self.symbol_interner.intern_str(fqn);
1352 let file_id = self.file_interner.intern(file);
1353 self.referenced_functions.insert(sym_id);
1354 record_ref(
1355 &self.symbol_reference_locations,
1356 &self.file_symbol_references,
1357 sym_id,
1358 file_id,
1359 line,
1360 col_start,
1361 col_end,
1362 );
1363 }
1364
1365 pub fn mark_class_referenced_at(
1369 &self,
1370 fqcn: &str,
1371 file: Arc<str>,
1372 line: u32,
1373 col_start: u16,
1374 col_end: u16,
1375 ) {
1376 self.ensure_expanded();
1377 let sym_id = self.symbol_interner.intern_str(fqcn);
1378 let file_id = self.file_interner.intern(file);
1379 record_ref(
1380 &self.symbol_reference_locations,
1381 &self.file_symbol_references,
1382 sym_id,
1383 file_id,
1384 line,
1385 col_start,
1386 col_end,
1387 );
1388 }
1389
1390 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
1394 if locs.is_empty() {
1395 return;
1396 }
1397 self.ensure_expanded();
1398 let file_id = self.file_interner.intern(file);
1399 for (symbol_key, line, col_start, col_end) in locs {
1400 let sym_id = self.symbol_interner.intern_str(symbol_key);
1401 record_ref(
1402 &self.symbol_reference_locations,
1403 &self.file_symbol_references,
1404 sym_id,
1405 file_id,
1406 *line,
1407 *col_start,
1408 *col_end,
1409 );
1410 }
1411 }
1412
1413 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1416 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
1417 return Vec::new();
1418 };
1419 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1421 let id = sym_id as usize;
1422 if id + 1 >= ci.sym_offsets.len() {
1423 return Vec::new();
1424 }
1425 let start = ci.sym_offsets[id] as usize;
1426 let end = ci.sym_offsets[id + 1] as usize;
1427 return ci.entries[start..end]
1428 .iter()
1429 .map(|&(_, file_id, line, col_start, col_end)| {
1430 (self.file_interner.get(file_id), line, col_start, col_end)
1431 })
1432 .collect();
1433 }
1434 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1436 return Vec::new();
1437 };
1438 entries
1439 .iter()
1440 .map(|&(file_id, line, col_start, col_end)| {
1441 (self.file_interner.get(file_id), line, col_start, col_end)
1442 })
1443 .collect()
1444 }
1445
1446 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1450 let Some(file_id) = self.file_interner.get_id(file) else {
1451 return Vec::new();
1452 };
1453 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1455 let id = file_id as usize;
1456 if id + 1 >= ci.file_offsets.len() {
1457 return Vec::new();
1458 }
1459 let start = ci.file_offsets[id] as usize;
1460 let end = ci.file_offsets[id + 1] as usize;
1461 return ci.by_file[start..end]
1462 .iter()
1463 .map(|&entry_idx| {
1464 let (sym_id, _, line, col_start, col_end) = ci.entries[entry_idx as usize];
1465 (self.symbol_interner.get(sym_id), line, col_start, col_end)
1466 })
1467 .collect();
1468 }
1469 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
1471 return Vec::new();
1472 };
1473 let mut out = Vec::new();
1474 for &sym_id in sym_ids.iter() {
1475 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1476 continue;
1477 };
1478 let sym_key = self.symbol_interner.get(sym_id);
1479 for &(entry_file_id, line, col_start, col_end) in entries.iter() {
1480 if entry_file_id == file_id {
1481 out.push((sym_key.clone(), line, col_start, col_end));
1482 }
1483 }
1484 }
1485 out
1486 }
1487
1488 pub fn file_has_symbol_references(&self, file: &str) -> bool {
1490 let Some(file_id) = self.file_interner.get_id(file) else {
1491 return false;
1492 };
1493 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1495 let id = file_id as usize;
1496 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
1497 }
1498 self.file_symbol_references.contains_key(&file_id)
1499 }
1500
1501 pub fn finalize(&self) {
1508 if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
1509 return;
1510 }
1511
1512 let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
1514 for fqcn in &class_keys {
1515 let parents = self.collect_class_ancestors(fqcn);
1516 if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
1517 cls.all_parents = parents;
1518 }
1519 }
1520
1521 let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
1523 for fqcn in &iface_keys {
1524 let parents = self.collect_interface_ancestors(fqcn);
1525 if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
1526 iface.all_parents = parents;
1527 }
1528 }
1529
1530 type PendingImports = Vec<(Arc<str>, Vec<(Arc<str>, Arc<str>, Arc<str>)>)>;
1533 let pending: PendingImports = self
1534 .classes
1535 .iter()
1536 .filter(|e| !e.pending_import_types.is_empty())
1537 .map(|e| (e.key().clone(), e.pending_import_types.clone()))
1538 .collect();
1539 for (dst_fqcn, imports) in pending {
1540 let mut resolved: std::collections::HashMap<Arc<str>, mir_types::Union> =
1541 std::collections::HashMap::new();
1542 for (local, original, from_class) in &imports {
1543 if let Some(src_cls) = self.classes.get(from_class.as_ref()) {
1544 if let Some(ty) = src_cls.type_aliases.get(original.as_ref()) {
1545 resolved.insert(local.clone(), ty.clone());
1546 }
1547 }
1548 }
1549 if !resolved.is_empty() {
1550 if let Some(mut dst_cls) = self.classes.get_mut(dst_fqcn.as_ref()) {
1551 for (k, v) in resolved {
1552 dst_cls.type_aliases.insert(k, v);
1553 }
1554 }
1555 }
1556 }
1557
1558 self.finalized
1559 .store(true, std::sync::atomic::Ordering::SeqCst);
1560 }
1561
1562 fn get_method_in_trait(
1570 &self,
1571 tr_fqcn: &Arc<str>,
1572 method_name: &str,
1573 ) -> Option<Arc<MethodStorage>> {
1574 let mut visited = std::collections::HashSet::new();
1575 self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
1576 }
1577
1578 fn get_method_in_trait_inner(
1579 &self,
1580 tr_fqcn: &Arc<str>,
1581 method_name: &str,
1582 visited: &mut std::collections::HashSet<String>,
1583 ) -> Option<Arc<MethodStorage>> {
1584 if !visited.insert(tr_fqcn.to_string()) {
1585 return None; }
1587 let tr = self.traits.get(tr_fqcn.as_ref())?;
1588 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
1589 return Some(Arc::clone(m));
1590 }
1591 let used_traits = tr.traits.clone();
1592 drop(tr);
1593 for used_fqcn in &used_traits {
1594 if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
1595 return Some(m);
1596 }
1597 }
1598 None
1599 }
1600
1601 fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1602 let mut result = Vec::new();
1603 let mut visited = std::collections::HashSet::new();
1604 self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
1605 result
1606 }
1607
1608 fn collect_class_ancestors_inner(
1609 &self,
1610 fqcn: &str,
1611 out: &mut Vec<Arc<str>>,
1612 visited: &mut std::collections::HashSet<String>,
1613 ) {
1614 if !visited.insert(fqcn.to_string()) {
1615 return; }
1617 let (parent, interfaces, traits) = {
1618 if let Some(cls) = self.classes.get(fqcn) {
1619 (
1620 cls.parent.clone(),
1621 cls.interfaces.clone(),
1622 cls.traits.clone(),
1623 )
1624 } else {
1625 return;
1626 }
1627 };
1628
1629 if let Some(p) = parent {
1630 out.push(p.clone());
1631 self.collect_class_ancestors_inner(&p, out, visited);
1632 }
1633 for iface in interfaces {
1634 out.push(iface.clone());
1635 self.collect_interface_ancestors_inner(&iface, out, visited);
1636 }
1637 for t in traits {
1638 out.push(t);
1639 }
1640 }
1641
1642 fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1643 let mut result = Vec::new();
1644 let mut visited = std::collections::HashSet::new();
1645 self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
1646 result
1647 }
1648
1649 fn collect_interface_ancestors_inner(
1650 &self,
1651 fqcn: &str,
1652 out: &mut Vec<Arc<str>>,
1653 visited: &mut std::collections::HashSet<String>,
1654 ) {
1655 if !visited.insert(fqcn.to_string()) {
1656 return;
1657 }
1658 let extends = {
1659 if let Some(iface) = self.interfaces.get(fqcn) {
1660 iface.extends.clone()
1661 } else {
1662 return;
1663 }
1664 };
1665 for e in extends {
1666 out.push(e.clone());
1667 self.collect_interface_ancestors_inner(&e, out, visited);
1668 }
1669 }
1670}
1671
1672pub struct CodebaseBuilder {
1684 cb: Codebase,
1685}
1686
1687impl CodebaseBuilder {
1688 pub fn new() -> Self {
1689 Self {
1690 cb: Codebase::new(),
1691 }
1692 }
1693
1694 pub fn add(&mut self, slice: crate::storage::StubSlice) {
1697 self.cb.inject_stub_slice(slice);
1698 }
1699
1700 pub fn finalize(self) -> Codebase {
1702 self.cb.finalize();
1703 self.cb
1704 }
1705
1706 pub fn codebase(&self) -> &Codebase {
1708 &self.cb
1709 }
1710}
1711
1712impl Default for CodebaseBuilder {
1713 fn default() -> Self {
1714 Self::new()
1715 }
1716}
1717
1718pub fn codebase_from_parts(parts: Vec<crate::storage::StubSlice>) -> Codebase {
1720 let mut b = CodebaseBuilder::new();
1721 for p in parts {
1722 b.add(p);
1723 }
1724 b.finalize()
1725}
1726
1727#[cfg(test)]
1728mod tests {
1729 use super::*;
1730
1731 fn arc(s: &str) -> Arc<str> {
1732 Arc::from(s)
1733 }
1734
1735 #[test]
1736 fn method_referenced_at_groups_spans_by_file() {
1737 let cb = Codebase::new();
1738 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
1739 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 10, 15);
1740 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 2, 0, 5);
1741
1742 let locs = cb.get_reference_locations("Foo::bar");
1743 let files: std::collections::HashSet<&str> =
1744 locs.iter().map(|(f, ..)| f.as_ref()).collect();
1745 assert_eq!(files.len(), 2, "two files, not three spans");
1746 assert!(locs.contains(&(arc("a.php"), 1, 0, 5)));
1747 assert!(locs.contains(&(arc("a.php"), 1, 10, 15)));
1748 assert_eq!(
1749 locs.iter().filter(|(f, ..)| f.as_ref() == "a.php").count(),
1750 2
1751 );
1752 assert!(locs.contains(&(arc("b.php"), 2, 0, 5)));
1753 assert!(
1754 cb.is_method_referenced("Foo", "bar"),
1755 "DashSet also updated"
1756 );
1757 }
1758
1759 #[test]
1760 fn duplicate_spans_are_deduplicated() {
1761 let cb = Codebase::new();
1762 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
1764 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
1765
1766 let count = cb
1767 .get_reference_locations("Foo::bar")
1768 .iter()
1769 .filter(|(f, ..)| f.as_ref() == "a.php")
1770 .count();
1771 assert_eq!(count, 1, "duplicate span deduplicated");
1772 }
1773
1774 #[test]
1775 fn method_key_is_lowercased() {
1776 let cb = Codebase::new();
1777 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 1, 0, 3);
1778 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
1779 }
1780
1781 #[test]
1782 fn property_referenced_at_records_location() {
1783 let cb = Codebase::new();
1784 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 1, 5, 10);
1785
1786 assert!(cb
1787 .get_reference_locations("Bar::count")
1788 .contains(&(arc("x.php"), 1, 5, 10)));
1789 assert!(cb.is_property_referenced("Bar", "count"));
1790 }
1791
1792 #[test]
1793 fn function_referenced_at_records_location() {
1794 let cb = Codebase::new();
1795 cb.mark_function_referenced_at("my_fn", arc("a.php"), 1, 10, 15);
1796
1797 assert!(cb
1798 .get_reference_locations("my_fn")
1799 .contains(&(arc("a.php"), 1, 10, 15)));
1800 assert!(cb.is_function_referenced("my_fn"));
1801 }
1802
1803 #[test]
1804 fn class_referenced_at_records_location() {
1805 let cb = Codebase::new();
1806 cb.mark_class_referenced_at("Foo", arc("a.php"), 1, 5, 8);
1807
1808 assert!(cb
1809 .get_reference_locations("Foo")
1810 .contains(&(arc("a.php"), 1, 5, 8)));
1811 }
1812
1813 #[test]
1814 fn get_reference_locations_flattens_all_files() {
1815 let cb = Codebase::new();
1816 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1817 cb.mark_function_referenced_at("fn1", arc("b.php"), 2, 0, 5);
1818
1819 let mut locs = cb.get_reference_locations("fn1");
1820 locs.sort_by_key(|&(_, line, col, _)| (line, col));
1821 assert_eq!(locs.len(), 2);
1822 assert_eq!(locs[0], (arc("a.php"), 1, 0, 5));
1823 assert_eq!(locs[1], (arc("b.php"), 2, 0, 5));
1824 }
1825
1826 #[test]
1827 fn replay_reference_locations_restores_index() {
1828 let cb = Codebase::new();
1829 let locs = vec![
1830 ("Foo::bar".to_string(), 1u32, 0u16, 5u16),
1831 ("Foo::bar".to_string(), 1, 10, 15),
1832 ("greet".to_string(), 2, 0, 5),
1833 ];
1834 cb.replay_reference_locations(arc("a.php"), &locs);
1835
1836 let bar_locs = cb.get_reference_locations("Foo::bar");
1837 assert!(bar_locs.contains(&(arc("a.php"), 1, 0, 5)));
1838 assert!(bar_locs.contains(&(arc("a.php"), 1, 10, 15)));
1839
1840 assert!(cb
1841 .get_reference_locations("greet")
1842 .contains(&(arc("a.php"), 2, 0, 5)));
1843
1844 assert!(cb.file_has_symbol_references("a.php"));
1845 }
1846
1847 #[test]
1848 fn remove_file_clears_its_spans_only() {
1849 let cb = Codebase::new();
1850 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1851 cb.mark_function_referenced_at("fn1", arc("b.php"), 1, 10, 15);
1852
1853 cb.remove_file_definitions("a.php");
1854
1855 let locs = cb.get_reference_locations("fn1");
1856 assert!(
1857 !locs.iter().any(|(f, ..)| f.as_ref() == "a.php"),
1858 "a.php spans removed"
1859 );
1860 assert!(
1861 locs.contains(&(arc("b.php"), 1, 10, 15)),
1862 "b.php spans untouched"
1863 );
1864 assert!(!cb.file_has_symbol_references("a.php"));
1865 }
1866
1867 #[test]
1868 fn remove_file_does_not_affect_other_files() {
1869 let cb = Codebase::new();
1870 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 1, 4);
1871 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 1, 7, 10);
1872
1873 cb.remove_file_definitions("x.php");
1874
1875 let locs = cb.get_reference_locations("Cls::prop");
1876 assert!(!locs.iter().any(|(f, ..)| f.as_ref() == "x.php"));
1877 assert!(locs.contains(&(arc("y.php"), 1, 7, 10)));
1878 }
1879
1880 #[test]
1881 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1882 let cb = Codebase::new();
1883 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1884
1885 cb.remove_file_definitions("ghost.php");
1887
1888 assert!(cb
1890 .get_reference_locations("fn1")
1891 .contains(&(arc("a.php"), 1, 0, 5)));
1892 assert!(!cb.file_has_symbol_references("ghost.php"));
1893 }
1894
1895 #[test]
1896 fn replay_reference_locations_with_empty_list_is_noop() {
1897 let cb = Codebase::new();
1898 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1899
1900 cb.replay_reference_locations(arc("b.php"), &[]);
1902
1903 assert!(
1904 !cb.file_has_symbol_references("b.php"),
1905 "empty replay must not create a file entry"
1906 );
1907 assert!(
1908 cb.get_reference_locations("fn1")
1909 .contains(&(arc("a.php"), 1, 0, 5)),
1910 "existing spans untouched"
1911 );
1912 }
1913
1914 #[test]
1915 fn replay_reference_locations_twice_does_not_duplicate_spans() {
1916 let cb = Codebase::new();
1917 let locs = vec![("fn1".to_string(), 1u32, 0u16, 5u16)];
1918
1919 cb.replay_reference_locations(arc("a.php"), &locs);
1920 cb.replay_reference_locations(arc("a.php"), &locs);
1921
1922 let count = cb
1923 .get_reference_locations("fn1")
1924 .iter()
1925 .filter(|(f, ..)| f.as_ref() == "a.php")
1926 .count();
1927 assert_eq!(
1928 count, 1,
1929 "replaying the same location twice must not create duplicate spans"
1930 );
1931 }
1932
1933 fn make_fn(fqn: &str, short_name: &str) -> crate::storage::FunctionStorage {
1938 crate::storage::FunctionStorage {
1939 fqn: Arc::from(fqn),
1940 short_name: Arc::from(short_name),
1941 params: vec![],
1942 return_type: None,
1943 inferred_return_type: None,
1944 template_params: vec![],
1945 assertions: vec![],
1946 throws: vec![],
1947 deprecated: None,
1948 is_pure: false,
1949 location: None,
1950 }
1951 }
1952
1953 #[test]
1954 fn inject_stub_slice_later_injection_overwrites_earlier() {
1955 let cb = Codebase::new();
1956
1957 cb.inject_stub_slice(crate::storage::StubSlice {
1958 functions: vec![make_fn("strlen", "phpstorm_version")],
1959 file: Some(Arc::from("phpstorm/standard.php")),
1960 ..Default::default()
1961 });
1962 assert_eq!(
1963 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1964 "phpstorm_version"
1965 );
1966
1967 cb.inject_stub_slice(crate::storage::StubSlice {
1968 functions: vec![make_fn("strlen", "custom_version")],
1969 file: Some(Arc::from("stubs/standard/basic.php")),
1970 ..Default::default()
1971 });
1972
1973 assert_eq!(
1974 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1975 "custom_version",
1976 "custom stub must overwrite phpstorm stub"
1977 );
1978 assert_eq!(
1979 cb.symbol_to_file.get("strlen").unwrap().as_ref(),
1980 "stubs/standard/basic.php",
1981 "symbol_to_file must point to the overriding file"
1982 );
1983 }
1984
1985 #[test]
1986 fn inject_stub_slice_constants_not_added_to_symbol_to_file() {
1987 let cb = Codebase::new();
1988
1989 cb.inject_stub_slice(crate::storage::StubSlice {
1990 constants: vec![(Arc::from("PHP_EOL"), mir_types::Union::empty())],
1991 file: Some(Arc::from("stubs/core/constants.php")),
1992 ..Default::default()
1993 });
1994
1995 assert!(
1996 cb.constants.contains_key("PHP_EOL"),
1997 "constant must be registered in constants map"
1998 );
1999 assert!(
2000 !cb.symbol_to_file.contains_key("PHP_EOL"),
2001 "constants must not appear in symbol_to_file — go-to-definition is not supported for them"
2002 );
2003 }
2004
2005 #[test]
2006 fn remove_file_definitions_purges_injected_global_vars() {
2007 let cb = Codebase::new();
2008
2009 cb.inject_stub_slice(crate::storage::StubSlice {
2010 global_vars: vec![(Arc::from("db_connection"), mir_types::Union::empty())],
2011 file: Some(Arc::from("src/bootstrap.php")),
2012 ..Default::default()
2013 });
2014 assert!(
2015 cb.global_vars.contains_key("db_connection"),
2016 "global var must be registered after injection"
2017 );
2018
2019 cb.remove_file_definitions("src/bootstrap.php");
2020
2021 assert!(
2022 !cb.global_vars.contains_key("db_connection"),
2023 "global var must be removed when its defining file is removed"
2024 );
2025 }
2026
2027 #[test]
2028 fn inject_stub_slice_without_file_discards_global_vars() {
2029 let cb = Codebase::new();
2030
2031 cb.inject_stub_slice(crate::storage::StubSlice {
2032 global_vars: vec![(Arc::from("orphan_var"), mir_types::Union::empty())],
2033 file: None,
2034 ..Default::default()
2035 });
2036
2037 assert!(
2038 !cb.global_vars.contains_key("orphan_var"),
2039 "global_vars must not be registered when slice.file is None"
2040 );
2041 }
2042
2043 #[test]
2054 fn inject_stub_slice_populates_file_namespace() {
2055 let cb = Codebase::new();
2059 cb.inject_stub_slice(crate::storage::StubSlice {
2060 file: Some(Arc::from("src/Service.php")),
2061 namespace: Some(Arc::from("App\\Service")),
2062 ..Default::default()
2063 });
2064 assert_eq!(
2065 cb.file_namespaces
2066 .get("src/Service.php")
2067 .as_deref()
2068 .map(|s| s.as_str()),
2069 Some("App\\Service"),
2070 "file_namespaces must be populated when slice carries a namespace"
2071 );
2072
2073 let cb2 = Codebase::new();
2075 cb2.inject_stub_slice(crate::storage::StubSlice {
2076 file: Some(Arc::from("src/global.php")),
2077 namespace: None,
2078 ..Default::default()
2079 });
2080 assert!(
2081 cb2.file_namespaces.is_empty(),
2082 "file_namespaces must not be written when slice.namespace is None"
2083 );
2084 }
2085
2086 #[test]
2087 fn inject_stub_slice_populates_file_imports() {
2088 let cb = Codebase::new();
2092 let mut imports = std::collections::HashMap::new();
2093 imports.insert("Entity".to_string(), "App\\Model\\Entity".to_string());
2094 imports.insert(
2095 "Repo".to_string(),
2096 "App\\Repository\\EntityRepo".to_string(),
2097 );
2098 cb.inject_stub_slice(crate::storage::StubSlice {
2099 file: Some(Arc::from("src/Handler.php")),
2100 imports,
2101 ..Default::default()
2102 });
2103 let stored = cb.file_imports.get("src/Handler.php").unwrap();
2104 assert_eq!(
2105 stored.get("Entity").map(|s| s.as_str()),
2106 Some("App\\Model\\Entity")
2107 );
2108 assert_eq!(
2109 stored.get("Repo").map(|s| s.as_str()),
2110 Some("App\\Repository\\EntityRepo")
2111 );
2112
2113 let cb2 = Codebase::new();
2115 cb2.inject_stub_slice(crate::storage::StubSlice {
2116 file: Some(Arc::from("src/no_imports.php")),
2117 imports: std::collections::HashMap::new(),
2118 ..Default::default()
2119 });
2120 assert!(
2121 cb2.file_imports.is_empty(),
2122 "file_imports must not be written when slice.imports is empty"
2123 );
2124 }
2125
2126 #[test]
2127 fn inject_stub_slice_skips_namespace_and_imports_when_no_file() {
2128 let cb = Codebase::new();
2132 let mut imports = std::collections::HashMap::new();
2133 imports.insert("Foo".to_string(), "Bar\\Foo".to_string());
2134 cb.inject_stub_slice(crate::storage::StubSlice {
2135 file: None,
2136 namespace: Some(Arc::from("Bar")),
2137 imports,
2138 ..Default::default()
2139 });
2140 assert!(
2141 cb.file_namespaces.is_empty(),
2142 "file_namespaces must not be written when slice.file is None"
2143 );
2144 assert!(
2145 cb.file_imports.is_empty(),
2146 "file_imports must not be written when slice.file is None"
2147 );
2148 }
2149
2150 #[test]
2151 fn remove_file_definitions_purges_file_namespaces_and_imports() {
2152 let cb = Codebase::new();
2158 let mut imports = std::collections::HashMap::new();
2159 imports.insert("Entity".to_string(), "App\\Model\\Entity".to_string());
2160 cb.inject_stub_slice(crate::storage::StubSlice {
2161 file: Some(Arc::from("src/Handler.php")),
2162 namespace: Some(Arc::from("App\\Service")),
2163 imports,
2164 ..Default::default()
2165 });
2166 assert!(
2167 cb.file_namespaces.contains_key("src/Handler.php"),
2168 "setup: namespace must be present"
2169 );
2170 assert!(
2171 cb.file_imports.contains_key("src/Handler.php"),
2172 "setup: imports must be present"
2173 );
2174
2175 cb.remove_file_definitions("src/Handler.php");
2176
2177 assert!(
2178 !cb.file_namespaces.contains_key("src/Handler.php"),
2179 "file_namespaces entry must be removed when its defining file is removed"
2180 );
2181 assert!(
2182 !cb.file_imports.contains_key("src/Handler.php"),
2183 "file_imports entry must be removed when its defining file is removed"
2184 );
2185 }
2186
2187 fn bare_class(fqcn: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2192 use indexmap::IndexMap;
2193 ClassStorage {
2194 fqcn: arc(fqcn),
2195 short_name: arc(fqcn),
2196 parent: None,
2197 interfaces: vec![],
2198 traits: vec![],
2199 own_methods: IndexMap::new(),
2200 own_properties: IndexMap::new(),
2201 own_constants: IndexMap::new(),
2202 mixins,
2203 template_params: vec![],
2204 extends_type_args: vec![],
2205 implements_type_args: vec![],
2206 is_abstract: false,
2207 is_final: false,
2208 is_readonly: false,
2209 all_parents: vec![],
2210 deprecated: None,
2211 is_internal: false,
2212 location: None,
2213 type_aliases: std::collections::HashMap::new(),
2214 pending_import_types: vec![],
2215 }
2216 }
2217
2218 fn class_with_method(fqcn: &str, method_name: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2219 use crate::storage::{MethodStorage, Visibility};
2220 use indexmap::IndexMap;
2221 let mut methods = IndexMap::new();
2222 methods.insert(
2223 arc(method_name),
2224 Arc::new(MethodStorage {
2225 name: arc(method_name),
2226 fqcn: arc(fqcn),
2227 params: vec![],
2228 return_type: None,
2229 inferred_return_type: None,
2230 visibility: Visibility::Public,
2231 is_static: false,
2232 is_abstract: false,
2233 is_final: false,
2234 is_constructor: false,
2235 template_params: vec![],
2236 assertions: vec![],
2237 throws: vec![],
2238 deprecated: None,
2239 is_internal: false,
2240 is_pure: false,
2241 location: None,
2242 }),
2243 );
2244 let mut cls = bare_class(fqcn, mixins);
2245 cls.own_methods = methods;
2246 cls
2247 }
2248
2249 fn class_with_property(fqcn: &str, prop_name: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2250 use crate::storage::{PropertyStorage, Visibility};
2251 use indexmap::IndexMap;
2252 let mut props = IndexMap::new();
2253 props.insert(
2254 arc(prop_name),
2255 PropertyStorage {
2256 name: arc(prop_name),
2257 ty: None,
2258 inferred_ty: None,
2259 visibility: Visibility::Public,
2260 is_static: false,
2261 is_readonly: false,
2262 default: None,
2263 location: None,
2264 },
2265 );
2266 let mut cls = bare_class(fqcn, mixins);
2267 cls.own_properties = props;
2268 cls
2269 }
2270
2271 #[test]
2272 fn get_method_two_way_mixin_cycle_returns_none() {
2273 let cb = Codebase::new();
2274 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2275 cb.classes.insert(arc("B"), bare_class("B", vec![arc("A")]));
2276 assert!(cb.get_method("A", "missing").is_none());
2277 }
2278
2279 #[test]
2280 fn get_method_self_mixin_returns_none() {
2281 let cb = Codebase::new();
2282 cb.classes.insert(arc("A"), bare_class("A", vec![arc("A")]));
2283 assert!(cb.get_method("A", "missing").is_none());
2284 }
2285
2286 #[test]
2287 fn get_method_three_way_cycle_returns_none() {
2288 let cb = Codebase::new();
2289 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2290 cb.classes.insert(arc("B"), bare_class("B", vec![arc("C")]));
2291 cb.classes.insert(arc("C"), bare_class("C", vec![arc("A")]));
2292 assert!(cb.get_method("A", "missing").is_none());
2293 }
2294
2295 #[test]
2296 fn get_method_resolves_through_mixin_when_no_cycle() {
2297 let cb = Codebase::new();
2298 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2299 cb.classes
2300 .insert(arc("B"), class_with_method("B", "fromB", vec![]));
2301 assert!(cb.get_method("A", "fromB").is_some());
2302 }
2303
2304 #[test]
2305 fn get_method_own_method_shadows_mixin() {
2306 let cb = Codebase::new();
2307 cb.classes
2308 .insert(arc("A"), class_with_method("A", "foo", vec![arc("B")]));
2309 cb.classes
2310 .insert(arc("B"), class_with_method("B", "foo", vec![]));
2311 let m = cb.get_method("A", "foo").unwrap();
2312 assert_eq!(m.fqcn.as_ref(), "A");
2313 }
2314
2315 #[test]
2316 fn get_method_mixin_nonexistent_class_returns_none() {
2317 let cb = Codebase::new();
2318 cb.classes
2319 .insert(arc("A"), bare_class("A", vec![arc("Ghost")]));
2320 assert!(cb.get_method("A", "foo").is_none());
2321 }
2322
2323 #[test]
2324 fn get_property_two_way_mixin_cycle_returns_none() {
2325 let cb = Codebase::new();
2326 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2327 cb.classes.insert(arc("B"), bare_class("B", vec![arc("A")]));
2328 assert!(cb.get_property("A", "missing").is_none());
2329 }
2330
2331 #[test]
2332 fn get_method_diamond_mixin_finds_method_via_first_path() {
2333 let cb = Codebase::new();
2337 cb.classes
2338 .insert(arc("A"), bare_class("A", vec![arc("B"), arc("C")]));
2339 cb.classes.insert(arc("B"), bare_class("B", vec![arc("D")]));
2340 cb.classes.insert(arc("C"), bare_class("C", vec![arc("D")]));
2341 cb.classes
2342 .insert(arc("D"), class_with_method("D", "foo", vec![]));
2343 assert!(cb.get_method("A", "foo").is_some());
2344 }
2345
2346 #[test]
2347 fn get_method_mixin_on_ancestor_is_followed() {
2348 let cb = Codebase::new();
2349 cb.classes.insert(
2350 arc("Child"),
2351 ClassStorage {
2352 all_parents: vec![arc("Parent")],
2353 ..bare_class("Child", vec![])
2354 },
2355 );
2356 cb.classes
2357 .insert(arc("Parent"), bare_class("Parent", vec![arc("Mixin")]));
2358 cb.classes.insert(
2359 arc("Mixin"),
2360 class_with_method("Mixin", "fromMixin", vec![]),
2361 );
2362 assert!(cb.get_method("Child", "fromMixin").is_some());
2363 assert!(cb.get_method("Parent", "fromMixin").is_some());
2364 }
2365
2366 #[test]
2367 fn get_method_mixin_on_transitive_ancestor_is_followed() {
2368 let cb = Codebase::new();
2369 cb.classes.insert(
2371 arc("A"),
2372 ClassStorage {
2373 all_parents: vec![arc("B"), arc("C")],
2374 ..bare_class("A", vec![])
2375 },
2376 );
2377 cb.classes.insert(
2378 arc("B"),
2379 ClassStorage {
2380 all_parents: vec![arc("C")],
2381 ..bare_class("B", vec![])
2382 },
2383 );
2384 cb.classes.insert(arc("C"), bare_class("C", vec![arc("D")]));
2385 cb.classes
2386 .insert(arc("D"), class_with_method("D", "foo", vec![]));
2387 assert!(cb.get_method("A", "foo").is_some());
2388 }
2389
2390 #[test]
2391 fn get_property_resolves_through_mixin_when_no_cycle() {
2392 let cb = Codebase::new();
2393 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2394 cb.classes
2395 .insert(arc("B"), class_with_property("B", "title", vec![]));
2396 assert!(cb.get_property("A", "title").is_some());
2397 }
2398}