1use std::sync::Arc;
2
3use dashmap::{DashMap, DashSet};
4
5use crate::interner::Interner;
6
7type ReferenceLocations = DashMap<u32, Vec<(u32, u32, u32)>>;
18
19use crate::storage::{
20 ClassStorage, EnumStorage, FunctionStorage, InterfaceStorage, MethodStorage, TraitStorage,
21};
22use mir_types::Union;
23
24#[inline]
33fn lookup_method<'a>(
34 map: &'a indexmap::IndexMap<Arc<str>, Arc<MethodStorage>>,
35 name: &str,
36) -> Option<&'a Arc<MethodStorage>> {
37 map.get(name).or_else(|| {
38 map.iter()
39 .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
40 .map(|(_, v)| v)
41 })
42}
43
44#[inline]
50fn record_ref(
51 sym_locs: &ReferenceLocations,
52 file_refs: &DashMap<u32, Vec<u32>>,
53 sym_id: u32,
54 file_id: u32,
55 start: u32,
56 end: u32,
57) {
58 {
59 let mut entries = sym_locs.entry(sym_id).or_default();
60 let span = (file_id, start, 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, u32)>,
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,
167 file_symbol_references: DashMap<u32, Vec<u32>>,
172
173 compact_ref_index: std::sync::RwLock<Option<CompactRefIndex>>,
177 is_compacted: std::sync::atomic::AtomicBool,
180
181 pub symbol_to_file: DashMap<Arc<str>, Arc<str>>,
184
185 pub known_symbols: DashSet<Arc<str>>,
189
190 pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
198 pub file_namespaces: DashMap<Arc<str>, String>,
206
207 finalized: std::sync::atomic::AtomicBool,
209}
210
211impl Codebase {
212 pub fn new() -> Self {
213 Self::default()
214 }
215
216 pub fn inject_stub_slice(&self, slice: crate::storage::StubSlice) {
226 let file = slice.file.clone();
227 for cls in slice.classes {
228 if let Some(f) = &file {
229 self.symbol_to_file.insert(cls.fqcn.clone(), f.clone());
230 }
231 self.classes.insert(cls.fqcn.clone(), cls);
232 }
233 for iface in slice.interfaces {
234 if let Some(f) = &file {
235 self.symbol_to_file.insert(iface.fqcn.clone(), f.clone());
236 }
237 self.interfaces.insert(iface.fqcn.clone(), iface);
238 }
239 for tr in slice.traits {
240 if let Some(f) = &file {
241 self.symbol_to_file.insert(tr.fqcn.clone(), f.clone());
242 }
243 self.traits.insert(tr.fqcn.clone(), tr);
244 }
245 for en in slice.enums {
246 if let Some(f) = &file {
247 self.symbol_to_file.insert(en.fqcn.clone(), f.clone());
248 }
249 self.enums.insert(en.fqcn.clone(), en);
250 }
251 for func in slice.functions {
252 if let Some(f) = &file {
253 self.symbol_to_file.insert(func.fqn.clone(), f.clone());
254 }
255 self.functions.insert(func.fqn.clone(), func);
256 }
257 for (name, ty) in slice.constants {
258 self.constants.insert(name, ty);
259 }
260 if let Some(f) = &file {
261 for (name, ty) in slice.global_vars {
262 self.register_global_var(f, name, ty);
263 }
264 }
265 }
266
267 pub fn compact_reference_index(&self) {
284 let mut entries: Vec<(u32, u32, u32, u32)> = self
286 .symbol_reference_locations
287 .iter()
288 .flat_map(|entry| {
289 let sym_id = *entry.key();
290 entry
291 .value()
292 .iter()
293 .map(move |&(file_id, start, end)| (sym_id, file_id, start, end))
294 .collect::<Vec<_>>()
295 })
296 .collect();
297
298 if entries.is_empty() {
299 return;
300 }
301
302 entries.sort_unstable();
304 entries.dedup();
305
306 let n = entries.len();
307
308 let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
310 let mut sym_offsets = vec![0u32; max_sym + 2];
311 for &(sym_id, ..) in &entries {
312 sym_offsets[sym_id as usize + 1] += 1;
313 }
314 for i in 1..sym_offsets.len() {
315 sym_offsets[i] += sym_offsets[i - 1];
316 }
317
318 let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
322 let mut by_file: Vec<u32> = (0..n as u32).collect();
323 by_file.sort_unstable_by_key(|&i| {
324 let (sym_id, file_id, start, end) = entries[i as usize];
325 (file_id, sym_id, start, end)
326 });
327
328 let mut file_offsets = vec![0u32; max_file + 2];
329 for &idx in &by_file {
330 let file_id = entries[idx as usize].1;
331 file_offsets[file_id as usize + 1] += 1;
332 }
333 for i in 1..file_offsets.len() {
334 file_offsets[i] += file_offsets[i - 1];
335 }
336
337 *self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
338 entries,
339 sym_offsets,
340 by_file,
341 file_offsets,
342 });
343 self.is_compacted
344 .store(true, std::sync::atomic::Ordering::Release);
345
346 self.symbol_reference_locations.clear();
348 self.file_symbol_references.clear();
349 }
350
351 fn ensure_expanded(&self) {
357 if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
359 return;
360 }
361 let mut guard = self.compact_ref_index.write().unwrap();
363 if let Some(ci) = guard.take() {
364 for &(sym_id, file_id, start, end) in &ci.entries {
365 record_ref(
366 &self.symbol_reference_locations,
367 &self.file_symbol_references,
368 sym_id,
369 file_id,
370 start,
371 end,
372 );
373 }
374 self.is_compacted
375 .store(false, std::sync::atomic::Ordering::Release);
376 }
377 }
379
380 pub fn invalidate_finalization(&self) {
386 self.finalized
387 .store(false, std::sync::atomic::Ordering::SeqCst);
388 }
389
390 pub fn remove_file_definitions(&self, file_path: &str) {
401 let symbols: Vec<Arc<str>> = self
403 .symbol_to_file
404 .iter()
405 .filter(|entry| entry.value().as_ref() == file_path)
406 .map(|entry| entry.key().clone())
407 .collect();
408
409 for sym in &symbols {
411 self.classes.remove(sym.as_ref());
412 self.interfaces.remove(sym.as_ref());
413 self.traits.remove(sym.as_ref());
414 self.enums.remove(sym.as_ref());
415 self.functions.remove(sym.as_ref());
416 self.constants.remove(sym.as_ref());
417 self.symbol_to_file.remove(sym.as_ref());
418 self.known_symbols.remove(sym.as_ref());
419 }
420
421 self.file_imports.remove(file_path);
423 self.file_namespaces.remove(file_path);
424
425 if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
427 for name in var_names {
428 self.global_vars.remove(name.as_ref());
429 }
430 }
431
432 self.ensure_expanded();
434
435 if let Some(file_id) = self.file_interner.get_id(file_path) {
438 if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
439 for sym_id in sym_ids {
440 if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
441 entries.retain(|&(fid, _, _)| fid != file_id);
442 }
443 }
444 }
445 }
446
447 self.invalidate_finalization();
448 }
449
450 pub fn file_structural_snapshot(&self, file_path: &str) -> StructuralSnapshot {
462 let symbols: Vec<Arc<str>> = self
463 .symbol_to_file
464 .iter()
465 .filter(|e| e.value().as_ref() == file_path)
466 .map(|e| e.key().clone())
467 .collect();
468
469 let mut classes = std::collections::HashMap::new();
470 let mut interfaces = std::collections::HashMap::new();
471
472 for sym in symbols {
473 if let Some(cls) = self.classes.get(sym.as_ref()) {
474 let mut ifaces = cls.interfaces.clone();
475 ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
476 let mut traits = cls.traits.clone();
477 traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
478 classes.insert(
479 sym,
480 ClassInheritance {
481 parent: cls.parent.clone(),
482 interfaces: ifaces,
483 traits,
484 all_parents: cls.all_parents.clone(),
485 },
486 );
487 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
488 let mut extends = iface.extends.clone();
489 extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
490 interfaces.insert(
491 sym,
492 InterfaceInheritance {
493 extends,
494 all_parents: iface.all_parents.clone(),
495 },
496 );
497 }
498 }
499
500 StructuralSnapshot {
501 classes,
502 interfaces,
503 }
504 }
505
506 pub fn structural_unchanged_after_pass1(
512 &self,
513 file_path: &str,
514 old: &StructuralSnapshot,
515 ) -> bool {
516 let symbols: Vec<Arc<str>> = self
517 .symbol_to_file
518 .iter()
519 .filter(|e| e.value().as_ref() == file_path)
520 .map(|e| e.key().clone())
521 .collect();
522
523 let mut seen_classes = 0usize;
524 let mut seen_interfaces = 0usize;
525
526 for sym in &symbols {
527 if let Some(cls) = self.classes.get(sym.as_ref()) {
528 seen_classes += 1;
529 let Some(old_cls) = old.classes.get(sym.as_ref()) else {
530 return false; };
532 if old_cls.parent != cls.parent {
533 return false;
534 }
535 let mut new_ifaces = cls.interfaces.clone();
536 new_ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
537 if old_cls.interfaces != new_ifaces {
538 return false;
539 }
540 let mut new_traits = cls.traits.clone();
541 new_traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
542 if old_cls.traits != new_traits {
543 return false;
544 }
545 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
546 seen_interfaces += 1;
547 let Some(old_iface) = old.interfaces.get(sym.as_ref()) else {
548 return false; };
550 let mut new_extends = iface.extends.clone();
551 new_extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
552 if old_iface.extends != new_extends {
553 return false;
554 }
555 }
556 }
558
559 seen_classes == old.classes.len() && seen_interfaces == old.interfaces.len()
561 }
562
563 pub fn restore_all_parents(&self, file_path: &str, snapshot: &StructuralSnapshot) {
570 let symbols: Vec<Arc<str>> = self
571 .symbol_to_file
572 .iter()
573 .filter(|e| e.value().as_ref() == file_path)
574 .map(|e| e.key().clone())
575 .collect();
576
577 for sym in &symbols {
578 if let Some(old_cls) = snapshot.classes.get(sym.as_ref()) {
579 if let Some(mut cls) = self.classes.get_mut(sym.as_ref()) {
580 cls.all_parents = old_cls.all_parents.clone();
581 }
582 } else if let Some(old_iface) = snapshot.interfaces.get(sym.as_ref()) {
583 if let Some(mut iface) = self.interfaces.get_mut(sym.as_ref()) {
584 iface.all_parents = old_iface.all_parents.clone();
585 }
586 }
587 }
588
589 self.finalized
590 .store(true, std::sync::atomic::Ordering::SeqCst);
591 }
592
593 pub fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
600 self.file_global_vars
601 .entry(file.clone())
602 .or_default()
603 .push(name.clone());
604 self.global_vars.insert(name, ty);
605 }
606
607 pub fn get_property(
613 &self,
614 fqcn: &str,
615 prop_name: &str,
616 ) -> Option<crate::storage::PropertyStorage> {
617 if let Some(cls) = self.classes.get(fqcn) {
619 if let Some(p) = cls.own_properties.get(prop_name) {
620 return Some(p.clone());
621 }
622 let mixins = cls.mixins.clone();
623 drop(cls);
624 for mixin in &mixins {
625 if let Some(p) = self.get_property(mixin.as_ref(), prop_name) {
626 return Some(p);
627 }
628 }
629 }
630
631 let all_parents = {
633 if let Some(cls) = self.classes.get(fqcn) {
634 cls.all_parents.clone()
635 } else {
636 return None;
637 }
638 };
639
640 for ancestor_fqcn in &all_parents {
641 if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
642 if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
643 return Some(p.clone());
644 }
645 }
646 }
647
648 let trait_list = {
650 if let Some(cls) = self.classes.get(fqcn) {
651 cls.traits.clone()
652 } else {
653 vec![]
654 }
655 };
656 for trait_fqcn in &trait_list {
657 if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
658 if let Some(p) = tr.own_properties.get(prop_name) {
659 return Some(p.clone());
660 }
661 }
662 }
663
664 None
665 }
666
667 pub fn get_class_constant(
669 &self,
670 fqcn: &str,
671 const_name: &str,
672 ) -> Option<crate::storage::ConstantStorage> {
673 if let Some(cls) = self.classes.get(fqcn) {
675 if let Some(c) = cls.own_constants.get(const_name) {
676 return Some(c.clone());
677 }
678 let all_parents = cls.all_parents.clone();
679 let interfaces = cls.interfaces.clone();
680 let traits = cls.traits.clone();
681 drop(cls);
682
683 for tr_fqcn in &traits {
684 if let Some(tr) = self.traits.get(tr_fqcn.as_ref()) {
685 if let Some(c) = tr.own_constants.get(const_name) {
686 return Some(c.clone());
687 }
688 }
689 }
690
691 for ancestor_fqcn in &all_parents {
692 if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
693 if let Some(c) = ancestor.own_constants.get(const_name) {
694 return Some(c.clone());
695 }
696 }
697 if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
698 if let Some(c) = iface.own_constants.get(const_name) {
699 return Some(c.clone());
700 }
701 }
702 }
703
704 for iface_fqcn in &interfaces {
705 if let Some(iface) = self.interfaces.get(iface_fqcn.as_ref()) {
706 if let Some(c) = iface.own_constants.get(const_name) {
707 return Some(c.clone());
708 }
709 }
710 }
711
712 return None;
713 }
714
715 if let Some(iface) = self.interfaces.get(fqcn) {
717 if let Some(c) = iface.own_constants.get(const_name) {
718 return Some(c.clone());
719 }
720 let parents = iface.all_parents.clone();
721 drop(iface);
722 for p in &parents {
723 if let Some(parent_iface) = self.interfaces.get(p.as_ref()) {
724 if let Some(c) = parent_iface.own_constants.get(const_name) {
725 return Some(c.clone());
726 }
727 }
728 }
729 return None;
730 }
731
732 if let Some(en) = self.enums.get(fqcn) {
734 if let Some(c) = en.own_constants.get(const_name) {
735 return Some(c.clone());
736 }
737 if en.cases.contains_key(const_name) {
738 return Some(crate::storage::ConstantStorage {
739 name: Arc::from(const_name),
740 ty: mir_types::Union::mixed(),
741 visibility: None,
742 is_final: false,
743 location: None,
744 });
745 }
746 return None;
747 }
748
749 if let Some(tr) = self.traits.get(fqcn) {
751 if let Some(c) = tr.own_constants.get(const_name) {
752 return Some(c.clone());
753 }
754 return None;
755 }
756
757 None
758 }
759
760 pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
762 let method_lower = method_name.to_lowercase();
764 let method_name = method_lower.as_str();
765
766 if let Some(cls) = self.classes.get(fqcn) {
768 if let Some(m) = lookup_method(&cls.own_methods, method_name) {
770 return Some(Arc::clone(m));
771 }
772 let own_traits = cls.traits.clone();
774 let ancestors = cls.all_parents.clone();
775 let mixins = cls.mixins.clone();
776 drop(cls);
777
778 for mixin_fqcn in &mixins {
780 if let Some(m) = self.get_method(mixin_fqcn, method_name) {
781 return Some(m);
782 }
783 }
784
785 for tr_fqcn in &own_traits {
787 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
788 return Some(m);
789 }
790 }
791
792 for ancestor_fqcn in &ancestors {
794 if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
795 if let Some(m) = lookup_method(&anc.own_methods, method_name) {
796 return Some(Arc::clone(m));
797 }
798 let anc_traits = anc.traits.clone();
799 drop(anc);
800 for tr_fqcn in &anc_traits {
801 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
802 return Some(m);
803 }
804 }
805 } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
806 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
807 let mut ms = (**m).clone();
808 ms.is_abstract = true;
809 return Some(Arc::new(ms));
810 }
811 }
812 }
814 return None;
815 }
816
817 if let Some(iface) = self.interfaces.get(fqcn) {
819 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
820 return Some(Arc::clone(m));
821 }
822 let parents = iface.all_parents.clone();
823 drop(iface);
824 for parent_fqcn in &parents {
825 if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
826 if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
827 return Some(Arc::clone(m));
828 }
829 }
830 }
831 return None;
832 }
833
834 if let Some(tr) = self.traits.get(fqcn) {
836 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
837 return Some(Arc::clone(m));
838 }
839 return None;
840 }
841
842 if let Some(e) = self.enums.get(fqcn) {
844 if let Some(m) = lookup_method(&e.own_methods, method_name) {
845 return Some(Arc::clone(m));
846 }
847 if matches!(method_name, "cases" | "from" | "tryfrom") {
849 return Some(Arc::new(crate::storage::MethodStorage {
850 fqcn: Arc::from(fqcn),
851 name: Arc::from(method_name),
852 params: vec![],
853 return_type: Some(mir_types::Union::mixed()),
854 inferred_return_type: None,
855 visibility: crate::storage::Visibility::Public,
856 is_static: true,
857 is_abstract: false,
858 is_constructor: false,
859 template_params: vec![],
860 assertions: vec![],
861 throws: vec![],
862 is_final: false,
863 is_internal: false,
864 is_pure: false,
865 deprecated: None,
866 location: None,
867 }));
868 }
869 }
870
871 None
872 }
873
874 pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
876 if child == ancestor {
877 return true;
878 }
879 if let Some(cls) = self.classes.get(child) {
880 return cls.implements_or_extends(ancestor);
881 }
882 if let Some(iface) = self.interfaces.get(child) {
883 return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
884 }
885 if let Some(en) = self.enums.get(child) {
888 if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
890 return true;
891 }
892 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
894 return true;
895 }
896 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
898 {
899 return true;
900 }
901 }
902 false
903 }
904
905 pub fn type_exists(&self, fqcn: &str) -> bool {
907 self.classes.contains_key(fqcn)
908 || self.interfaces.contains_key(fqcn)
909 || self.traits.contains_key(fqcn)
910 || self.enums.contains_key(fqcn)
911 }
912
913 pub fn function_exists(&self, fqn: &str) -> bool {
914 self.functions.contains_key(fqn)
915 }
916
917 pub fn is_abstract_class(&self, fqcn: &str) -> bool {
921 self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
922 }
923
924 pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
927 if let Some(cls) = self.classes.get(fqcn) {
928 return cls.template_params.clone();
929 }
930 if let Some(iface) = self.interfaces.get(fqcn) {
931 return iface.template_params.clone();
932 }
933 if let Some(tr) = self.traits.get(fqcn) {
934 return tr.template_params.clone();
935 }
936 vec![]
937 }
938
939 pub fn get_inherited_template_bindings(
944 &self,
945 fqcn: &str,
946 ) -> std::collections::HashMap<Arc<str>, Union> {
947 let mut bindings = std::collections::HashMap::new();
948 let mut current = fqcn.to_string();
949
950 loop {
951 let (parent_fqcn, extends_type_args) = {
952 let cls = match self.classes.get(current.as_str()) {
953 Some(c) => c,
954 None => break,
955 };
956 let parent = match &cls.parent {
957 Some(p) => p.clone(),
958 None => break,
959 };
960 let args = cls.extends_type_args.clone();
961 (parent, args)
962 };
963
964 if !extends_type_args.is_empty() {
965 let parent_tps = self.get_class_template_params(&parent_fqcn);
966 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
967 bindings
968 .entry(tp.name.clone())
969 .or_insert_with(|| ty.clone());
970 }
971 }
972
973 current = parent_fqcn.to_string();
974 }
975
976 bindings
977 }
978
979 pub fn has_magic_get(&self, fqcn: &str) -> bool {
982 self.get_method(fqcn, "__get").is_some()
983 }
984
985 pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
993 if let Some(iface) = self.interfaces.get(fqcn) {
995 let parents = iface.all_parents.clone();
996 drop(iface);
997 for p in &parents {
998 if !self.type_exists(p.as_ref()) {
999 return true;
1000 }
1001 }
1002 return false;
1003 }
1004
1005 let (parent, interfaces, traits, all_parents) = {
1007 let Some(cls) = self.classes.get(fqcn) else {
1008 return false;
1009 };
1010 (
1011 cls.parent.clone(),
1012 cls.interfaces.clone(),
1013 cls.traits.clone(),
1014 cls.all_parents.clone(),
1015 )
1016 };
1017
1018 if let Some(ref p) = parent {
1020 if !self.type_exists(p.as_ref()) {
1021 return true;
1022 }
1023 }
1024 for iface in &interfaces {
1025 if !self.type_exists(iface.as_ref()) {
1026 return true;
1027 }
1028 }
1029 for tr in &traits {
1030 if !self.type_exists(tr.as_ref()) {
1031 return true;
1032 }
1033 }
1034
1035 for ancestor in &all_parents {
1037 if !self.type_exists(ancestor.as_ref()) {
1038 return true;
1039 }
1040 }
1041
1042 false
1043 }
1044
1045 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
1052 let name = name.trim_start_matches('\\');
1053 if name.is_empty() {
1054 return name.to_string();
1055 }
1056 if name.contains('\\') {
1061 let first_segment = name.split('\\').next().unwrap_or(name);
1063 if let Some(imports) = self.file_imports.get(file) {
1064 if let Some(resolved_prefix) = imports.get(first_segment) {
1065 let rest = &name[first_segment.len()..]; return format!("{resolved_prefix}{rest}");
1067 }
1068 }
1069 if self.type_exists(name) {
1071 return name.to_string();
1072 }
1073 if let Some(ns) = self.file_namespaces.get(file) {
1075 let qualified = format!("{}\\{}", *ns, name);
1076 if self.type_exists(&qualified) {
1077 return qualified;
1078 }
1079 }
1080 return name.to_string();
1081 }
1082 match name {
1084 "self" | "parent" | "static" | "this" => return name.to_string(),
1085 _ => {}
1086 }
1087 if let Some(imports) = self.file_imports.get(file) {
1089 if let Some(resolved) = imports.get(name) {
1090 return resolved.clone();
1091 }
1092 let name_lower = name.to_lowercase();
1094 for (alias, resolved) in imports.iter() {
1095 if alias.to_lowercase() == name_lower {
1096 return resolved.clone();
1097 }
1098 }
1099 }
1100 if let Some(ns) = self.file_namespaces.get(file) {
1102 let qualified = format!("{}\\{}", *ns, name);
1103 if self.type_exists(&qualified) {
1108 return qualified;
1109 }
1110 if self.type_exists(name) {
1111 return name.to_string();
1112 }
1113 return qualified;
1114 }
1115 name.to_string()
1116 }
1117
1118 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
1125 if let Some(cls) = self.classes.get(fqcn) {
1126 return cls.location.clone();
1127 }
1128 if let Some(iface) = self.interfaces.get(fqcn) {
1129 return iface.location.clone();
1130 }
1131 if let Some(tr) = self.traits.get(fqcn) {
1132 return tr.location.clone();
1133 }
1134 if let Some(en) = self.enums.get(fqcn) {
1135 return en.location.clone();
1136 }
1137 if let Some(func) = self.functions.get(fqcn) {
1138 return func.location.clone();
1139 }
1140 None
1141 }
1142
1143 pub fn get_member_location(
1145 &self,
1146 fqcn: &str,
1147 member_name: &str,
1148 ) -> Option<crate::storage::Location> {
1149 if let Some(method) = self.get_method(fqcn, member_name) {
1151 return method.location.clone();
1152 }
1153 if let Some(prop) = self.get_property(fqcn, member_name) {
1155 return prop.location.clone();
1156 }
1157 if let Some(cls) = self.classes.get(fqcn) {
1159 if let Some(c) = cls.own_constants.get(member_name) {
1160 return c.location.clone();
1161 }
1162 }
1163 if let Some(iface) = self.interfaces.get(fqcn) {
1165 if let Some(c) = iface.own_constants.get(member_name) {
1166 return c.location.clone();
1167 }
1168 }
1169 if let Some(tr) = self.traits.get(fqcn) {
1171 if let Some(c) = tr.own_constants.get(member_name) {
1172 return c.location.clone();
1173 }
1174 }
1175 if let Some(en) = self.enums.get(fqcn) {
1177 if let Some(c) = en.own_constants.get(member_name) {
1178 return c.location.clone();
1179 }
1180 if let Some(case) = en.cases.get(member_name) {
1181 return case.location.clone();
1182 }
1183 }
1184 None
1185 }
1186
1187 pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
1193 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1194 let id = self.symbol_interner.intern_str(&key);
1195 self.referenced_methods.insert(id);
1196 }
1197
1198 pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
1200 let key = format!("{fqcn}::{prop_name}");
1201 let id = self.symbol_interner.intern_str(&key);
1202 self.referenced_properties.insert(id);
1203 }
1204
1205 pub fn mark_function_referenced(&self, fqn: &str) {
1207 let id = self.symbol_interner.intern_str(fqn);
1208 self.referenced_functions.insert(id);
1209 }
1210
1211 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
1212 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1213 match self.symbol_interner.get_id(&key) {
1214 Some(id) => self.referenced_methods.contains(&id),
1215 None => false,
1216 }
1217 }
1218
1219 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
1220 let key = format!("{fqcn}::{prop_name}");
1221 match self.symbol_interner.get_id(&key) {
1222 Some(id) => self.referenced_properties.contains(&id),
1223 None => false,
1224 }
1225 }
1226
1227 pub fn is_function_referenced(&self, fqn: &str) -> bool {
1228 match self.symbol_interner.get_id(fqn) {
1229 Some(id) => self.referenced_functions.contains(&id),
1230 None => false,
1231 }
1232 }
1233
1234 pub fn mark_method_referenced_at(
1237 &self,
1238 fqcn: &str,
1239 method_name: &str,
1240 file: Arc<str>,
1241 start: u32,
1242 end: u32,
1243 ) {
1244 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1245 self.ensure_expanded();
1246 let sym_id = self.symbol_interner.intern_str(&key);
1247 let file_id = self.file_interner.intern(file);
1248 self.referenced_methods.insert(sym_id);
1249 record_ref(
1250 &self.symbol_reference_locations,
1251 &self.file_symbol_references,
1252 sym_id,
1253 file_id,
1254 start,
1255 end,
1256 );
1257 }
1258
1259 pub fn mark_property_referenced_at(
1262 &self,
1263 fqcn: &str,
1264 prop_name: &str,
1265 file: Arc<str>,
1266 start: u32,
1267 end: u32,
1268 ) {
1269 let key = format!("{fqcn}::{prop_name}");
1270 self.ensure_expanded();
1271 let sym_id = self.symbol_interner.intern_str(&key);
1272 let file_id = self.file_interner.intern(file);
1273 self.referenced_properties.insert(sym_id);
1274 record_ref(
1275 &self.symbol_reference_locations,
1276 &self.file_symbol_references,
1277 sym_id,
1278 file_id,
1279 start,
1280 end,
1281 );
1282 }
1283
1284 pub fn mark_function_referenced_at(&self, fqn: &str, file: Arc<str>, start: u32, end: u32) {
1287 self.ensure_expanded();
1288 let sym_id = self.symbol_interner.intern_str(fqn);
1289 let file_id = self.file_interner.intern(file);
1290 self.referenced_functions.insert(sym_id);
1291 record_ref(
1292 &self.symbol_reference_locations,
1293 &self.file_symbol_references,
1294 sym_id,
1295 file_id,
1296 start,
1297 end,
1298 );
1299 }
1300
1301 pub fn mark_class_referenced_at(&self, fqcn: &str, file: Arc<str>, start: u32, end: u32) {
1305 self.ensure_expanded();
1306 let sym_id = self.symbol_interner.intern_str(fqcn);
1307 let file_id = self.file_interner.intern(file);
1308 record_ref(
1309 &self.symbol_reference_locations,
1310 &self.file_symbol_references,
1311 sym_id,
1312 file_id,
1313 start,
1314 end,
1315 );
1316 }
1317
1318 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u32)]) {
1322 if locs.is_empty() {
1323 return;
1324 }
1325 self.ensure_expanded();
1326 let file_id = self.file_interner.intern(file);
1327 for (symbol_key, start, end) in locs {
1328 let sym_id = self.symbol_interner.intern_str(symbol_key);
1329 record_ref(
1330 &self.symbol_reference_locations,
1331 &self.file_symbol_references,
1332 sym_id,
1333 file_id,
1334 *start,
1335 *end,
1336 );
1337 }
1338 }
1339
1340 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u32)> {
1343 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
1344 return Vec::new();
1345 };
1346 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1348 let id = sym_id as usize;
1349 if id + 1 >= ci.sym_offsets.len() {
1350 return Vec::new();
1351 }
1352 let start = ci.sym_offsets[id] as usize;
1353 let end = ci.sym_offsets[id + 1] as usize;
1354 return ci.entries[start..end]
1355 .iter()
1356 .map(|&(_, file_id, s, e)| (self.file_interner.get(file_id), s, e))
1357 .collect();
1358 }
1359 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1361 return Vec::new();
1362 };
1363 entries
1364 .iter()
1365 .map(|&(file_id, start, end)| (self.file_interner.get(file_id), start, end))
1366 .collect()
1367 }
1368
1369 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u32)> {
1372 let Some(file_id) = self.file_interner.get_id(file) else {
1373 return Vec::new();
1374 };
1375 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1377 let id = file_id as usize;
1378 if id + 1 >= ci.file_offsets.len() {
1379 return Vec::new();
1380 }
1381 let start = ci.file_offsets[id] as usize;
1382 let end = ci.file_offsets[id + 1] as usize;
1383 return ci.by_file[start..end]
1384 .iter()
1385 .map(|&entry_idx| {
1386 let (sym_id, _, s, e) = ci.entries[entry_idx as usize];
1387 (self.symbol_interner.get(sym_id), s, e)
1388 })
1389 .collect();
1390 }
1391 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
1393 return Vec::new();
1394 };
1395 let mut out = Vec::new();
1396 for &sym_id in sym_ids.iter() {
1397 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1398 continue;
1399 };
1400 let sym_key = self.symbol_interner.get(sym_id);
1401 for &(entry_file_id, start, end) in entries.iter() {
1402 if entry_file_id == file_id {
1403 out.push((sym_key.clone(), start, end));
1404 }
1405 }
1406 }
1407 out
1408 }
1409
1410 pub fn file_has_symbol_references(&self, file: &str) -> bool {
1412 let Some(file_id) = self.file_interner.get_id(file) else {
1413 return false;
1414 };
1415 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1417 let id = file_id as usize;
1418 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
1419 }
1420 self.file_symbol_references.contains_key(&file_id)
1421 }
1422
1423 pub fn finalize(&self) {
1430 if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
1431 return;
1432 }
1433
1434 let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
1436 for fqcn in &class_keys {
1437 let parents = self.collect_class_ancestors(fqcn);
1438 if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
1439 cls.all_parents = parents;
1440 }
1441 }
1442
1443 let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
1445 for fqcn in &iface_keys {
1446 let parents = self.collect_interface_ancestors(fqcn);
1447 if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
1448 iface.all_parents = parents;
1449 }
1450 }
1451
1452 type PendingImports = Vec<(Arc<str>, Vec<(Arc<str>, Arc<str>, Arc<str>)>)>;
1455 let pending: PendingImports = self
1456 .classes
1457 .iter()
1458 .filter(|e| !e.pending_import_types.is_empty())
1459 .map(|e| (e.key().clone(), e.pending_import_types.clone()))
1460 .collect();
1461 for (dst_fqcn, imports) in pending {
1462 let mut resolved: std::collections::HashMap<Arc<str>, mir_types::Union> =
1463 std::collections::HashMap::new();
1464 for (local, original, from_class) in &imports {
1465 if let Some(src_cls) = self.classes.get(from_class.as_ref()) {
1466 if let Some(ty) = src_cls.type_aliases.get(original.as_ref()) {
1467 resolved.insert(local.clone(), ty.clone());
1468 }
1469 }
1470 }
1471 if !resolved.is_empty() {
1472 if let Some(mut dst_cls) = self.classes.get_mut(dst_fqcn.as_ref()) {
1473 for (k, v) in resolved {
1474 dst_cls.type_aliases.insert(k, v);
1475 }
1476 }
1477 }
1478 }
1479
1480 self.finalized
1481 .store(true, std::sync::atomic::Ordering::SeqCst);
1482 }
1483
1484 fn get_method_in_trait(
1492 &self,
1493 tr_fqcn: &Arc<str>,
1494 method_name: &str,
1495 ) -> Option<Arc<MethodStorage>> {
1496 let mut visited = std::collections::HashSet::new();
1497 self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
1498 }
1499
1500 fn get_method_in_trait_inner(
1501 &self,
1502 tr_fqcn: &Arc<str>,
1503 method_name: &str,
1504 visited: &mut std::collections::HashSet<String>,
1505 ) -> Option<Arc<MethodStorage>> {
1506 if !visited.insert(tr_fqcn.to_string()) {
1507 return None; }
1509 let tr = self.traits.get(tr_fqcn.as_ref())?;
1510 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
1511 return Some(Arc::clone(m));
1512 }
1513 let used_traits = tr.traits.clone();
1514 drop(tr);
1515 for used_fqcn in &used_traits {
1516 if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
1517 return Some(m);
1518 }
1519 }
1520 None
1521 }
1522
1523 fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1524 let mut result = Vec::new();
1525 let mut visited = std::collections::HashSet::new();
1526 self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
1527 result
1528 }
1529
1530 fn collect_class_ancestors_inner(
1531 &self,
1532 fqcn: &str,
1533 out: &mut Vec<Arc<str>>,
1534 visited: &mut std::collections::HashSet<String>,
1535 ) {
1536 if !visited.insert(fqcn.to_string()) {
1537 return; }
1539 let (parent, interfaces, traits) = {
1540 if let Some(cls) = self.classes.get(fqcn) {
1541 (
1542 cls.parent.clone(),
1543 cls.interfaces.clone(),
1544 cls.traits.clone(),
1545 )
1546 } else {
1547 return;
1548 }
1549 };
1550
1551 if let Some(p) = parent {
1552 out.push(p.clone());
1553 self.collect_class_ancestors_inner(&p, out, visited);
1554 }
1555 for iface in interfaces {
1556 out.push(iface.clone());
1557 self.collect_interface_ancestors_inner(&iface, out, visited);
1558 }
1559 for t in traits {
1560 out.push(t);
1561 }
1562 }
1563
1564 fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1565 let mut result = Vec::new();
1566 let mut visited = std::collections::HashSet::new();
1567 self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
1568 result
1569 }
1570
1571 fn collect_interface_ancestors_inner(
1572 &self,
1573 fqcn: &str,
1574 out: &mut Vec<Arc<str>>,
1575 visited: &mut std::collections::HashSet<String>,
1576 ) {
1577 if !visited.insert(fqcn.to_string()) {
1578 return;
1579 }
1580 let extends = {
1581 if let Some(iface) = self.interfaces.get(fqcn) {
1582 iface.extends.clone()
1583 } else {
1584 return;
1585 }
1586 };
1587 for e in extends {
1588 out.push(e.clone());
1589 self.collect_interface_ancestors_inner(&e, out, visited);
1590 }
1591 }
1592}
1593
1594pub struct CodebaseBuilder {
1606 cb: Codebase,
1607}
1608
1609impl CodebaseBuilder {
1610 pub fn new() -> Self {
1611 Self {
1612 cb: Codebase::new(),
1613 }
1614 }
1615
1616 pub fn add(&mut self, slice: crate::storage::StubSlice) {
1619 self.cb.inject_stub_slice(slice);
1620 }
1621
1622 pub fn finalize(self) -> Codebase {
1624 self.cb.finalize();
1625 self.cb
1626 }
1627
1628 pub fn codebase(&self) -> &Codebase {
1630 &self.cb
1631 }
1632}
1633
1634impl Default for CodebaseBuilder {
1635 fn default() -> Self {
1636 Self::new()
1637 }
1638}
1639
1640pub fn codebase_from_parts(parts: Vec<crate::storage::StubSlice>) -> Codebase {
1642 let mut b = CodebaseBuilder::new();
1643 for p in parts {
1644 b.add(p);
1645 }
1646 b.finalize()
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651 use super::*;
1652
1653 fn arc(s: &str) -> Arc<str> {
1654 Arc::from(s)
1655 }
1656
1657 #[test]
1658 fn method_referenced_at_groups_spans_by_file() {
1659 let cb = Codebase::new();
1660 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1661 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 10, 15);
1662 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 20, 25);
1663
1664 let locs = cb.get_reference_locations("Foo::bar");
1665 let files: std::collections::HashSet<&str> =
1666 locs.iter().map(|(f, _, _)| f.as_ref()).collect();
1667 assert_eq!(files.len(), 2, "two files, not three spans");
1668 assert!(locs.contains(&(arc("a.php"), 0, 5)));
1669 assert!(locs.contains(&(arc("a.php"), 10, 15)));
1670 assert_eq!(
1671 locs.iter()
1672 .filter(|(f, _, _)| f.as_ref() == "a.php")
1673 .count(),
1674 2
1675 );
1676 assert!(locs.contains(&(arc("b.php"), 20, 25)));
1677 assert!(
1678 cb.is_method_referenced("Foo", "bar"),
1679 "DashSet also updated"
1680 );
1681 }
1682
1683 #[test]
1684 fn duplicate_spans_are_deduplicated() {
1685 let cb = Codebase::new();
1686 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1688 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1689
1690 let count = cb
1691 .get_reference_locations("Foo::bar")
1692 .iter()
1693 .filter(|(f, _, _)| f.as_ref() == "a.php")
1694 .count();
1695 assert_eq!(count, 1, "duplicate span deduplicated");
1696 }
1697
1698 #[test]
1699 fn method_key_is_lowercased() {
1700 let cb = Codebase::new();
1701 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 0, 3);
1702 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
1703 }
1704
1705 #[test]
1706 fn property_referenced_at_records_location() {
1707 let cb = Codebase::new();
1708 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 5, 10);
1709
1710 assert!(cb
1711 .get_reference_locations("Bar::count")
1712 .contains(&(arc("x.php"), 5, 10)));
1713 assert!(cb.is_property_referenced("Bar", "count"));
1714 }
1715
1716 #[test]
1717 fn function_referenced_at_records_location() {
1718 let cb = Codebase::new();
1719 cb.mark_function_referenced_at("my_fn", arc("a.php"), 10, 15);
1720
1721 assert!(cb
1722 .get_reference_locations("my_fn")
1723 .contains(&(arc("a.php"), 10, 15)));
1724 assert!(cb.is_function_referenced("my_fn"));
1725 }
1726
1727 #[test]
1728 fn class_referenced_at_records_location() {
1729 let cb = Codebase::new();
1730 cb.mark_class_referenced_at("Foo", arc("a.php"), 5, 8);
1731
1732 assert!(cb
1733 .get_reference_locations("Foo")
1734 .contains(&(arc("a.php"), 5, 8)));
1735 }
1736
1737 #[test]
1738 fn get_reference_locations_flattens_all_files() {
1739 let cb = Codebase::new();
1740 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1741 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1742
1743 let mut locs = cb.get_reference_locations("fn1");
1744 locs.sort_by_key(|(_, s, _)| *s);
1745 assert_eq!(locs.len(), 2);
1746 assert_eq!(locs[0], (arc("a.php"), 0, 5));
1747 assert_eq!(locs[1], (arc("b.php"), 10, 15));
1748 }
1749
1750 #[test]
1751 fn replay_reference_locations_restores_index() {
1752 let cb = Codebase::new();
1753 let locs = vec![
1754 ("Foo::bar".to_string(), 0u32, 5u32),
1755 ("Foo::bar".to_string(), 10, 15),
1756 ("greet".to_string(), 20, 25),
1757 ];
1758 cb.replay_reference_locations(arc("a.php"), &locs);
1759
1760 let bar_locs = cb.get_reference_locations("Foo::bar");
1761 assert!(bar_locs.contains(&(arc("a.php"), 0, 5)));
1762 assert!(bar_locs.contains(&(arc("a.php"), 10, 15)));
1763
1764 assert!(cb
1765 .get_reference_locations("greet")
1766 .contains(&(arc("a.php"), 20, 25)));
1767
1768 assert!(cb.file_has_symbol_references("a.php"));
1769 }
1770
1771 #[test]
1772 fn remove_file_clears_its_spans_only() {
1773 let cb = Codebase::new();
1774 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1775 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1776
1777 cb.remove_file_definitions("a.php");
1778
1779 let locs = cb.get_reference_locations("fn1");
1780 assert!(
1781 !locs.iter().any(|(f, _, _)| f.as_ref() == "a.php"),
1782 "a.php spans removed"
1783 );
1784 assert!(
1785 locs.contains(&(arc("b.php"), 10, 15)),
1786 "b.php spans untouched"
1787 );
1788 assert!(!cb.file_has_symbol_references("a.php"));
1789 }
1790
1791 #[test]
1792 fn remove_file_does_not_affect_other_files() {
1793 let cb = Codebase::new();
1794 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 4);
1795 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 7, 10);
1796
1797 cb.remove_file_definitions("x.php");
1798
1799 let locs = cb.get_reference_locations("Cls::prop");
1800 assert!(!locs.iter().any(|(f, _, _)| f.as_ref() == "x.php"));
1801 assert!(locs.contains(&(arc("y.php"), 7, 10)));
1802 }
1803
1804 #[test]
1805 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1806 let cb = Codebase::new();
1807 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1808
1809 cb.remove_file_definitions("ghost.php");
1811
1812 assert!(cb
1814 .get_reference_locations("fn1")
1815 .contains(&(arc("a.php"), 0, 5)));
1816 assert!(!cb.file_has_symbol_references("ghost.php"));
1817 }
1818
1819 #[test]
1820 fn replay_reference_locations_with_empty_list_is_noop() {
1821 let cb = Codebase::new();
1822 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1823
1824 cb.replay_reference_locations(arc("b.php"), &[]);
1826
1827 assert!(
1828 !cb.file_has_symbol_references("b.php"),
1829 "empty replay must not create a file entry"
1830 );
1831 assert!(
1832 cb.get_reference_locations("fn1")
1833 .contains(&(arc("a.php"), 0, 5)),
1834 "existing spans untouched"
1835 );
1836 }
1837
1838 #[test]
1839 fn replay_reference_locations_twice_does_not_duplicate_spans() {
1840 let cb = Codebase::new();
1841 let locs = vec![("fn1".to_string(), 0u32, 5u32)];
1842
1843 cb.replay_reference_locations(arc("a.php"), &locs);
1844 cb.replay_reference_locations(arc("a.php"), &locs);
1845
1846 let count = cb
1847 .get_reference_locations("fn1")
1848 .iter()
1849 .filter(|(f, _, _)| f.as_ref() == "a.php")
1850 .count();
1851 assert_eq!(
1852 count, 1,
1853 "replaying the same location twice must not create duplicate spans"
1854 );
1855 }
1856
1857 fn make_fn(fqn: &str, short_name: &str) -> crate::storage::FunctionStorage {
1862 crate::storage::FunctionStorage {
1863 fqn: Arc::from(fqn),
1864 short_name: Arc::from(short_name),
1865 params: vec![],
1866 return_type: None,
1867 inferred_return_type: None,
1868 template_params: vec![],
1869 assertions: vec![],
1870 throws: vec![],
1871 deprecated: None,
1872 is_pure: false,
1873 location: None,
1874 }
1875 }
1876
1877 #[test]
1878 fn inject_stub_slice_later_injection_overwrites_earlier() {
1879 let cb = Codebase::new();
1880
1881 cb.inject_stub_slice(crate::storage::StubSlice {
1882 functions: vec![make_fn("strlen", "phpstorm_version")],
1883 file: Some(Arc::from("phpstorm/standard.php")),
1884 ..Default::default()
1885 });
1886 assert_eq!(
1887 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1888 "phpstorm_version"
1889 );
1890
1891 cb.inject_stub_slice(crate::storage::StubSlice {
1892 functions: vec![make_fn("strlen", "custom_version")],
1893 file: Some(Arc::from("stubs/standard/basic.php")),
1894 ..Default::default()
1895 });
1896
1897 assert_eq!(
1898 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1899 "custom_version",
1900 "custom stub must overwrite phpstorm stub"
1901 );
1902 assert_eq!(
1903 cb.symbol_to_file.get("strlen").unwrap().as_ref(),
1904 "stubs/standard/basic.php",
1905 "symbol_to_file must point to the overriding file"
1906 );
1907 }
1908
1909 #[test]
1910 fn inject_stub_slice_constants_not_added_to_symbol_to_file() {
1911 let cb = Codebase::new();
1912
1913 cb.inject_stub_slice(crate::storage::StubSlice {
1914 constants: vec![(Arc::from("PHP_EOL"), mir_types::Union::empty())],
1915 file: Some(Arc::from("stubs/core/constants.php")),
1916 ..Default::default()
1917 });
1918
1919 assert!(
1920 cb.constants.contains_key("PHP_EOL"),
1921 "constant must be registered in constants map"
1922 );
1923 assert!(
1924 !cb.symbol_to_file.contains_key("PHP_EOL"),
1925 "constants must not appear in symbol_to_file — go-to-definition is not supported for them"
1926 );
1927 }
1928
1929 #[test]
1930 fn remove_file_definitions_purges_injected_global_vars() {
1931 let cb = Codebase::new();
1932
1933 cb.inject_stub_slice(crate::storage::StubSlice {
1934 global_vars: vec![(Arc::from("db_connection"), mir_types::Union::empty())],
1935 file: Some(Arc::from("src/bootstrap.php")),
1936 ..Default::default()
1937 });
1938 assert!(
1939 cb.global_vars.contains_key("db_connection"),
1940 "global var must be registered after injection"
1941 );
1942
1943 cb.remove_file_definitions("src/bootstrap.php");
1944
1945 assert!(
1946 !cb.global_vars.contains_key("db_connection"),
1947 "global var must be removed when its defining file is removed"
1948 );
1949 }
1950
1951 #[test]
1952 fn inject_stub_slice_without_file_discards_global_vars() {
1953 let cb = Codebase::new();
1954
1955 cb.inject_stub_slice(crate::storage::StubSlice {
1956 global_vars: vec![(Arc::from("orphan_var"), mir_types::Union::empty())],
1957 file: None,
1958 ..Default::default()
1959 });
1960
1961 assert!(
1962 !cb.global_vars.contains_key("orphan_var"),
1963 "global_vars must not be registered when slice.file is None"
1964 );
1965 }
1966}