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 self.get_property_inner(fqcn, prop_name, &mut std::collections::HashSet::new())
618 }
619
620 fn get_property_inner(
621 &self,
622 fqcn: &str,
623 prop_name: &str,
624 visited: &mut std::collections::HashSet<String>,
625 ) -> Option<crate::storage::PropertyStorage> {
626 if !visited.insert(fqcn.to_string()) {
627 return None;
628 }
629 if let Some(cls) = self.classes.get(fqcn) {
631 if let Some(p) = cls.own_properties.get(prop_name) {
632 return Some(p.clone());
633 }
634 let mixins = cls.mixins.clone();
635 drop(cls);
636 for mixin in &mixins {
637 if let Some(p) = self.get_property_inner(mixin.as_ref(), prop_name, visited) {
638 return Some(p);
639 }
640 }
641 }
642
643 let all_parents = {
645 if let Some(cls) = self.classes.get(fqcn) {
646 cls.all_parents.clone()
647 } else {
648 return None;
649 }
650 };
651
652 for ancestor_fqcn in &all_parents {
653 if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
654 if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
655 return Some(p.clone());
656 }
657 let anc_mixins = ancestor_cls.mixins.clone();
658 drop(ancestor_cls);
659 for mixin_fqcn in &anc_mixins {
660 if let Some(p) = self.get_property_inner(mixin_fqcn, prop_name, visited) {
661 return Some(p);
662 }
663 }
664 }
665 }
666
667 let trait_list = {
669 if let Some(cls) = self.classes.get(fqcn) {
670 cls.traits.clone()
671 } else {
672 vec![]
673 }
674 };
675 for trait_fqcn in &trait_list {
676 if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
677 if let Some(p) = tr.own_properties.get(prop_name) {
678 return Some(p.clone());
679 }
680 }
681 }
682
683 None
684 }
685
686 pub fn get_class_constant(
688 &self,
689 fqcn: &str,
690 const_name: &str,
691 ) -> Option<crate::storage::ConstantStorage> {
692 if let Some(cls) = self.classes.get(fqcn) {
694 if let Some(c) = cls.own_constants.get(const_name) {
695 return Some(c.clone());
696 }
697 let all_parents = cls.all_parents.clone();
698 let interfaces = cls.interfaces.clone();
699 let traits = cls.traits.clone();
700 drop(cls);
701
702 for tr_fqcn in &traits {
703 if let Some(tr) = self.traits.get(tr_fqcn.as_ref()) {
704 if let Some(c) = tr.own_constants.get(const_name) {
705 return Some(c.clone());
706 }
707 }
708 }
709
710 for ancestor_fqcn in &all_parents {
711 if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
712 if let Some(c) = ancestor.own_constants.get(const_name) {
713 return Some(c.clone());
714 }
715 }
716 if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
717 if let Some(c) = iface.own_constants.get(const_name) {
718 return Some(c.clone());
719 }
720 }
721 }
722
723 for iface_fqcn in &interfaces {
724 if let Some(iface) = self.interfaces.get(iface_fqcn.as_ref()) {
725 if let Some(c) = iface.own_constants.get(const_name) {
726 return Some(c.clone());
727 }
728 }
729 }
730
731 return None;
732 }
733
734 if let Some(iface) = self.interfaces.get(fqcn) {
736 if let Some(c) = iface.own_constants.get(const_name) {
737 return Some(c.clone());
738 }
739 let parents = iface.all_parents.clone();
740 drop(iface);
741 for p in &parents {
742 if let Some(parent_iface) = self.interfaces.get(p.as_ref()) {
743 if let Some(c) = parent_iface.own_constants.get(const_name) {
744 return Some(c.clone());
745 }
746 }
747 }
748 return None;
749 }
750
751 if let Some(en) = self.enums.get(fqcn) {
753 if let Some(c) = en.own_constants.get(const_name) {
754 return Some(c.clone());
755 }
756 if en.cases.contains_key(const_name) {
757 return Some(crate::storage::ConstantStorage {
758 name: Arc::from(const_name),
759 ty: mir_types::Union::mixed(),
760 visibility: None,
761 is_final: false,
762 location: None,
763 });
764 }
765 return None;
766 }
767
768 if let Some(tr) = self.traits.get(fqcn) {
770 if let Some(c) = tr.own_constants.get(const_name) {
771 return Some(c.clone());
772 }
773 return None;
774 }
775
776 None
777 }
778
779 pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
781 self.get_method_inner(fqcn, method_name, &mut std::collections::HashSet::new())
782 }
783
784 fn get_method_inner(
785 &self,
786 fqcn: &str,
787 method_name: &str,
788 visited: &mut std::collections::HashSet<String>,
789 ) -> Option<Arc<MethodStorage>> {
790 if !visited.insert(fqcn.to_string()) {
791 return None;
792 }
793 let method_lower = method_name.to_lowercase();
795 let method_name = method_lower.as_str();
796
797 if let Some(cls) = self.classes.get(fqcn) {
799 if let Some(m) = lookup_method(&cls.own_methods, method_name) {
801 return Some(Arc::clone(m));
802 }
803 let own_traits = cls.traits.clone();
805 let ancestors = cls.all_parents.clone();
806 let mixins = cls.mixins.clone();
807 drop(cls);
808
809 for mixin_fqcn in &mixins {
811 if let Some(m) = self.get_method_inner(mixin_fqcn, method_name, visited) {
812 return Some(m);
813 }
814 }
815
816 for tr_fqcn in &own_traits {
818 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
819 return Some(m);
820 }
821 }
822
823 for ancestor_fqcn in &ancestors {
825 if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
826 if let Some(m) = lookup_method(&anc.own_methods, method_name) {
827 return Some(Arc::clone(m));
828 }
829 let anc_traits = anc.traits.clone();
830 let anc_mixins = anc.mixins.clone();
831 drop(anc);
832 for tr_fqcn in &anc_traits {
833 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
834 return Some(m);
835 }
836 }
837 for mixin_fqcn in &anc_mixins {
838 if let Some(m) = self.get_method_inner(mixin_fqcn, method_name, visited) {
839 return Some(m);
840 }
841 }
842 } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
843 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
844 let mut ms = (**m).clone();
845 ms.is_abstract = true;
846 return Some(Arc::new(ms));
847 }
848 }
849 }
851 return None;
852 }
853
854 if let Some(iface) = self.interfaces.get(fqcn) {
856 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
857 return Some(Arc::clone(m));
858 }
859 let parents = iface.all_parents.clone();
860 drop(iface);
861 for parent_fqcn in &parents {
862 if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
863 if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
864 return Some(Arc::clone(m));
865 }
866 }
867 }
868 return None;
869 }
870
871 if let Some(tr) = self.traits.get(fqcn) {
873 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
874 return Some(Arc::clone(m));
875 }
876 return None;
877 }
878
879 if let Some(e) = self.enums.get(fqcn) {
881 if let Some(m) = lookup_method(&e.own_methods, method_name) {
882 return Some(Arc::clone(m));
883 }
884 if matches!(method_name, "cases" | "from" | "tryfrom") {
886 return Some(Arc::new(crate::storage::MethodStorage {
887 fqcn: Arc::from(fqcn),
888 name: Arc::from(method_name),
889 params: vec![],
890 return_type: Some(mir_types::Union::mixed()),
891 inferred_return_type: None,
892 visibility: crate::storage::Visibility::Public,
893 is_static: true,
894 is_abstract: false,
895 is_constructor: false,
896 template_params: vec![],
897 assertions: vec![],
898 throws: vec![],
899 is_final: false,
900 is_internal: false,
901 is_pure: false,
902 deprecated: None,
903 location: None,
904 }));
905 }
906 }
907
908 None
909 }
910
911 pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
913 if child == ancestor {
914 return true;
915 }
916 if let Some(cls) = self.classes.get(child) {
917 return cls.implements_or_extends(ancestor);
918 }
919 if let Some(iface) = self.interfaces.get(child) {
920 return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
921 }
922 if let Some(en) = self.enums.get(child) {
925 if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
927 return true;
928 }
929 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
931 return true;
932 }
933 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
935 {
936 return true;
937 }
938 }
939 false
940 }
941
942 pub fn type_exists(&self, fqcn: &str) -> bool {
944 self.classes.contains_key(fqcn)
945 || self.interfaces.contains_key(fqcn)
946 || self.traits.contains_key(fqcn)
947 || self.enums.contains_key(fqcn)
948 }
949
950 pub fn function_exists(&self, fqn: &str) -> bool {
951 self.functions.contains_key(fqn)
952 }
953
954 pub fn is_abstract_class(&self, fqcn: &str) -> bool {
958 self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
959 }
960
961 pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
964 if let Some(cls) = self.classes.get(fqcn) {
965 return cls.template_params.clone();
966 }
967 if let Some(iface) = self.interfaces.get(fqcn) {
968 return iface.template_params.clone();
969 }
970 if let Some(tr) = self.traits.get(fqcn) {
971 return tr.template_params.clone();
972 }
973 vec![]
974 }
975
976 pub fn get_inherited_template_bindings(
981 &self,
982 fqcn: &str,
983 ) -> std::collections::HashMap<Arc<str>, Union> {
984 let mut bindings = std::collections::HashMap::new();
985 let mut current = fqcn.to_string();
986
987 loop {
988 let (parent_fqcn, extends_type_args) = {
989 let cls = match self.classes.get(current.as_str()) {
990 Some(c) => c,
991 None => break,
992 };
993 let parent = match &cls.parent {
994 Some(p) => p.clone(),
995 None => break,
996 };
997 let args = cls.extends_type_args.clone();
998 (parent, args)
999 };
1000
1001 if !extends_type_args.is_empty() {
1002 let parent_tps = self.get_class_template_params(&parent_fqcn);
1003 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
1004 bindings
1005 .entry(tp.name.clone())
1006 .or_insert_with(|| ty.clone());
1007 }
1008 }
1009
1010 current = parent_fqcn.to_string();
1011 }
1012
1013 bindings
1014 }
1015
1016 pub fn has_magic_get(&self, fqcn: &str) -> bool {
1019 self.get_method(fqcn, "__get").is_some()
1020 }
1021
1022 pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
1030 if let Some(iface) = self.interfaces.get(fqcn) {
1032 let parents = iface.all_parents.clone();
1033 drop(iface);
1034 for p in &parents {
1035 if !self.type_exists(p.as_ref()) {
1036 return true;
1037 }
1038 }
1039 return false;
1040 }
1041
1042 let (parent, interfaces, traits, all_parents) = {
1044 let Some(cls) = self.classes.get(fqcn) else {
1045 return false;
1046 };
1047 (
1048 cls.parent.clone(),
1049 cls.interfaces.clone(),
1050 cls.traits.clone(),
1051 cls.all_parents.clone(),
1052 )
1053 };
1054
1055 if let Some(ref p) = parent {
1057 if !self.type_exists(p.as_ref()) {
1058 return true;
1059 }
1060 }
1061 for iface in &interfaces {
1062 if !self.type_exists(iface.as_ref()) {
1063 return true;
1064 }
1065 }
1066 for tr in &traits {
1067 if !self.type_exists(tr.as_ref()) {
1068 return true;
1069 }
1070 }
1071
1072 for ancestor in &all_parents {
1074 if !self.type_exists(ancestor.as_ref()) {
1075 return true;
1076 }
1077 }
1078
1079 false
1080 }
1081
1082 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
1089 let name = name.trim_start_matches('\\');
1090 if name.is_empty() {
1091 return name.to_string();
1092 }
1093 if name.contains('\\') {
1098 let first_segment = name.split('\\').next().unwrap_or(name);
1100 if let Some(imports) = self.file_imports.get(file) {
1101 if let Some(resolved_prefix) = imports.get(first_segment) {
1102 let rest = &name[first_segment.len()..]; return format!("{resolved_prefix}{rest}");
1104 }
1105 }
1106 if self.type_exists(name) {
1108 return name.to_string();
1109 }
1110 if let Some(ns) = self.file_namespaces.get(file) {
1112 let qualified = format!("{}\\{}", *ns, name);
1113 if self.type_exists(&qualified) {
1114 return qualified;
1115 }
1116 }
1117 return name.to_string();
1118 }
1119 match name {
1121 "self" | "parent" | "static" | "this" => return name.to_string(),
1122 _ => {}
1123 }
1124 if let Some(imports) = self.file_imports.get(file) {
1126 if let Some(resolved) = imports.get(name) {
1127 return resolved.clone();
1128 }
1129 let name_lower = name.to_lowercase();
1131 for (alias, resolved) in imports.iter() {
1132 if alias.to_lowercase() == name_lower {
1133 return resolved.clone();
1134 }
1135 }
1136 }
1137 if let Some(ns) = self.file_namespaces.get(file) {
1139 let qualified = format!("{}\\{}", *ns, name);
1140 if self.type_exists(&qualified) {
1145 return qualified;
1146 }
1147 if self.type_exists(name) {
1148 return name.to_string();
1149 }
1150 return qualified;
1151 }
1152 name.to_string()
1153 }
1154
1155 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
1162 if let Some(cls) = self.classes.get(fqcn) {
1163 return cls.location.clone();
1164 }
1165 if let Some(iface) = self.interfaces.get(fqcn) {
1166 return iface.location.clone();
1167 }
1168 if let Some(tr) = self.traits.get(fqcn) {
1169 return tr.location.clone();
1170 }
1171 if let Some(en) = self.enums.get(fqcn) {
1172 return en.location.clone();
1173 }
1174 if let Some(func) = self.functions.get(fqcn) {
1175 return func.location.clone();
1176 }
1177 None
1178 }
1179
1180 pub fn get_member_location(
1182 &self,
1183 fqcn: &str,
1184 member_name: &str,
1185 ) -> Option<crate::storage::Location> {
1186 if let Some(method) = self.get_method(fqcn, member_name) {
1188 return method.location.clone();
1189 }
1190 if let Some(prop) = self.get_property(fqcn, member_name) {
1192 return prop.location.clone();
1193 }
1194 if let Some(cls) = self.classes.get(fqcn) {
1196 if let Some(c) = cls.own_constants.get(member_name) {
1197 return c.location.clone();
1198 }
1199 }
1200 if let Some(iface) = self.interfaces.get(fqcn) {
1202 if let Some(c) = iface.own_constants.get(member_name) {
1203 return c.location.clone();
1204 }
1205 }
1206 if let Some(tr) = self.traits.get(fqcn) {
1208 if let Some(c) = tr.own_constants.get(member_name) {
1209 return c.location.clone();
1210 }
1211 }
1212 if let Some(en) = self.enums.get(fqcn) {
1214 if let Some(c) = en.own_constants.get(member_name) {
1215 return c.location.clone();
1216 }
1217 if let Some(case) = en.cases.get(member_name) {
1218 return case.location.clone();
1219 }
1220 }
1221 None
1222 }
1223
1224 pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
1230 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1231 let id = self.symbol_interner.intern_str(&key);
1232 self.referenced_methods.insert(id);
1233 }
1234
1235 pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
1237 let key = format!("{fqcn}::{prop_name}");
1238 let id = self.symbol_interner.intern_str(&key);
1239 self.referenced_properties.insert(id);
1240 }
1241
1242 pub fn mark_function_referenced(&self, fqn: &str) {
1244 let id = self.symbol_interner.intern_str(fqn);
1245 self.referenced_functions.insert(id);
1246 }
1247
1248 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
1249 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1250 match self.symbol_interner.get_id(&key) {
1251 Some(id) => self.referenced_methods.contains(&id),
1252 None => false,
1253 }
1254 }
1255
1256 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
1257 let key = format!("{fqcn}::{prop_name}");
1258 match self.symbol_interner.get_id(&key) {
1259 Some(id) => self.referenced_properties.contains(&id),
1260 None => false,
1261 }
1262 }
1263
1264 pub fn is_function_referenced(&self, fqn: &str) -> bool {
1265 match self.symbol_interner.get_id(fqn) {
1266 Some(id) => self.referenced_functions.contains(&id),
1267 None => false,
1268 }
1269 }
1270
1271 pub fn mark_method_referenced_at(
1274 &self,
1275 fqcn: &str,
1276 method_name: &str,
1277 file: Arc<str>,
1278 start: u32,
1279 end: u32,
1280 ) {
1281 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1282 self.ensure_expanded();
1283 let sym_id = self.symbol_interner.intern_str(&key);
1284 let file_id = self.file_interner.intern(file);
1285 self.referenced_methods.insert(sym_id);
1286 record_ref(
1287 &self.symbol_reference_locations,
1288 &self.file_symbol_references,
1289 sym_id,
1290 file_id,
1291 start,
1292 end,
1293 );
1294 }
1295
1296 pub fn mark_property_referenced_at(
1299 &self,
1300 fqcn: &str,
1301 prop_name: &str,
1302 file: Arc<str>,
1303 start: u32,
1304 end: u32,
1305 ) {
1306 let key = format!("{fqcn}::{prop_name}");
1307 self.ensure_expanded();
1308 let sym_id = self.symbol_interner.intern_str(&key);
1309 let file_id = self.file_interner.intern(file);
1310 self.referenced_properties.insert(sym_id);
1311 record_ref(
1312 &self.symbol_reference_locations,
1313 &self.file_symbol_references,
1314 sym_id,
1315 file_id,
1316 start,
1317 end,
1318 );
1319 }
1320
1321 pub fn mark_function_referenced_at(&self, fqn: &str, file: Arc<str>, start: u32, end: u32) {
1324 self.ensure_expanded();
1325 let sym_id = self.symbol_interner.intern_str(fqn);
1326 let file_id = self.file_interner.intern(file);
1327 self.referenced_functions.insert(sym_id);
1328 record_ref(
1329 &self.symbol_reference_locations,
1330 &self.file_symbol_references,
1331 sym_id,
1332 file_id,
1333 start,
1334 end,
1335 );
1336 }
1337
1338 pub fn mark_class_referenced_at(&self, fqcn: &str, file: Arc<str>, start: u32, end: u32) {
1342 self.ensure_expanded();
1343 let sym_id = self.symbol_interner.intern_str(fqcn);
1344 let file_id = self.file_interner.intern(file);
1345 record_ref(
1346 &self.symbol_reference_locations,
1347 &self.file_symbol_references,
1348 sym_id,
1349 file_id,
1350 start,
1351 end,
1352 );
1353 }
1354
1355 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u32)]) {
1359 if locs.is_empty() {
1360 return;
1361 }
1362 self.ensure_expanded();
1363 let file_id = self.file_interner.intern(file);
1364 for (symbol_key, start, end) in locs {
1365 let sym_id = self.symbol_interner.intern_str(symbol_key);
1366 record_ref(
1367 &self.symbol_reference_locations,
1368 &self.file_symbol_references,
1369 sym_id,
1370 file_id,
1371 *start,
1372 *end,
1373 );
1374 }
1375 }
1376
1377 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u32)> {
1380 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
1381 return Vec::new();
1382 };
1383 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1385 let id = sym_id as usize;
1386 if id + 1 >= ci.sym_offsets.len() {
1387 return Vec::new();
1388 }
1389 let start = ci.sym_offsets[id] as usize;
1390 let end = ci.sym_offsets[id + 1] as usize;
1391 return ci.entries[start..end]
1392 .iter()
1393 .map(|&(_, file_id, s, e)| (self.file_interner.get(file_id), s, e))
1394 .collect();
1395 }
1396 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1398 return Vec::new();
1399 };
1400 entries
1401 .iter()
1402 .map(|&(file_id, start, end)| (self.file_interner.get(file_id), start, end))
1403 .collect()
1404 }
1405
1406 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u32)> {
1409 let Some(file_id) = self.file_interner.get_id(file) else {
1410 return Vec::new();
1411 };
1412 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1414 let id = file_id as usize;
1415 if id + 1 >= ci.file_offsets.len() {
1416 return Vec::new();
1417 }
1418 let start = ci.file_offsets[id] as usize;
1419 let end = ci.file_offsets[id + 1] as usize;
1420 return ci.by_file[start..end]
1421 .iter()
1422 .map(|&entry_idx| {
1423 let (sym_id, _, s, e) = ci.entries[entry_idx as usize];
1424 (self.symbol_interner.get(sym_id), s, e)
1425 })
1426 .collect();
1427 }
1428 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
1430 return Vec::new();
1431 };
1432 let mut out = Vec::new();
1433 for &sym_id in sym_ids.iter() {
1434 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1435 continue;
1436 };
1437 let sym_key = self.symbol_interner.get(sym_id);
1438 for &(entry_file_id, start, end) in entries.iter() {
1439 if entry_file_id == file_id {
1440 out.push((sym_key.clone(), start, end));
1441 }
1442 }
1443 }
1444 out
1445 }
1446
1447 pub fn file_has_symbol_references(&self, file: &str) -> bool {
1449 let Some(file_id) = self.file_interner.get_id(file) else {
1450 return false;
1451 };
1452 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1454 let id = file_id as usize;
1455 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
1456 }
1457 self.file_symbol_references.contains_key(&file_id)
1458 }
1459
1460 pub fn finalize(&self) {
1467 if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
1468 return;
1469 }
1470
1471 let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
1473 for fqcn in &class_keys {
1474 let parents = self.collect_class_ancestors(fqcn);
1475 if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
1476 cls.all_parents = parents;
1477 }
1478 }
1479
1480 let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
1482 for fqcn in &iface_keys {
1483 let parents = self.collect_interface_ancestors(fqcn);
1484 if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
1485 iface.all_parents = parents;
1486 }
1487 }
1488
1489 type PendingImports = Vec<(Arc<str>, Vec<(Arc<str>, Arc<str>, Arc<str>)>)>;
1492 let pending: PendingImports = self
1493 .classes
1494 .iter()
1495 .filter(|e| !e.pending_import_types.is_empty())
1496 .map(|e| (e.key().clone(), e.pending_import_types.clone()))
1497 .collect();
1498 for (dst_fqcn, imports) in pending {
1499 let mut resolved: std::collections::HashMap<Arc<str>, mir_types::Union> =
1500 std::collections::HashMap::new();
1501 for (local, original, from_class) in &imports {
1502 if let Some(src_cls) = self.classes.get(from_class.as_ref()) {
1503 if let Some(ty) = src_cls.type_aliases.get(original.as_ref()) {
1504 resolved.insert(local.clone(), ty.clone());
1505 }
1506 }
1507 }
1508 if !resolved.is_empty() {
1509 if let Some(mut dst_cls) = self.classes.get_mut(dst_fqcn.as_ref()) {
1510 for (k, v) in resolved {
1511 dst_cls.type_aliases.insert(k, v);
1512 }
1513 }
1514 }
1515 }
1516
1517 self.finalized
1518 .store(true, std::sync::atomic::Ordering::SeqCst);
1519 }
1520
1521 fn get_method_in_trait(
1529 &self,
1530 tr_fqcn: &Arc<str>,
1531 method_name: &str,
1532 ) -> Option<Arc<MethodStorage>> {
1533 let mut visited = std::collections::HashSet::new();
1534 self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
1535 }
1536
1537 fn get_method_in_trait_inner(
1538 &self,
1539 tr_fqcn: &Arc<str>,
1540 method_name: &str,
1541 visited: &mut std::collections::HashSet<String>,
1542 ) -> Option<Arc<MethodStorage>> {
1543 if !visited.insert(tr_fqcn.to_string()) {
1544 return None; }
1546 let tr = self.traits.get(tr_fqcn.as_ref())?;
1547 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
1548 return Some(Arc::clone(m));
1549 }
1550 let used_traits = tr.traits.clone();
1551 drop(tr);
1552 for used_fqcn in &used_traits {
1553 if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
1554 return Some(m);
1555 }
1556 }
1557 None
1558 }
1559
1560 fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1561 let mut result = Vec::new();
1562 let mut visited = std::collections::HashSet::new();
1563 self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
1564 result
1565 }
1566
1567 fn collect_class_ancestors_inner(
1568 &self,
1569 fqcn: &str,
1570 out: &mut Vec<Arc<str>>,
1571 visited: &mut std::collections::HashSet<String>,
1572 ) {
1573 if !visited.insert(fqcn.to_string()) {
1574 return; }
1576 let (parent, interfaces, traits) = {
1577 if let Some(cls) = self.classes.get(fqcn) {
1578 (
1579 cls.parent.clone(),
1580 cls.interfaces.clone(),
1581 cls.traits.clone(),
1582 )
1583 } else {
1584 return;
1585 }
1586 };
1587
1588 if let Some(p) = parent {
1589 out.push(p.clone());
1590 self.collect_class_ancestors_inner(&p, out, visited);
1591 }
1592 for iface in interfaces {
1593 out.push(iface.clone());
1594 self.collect_interface_ancestors_inner(&iface, out, visited);
1595 }
1596 for t in traits {
1597 out.push(t);
1598 }
1599 }
1600
1601 fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1602 let mut result = Vec::new();
1603 let mut visited = std::collections::HashSet::new();
1604 self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
1605 result
1606 }
1607
1608 fn collect_interface_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;
1616 }
1617 let extends = {
1618 if let Some(iface) = self.interfaces.get(fqcn) {
1619 iface.extends.clone()
1620 } else {
1621 return;
1622 }
1623 };
1624 for e in extends {
1625 out.push(e.clone());
1626 self.collect_interface_ancestors_inner(&e, out, visited);
1627 }
1628 }
1629}
1630
1631pub struct CodebaseBuilder {
1643 cb: Codebase,
1644}
1645
1646impl CodebaseBuilder {
1647 pub fn new() -> Self {
1648 Self {
1649 cb: Codebase::new(),
1650 }
1651 }
1652
1653 pub fn add(&mut self, slice: crate::storage::StubSlice) {
1656 self.cb.inject_stub_slice(slice);
1657 }
1658
1659 pub fn finalize(self) -> Codebase {
1661 self.cb.finalize();
1662 self.cb
1663 }
1664
1665 pub fn codebase(&self) -> &Codebase {
1667 &self.cb
1668 }
1669}
1670
1671impl Default for CodebaseBuilder {
1672 fn default() -> Self {
1673 Self::new()
1674 }
1675}
1676
1677pub fn codebase_from_parts(parts: Vec<crate::storage::StubSlice>) -> Codebase {
1679 let mut b = CodebaseBuilder::new();
1680 for p in parts {
1681 b.add(p);
1682 }
1683 b.finalize()
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688 use super::*;
1689
1690 fn arc(s: &str) -> Arc<str> {
1691 Arc::from(s)
1692 }
1693
1694 #[test]
1695 fn method_referenced_at_groups_spans_by_file() {
1696 let cb = Codebase::new();
1697 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1698 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 10, 15);
1699 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 20, 25);
1700
1701 let locs = cb.get_reference_locations("Foo::bar");
1702 let files: std::collections::HashSet<&str> =
1703 locs.iter().map(|(f, _, _)| f.as_ref()).collect();
1704 assert_eq!(files.len(), 2, "two files, not three spans");
1705 assert!(locs.contains(&(arc("a.php"), 0, 5)));
1706 assert!(locs.contains(&(arc("a.php"), 10, 15)));
1707 assert_eq!(
1708 locs.iter()
1709 .filter(|(f, _, _)| f.as_ref() == "a.php")
1710 .count(),
1711 2
1712 );
1713 assert!(locs.contains(&(arc("b.php"), 20, 25)));
1714 assert!(
1715 cb.is_method_referenced("Foo", "bar"),
1716 "DashSet also updated"
1717 );
1718 }
1719
1720 #[test]
1721 fn duplicate_spans_are_deduplicated() {
1722 let cb = Codebase::new();
1723 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1725 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1726
1727 let count = cb
1728 .get_reference_locations("Foo::bar")
1729 .iter()
1730 .filter(|(f, _, _)| f.as_ref() == "a.php")
1731 .count();
1732 assert_eq!(count, 1, "duplicate span deduplicated");
1733 }
1734
1735 #[test]
1736 fn method_key_is_lowercased() {
1737 let cb = Codebase::new();
1738 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 0, 3);
1739 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
1740 }
1741
1742 #[test]
1743 fn property_referenced_at_records_location() {
1744 let cb = Codebase::new();
1745 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 5, 10);
1746
1747 assert!(cb
1748 .get_reference_locations("Bar::count")
1749 .contains(&(arc("x.php"), 5, 10)));
1750 assert!(cb.is_property_referenced("Bar", "count"));
1751 }
1752
1753 #[test]
1754 fn function_referenced_at_records_location() {
1755 let cb = Codebase::new();
1756 cb.mark_function_referenced_at("my_fn", arc("a.php"), 10, 15);
1757
1758 assert!(cb
1759 .get_reference_locations("my_fn")
1760 .contains(&(arc("a.php"), 10, 15)));
1761 assert!(cb.is_function_referenced("my_fn"));
1762 }
1763
1764 #[test]
1765 fn class_referenced_at_records_location() {
1766 let cb = Codebase::new();
1767 cb.mark_class_referenced_at("Foo", arc("a.php"), 5, 8);
1768
1769 assert!(cb
1770 .get_reference_locations("Foo")
1771 .contains(&(arc("a.php"), 5, 8)));
1772 }
1773
1774 #[test]
1775 fn get_reference_locations_flattens_all_files() {
1776 let cb = Codebase::new();
1777 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1778 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1779
1780 let mut locs = cb.get_reference_locations("fn1");
1781 locs.sort_by_key(|(_, s, _)| *s);
1782 assert_eq!(locs.len(), 2);
1783 assert_eq!(locs[0], (arc("a.php"), 0, 5));
1784 assert_eq!(locs[1], (arc("b.php"), 10, 15));
1785 }
1786
1787 #[test]
1788 fn replay_reference_locations_restores_index() {
1789 let cb = Codebase::new();
1790 let locs = vec![
1791 ("Foo::bar".to_string(), 0u32, 5u32),
1792 ("Foo::bar".to_string(), 10, 15),
1793 ("greet".to_string(), 20, 25),
1794 ];
1795 cb.replay_reference_locations(arc("a.php"), &locs);
1796
1797 let bar_locs = cb.get_reference_locations("Foo::bar");
1798 assert!(bar_locs.contains(&(arc("a.php"), 0, 5)));
1799 assert!(bar_locs.contains(&(arc("a.php"), 10, 15)));
1800
1801 assert!(cb
1802 .get_reference_locations("greet")
1803 .contains(&(arc("a.php"), 20, 25)));
1804
1805 assert!(cb.file_has_symbol_references("a.php"));
1806 }
1807
1808 #[test]
1809 fn remove_file_clears_its_spans_only() {
1810 let cb = Codebase::new();
1811 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1812 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1813
1814 cb.remove_file_definitions("a.php");
1815
1816 let locs = cb.get_reference_locations("fn1");
1817 assert!(
1818 !locs.iter().any(|(f, _, _)| f.as_ref() == "a.php"),
1819 "a.php spans removed"
1820 );
1821 assert!(
1822 locs.contains(&(arc("b.php"), 10, 15)),
1823 "b.php spans untouched"
1824 );
1825 assert!(!cb.file_has_symbol_references("a.php"));
1826 }
1827
1828 #[test]
1829 fn remove_file_does_not_affect_other_files() {
1830 let cb = Codebase::new();
1831 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 4);
1832 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 7, 10);
1833
1834 cb.remove_file_definitions("x.php");
1835
1836 let locs = cb.get_reference_locations("Cls::prop");
1837 assert!(!locs.iter().any(|(f, _, _)| f.as_ref() == "x.php"));
1838 assert!(locs.contains(&(arc("y.php"), 7, 10)));
1839 }
1840
1841 #[test]
1842 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1843 let cb = Codebase::new();
1844 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1845
1846 cb.remove_file_definitions("ghost.php");
1848
1849 assert!(cb
1851 .get_reference_locations("fn1")
1852 .contains(&(arc("a.php"), 0, 5)));
1853 assert!(!cb.file_has_symbol_references("ghost.php"));
1854 }
1855
1856 #[test]
1857 fn replay_reference_locations_with_empty_list_is_noop() {
1858 let cb = Codebase::new();
1859 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1860
1861 cb.replay_reference_locations(arc("b.php"), &[]);
1863
1864 assert!(
1865 !cb.file_has_symbol_references("b.php"),
1866 "empty replay must not create a file entry"
1867 );
1868 assert!(
1869 cb.get_reference_locations("fn1")
1870 .contains(&(arc("a.php"), 0, 5)),
1871 "existing spans untouched"
1872 );
1873 }
1874
1875 #[test]
1876 fn replay_reference_locations_twice_does_not_duplicate_spans() {
1877 let cb = Codebase::new();
1878 let locs = vec![("fn1".to_string(), 0u32, 5u32)];
1879
1880 cb.replay_reference_locations(arc("a.php"), &locs);
1881 cb.replay_reference_locations(arc("a.php"), &locs);
1882
1883 let count = cb
1884 .get_reference_locations("fn1")
1885 .iter()
1886 .filter(|(f, _, _)| f.as_ref() == "a.php")
1887 .count();
1888 assert_eq!(
1889 count, 1,
1890 "replaying the same location twice must not create duplicate spans"
1891 );
1892 }
1893
1894 fn make_fn(fqn: &str, short_name: &str) -> crate::storage::FunctionStorage {
1899 crate::storage::FunctionStorage {
1900 fqn: Arc::from(fqn),
1901 short_name: Arc::from(short_name),
1902 params: vec![],
1903 return_type: None,
1904 inferred_return_type: None,
1905 template_params: vec![],
1906 assertions: vec![],
1907 throws: vec![],
1908 deprecated: None,
1909 is_pure: false,
1910 location: None,
1911 }
1912 }
1913
1914 #[test]
1915 fn inject_stub_slice_later_injection_overwrites_earlier() {
1916 let cb = Codebase::new();
1917
1918 cb.inject_stub_slice(crate::storage::StubSlice {
1919 functions: vec![make_fn("strlen", "phpstorm_version")],
1920 file: Some(Arc::from("phpstorm/standard.php")),
1921 ..Default::default()
1922 });
1923 assert_eq!(
1924 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1925 "phpstorm_version"
1926 );
1927
1928 cb.inject_stub_slice(crate::storage::StubSlice {
1929 functions: vec![make_fn("strlen", "custom_version")],
1930 file: Some(Arc::from("stubs/standard/basic.php")),
1931 ..Default::default()
1932 });
1933
1934 assert_eq!(
1935 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1936 "custom_version",
1937 "custom stub must overwrite phpstorm stub"
1938 );
1939 assert_eq!(
1940 cb.symbol_to_file.get("strlen").unwrap().as_ref(),
1941 "stubs/standard/basic.php",
1942 "symbol_to_file must point to the overriding file"
1943 );
1944 }
1945
1946 #[test]
1947 fn inject_stub_slice_constants_not_added_to_symbol_to_file() {
1948 let cb = Codebase::new();
1949
1950 cb.inject_stub_slice(crate::storage::StubSlice {
1951 constants: vec![(Arc::from("PHP_EOL"), mir_types::Union::empty())],
1952 file: Some(Arc::from("stubs/core/constants.php")),
1953 ..Default::default()
1954 });
1955
1956 assert!(
1957 cb.constants.contains_key("PHP_EOL"),
1958 "constant must be registered in constants map"
1959 );
1960 assert!(
1961 !cb.symbol_to_file.contains_key("PHP_EOL"),
1962 "constants must not appear in symbol_to_file — go-to-definition is not supported for them"
1963 );
1964 }
1965
1966 #[test]
1967 fn remove_file_definitions_purges_injected_global_vars() {
1968 let cb = Codebase::new();
1969
1970 cb.inject_stub_slice(crate::storage::StubSlice {
1971 global_vars: vec![(Arc::from("db_connection"), mir_types::Union::empty())],
1972 file: Some(Arc::from("src/bootstrap.php")),
1973 ..Default::default()
1974 });
1975 assert!(
1976 cb.global_vars.contains_key("db_connection"),
1977 "global var must be registered after injection"
1978 );
1979
1980 cb.remove_file_definitions("src/bootstrap.php");
1981
1982 assert!(
1983 !cb.global_vars.contains_key("db_connection"),
1984 "global var must be removed when its defining file is removed"
1985 );
1986 }
1987
1988 #[test]
1989 fn inject_stub_slice_without_file_discards_global_vars() {
1990 let cb = Codebase::new();
1991
1992 cb.inject_stub_slice(crate::storage::StubSlice {
1993 global_vars: vec![(Arc::from("orphan_var"), mir_types::Union::empty())],
1994 file: None,
1995 ..Default::default()
1996 });
1997
1998 assert!(
1999 !cb.global_vars.contains_key("orphan_var"),
2000 "global_vars must not be registered when slice.file is None"
2001 );
2002 }
2003
2004 fn bare_class(fqcn: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2009 use indexmap::IndexMap;
2010 ClassStorage {
2011 fqcn: arc(fqcn),
2012 short_name: arc(fqcn),
2013 parent: None,
2014 interfaces: vec![],
2015 traits: vec![],
2016 own_methods: IndexMap::new(),
2017 own_properties: IndexMap::new(),
2018 own_constants: IndexMap::new(),
2019 mixins,
2020 template_params: vec![],
2021 extends_type_args: vec![],
2022 implements_type_args: vec![],
2023 is_abstract: false,
2024 is_final: false,
2025 is_readonly: false,
2026 all_parents: vec![],
2027 deprecated: None,
2028 is_internal: false,
2029 location: None,
2030 type_aliases: std::collections::HashMap::new(),
2031 pending_import_types: vec![],
2032 }
2033 }
2034
2035 fn class_with_method(fqcn: &str, method_name: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2036 use crate::storage::{MethodStorage, Visibility};
2037 use indexmap::IndexMap;
2038 let mut methods = IndexMap::new();
2039 methods.insert(
2040 arc(method_name),
2041 Arc::new(MethodStorage {
2042 name: arc(method_name),
2043 fqcn: arc(fqcn),
2044 params: vec![],
2045 return_type: None,
2046 inferred_return_type: None,
2047 visibility: Visibility::Public,
2048 is_static: false,
2049 is_abstract: false,
2050 is_final: false,
2051 is_constructor: false,
2052 template_params: vec![],
2053 assertions: vec![],
2054 throws: vec![],
2055 deprecated: None,
2056 is_internal: false,
2057 is_pure: false,
2058 location: None,
2059 }),
2060 );
2061 let mut cls = bare_class(fqcn, mixins);
2062 cls.own_methods = methods;
2063 cls
2064 }
2065
2066 fn class_with_property(fqcn: &str, prop_name: &str, mixins: Vec<Arc<str>>) -> ClassStorage {
2067 use crate::storage::{PropertyStorage, Visibility};
2068 use indexmap::IndexMap;
2069 let mut props = IndexMap::new();
2070 props.insert(
2071 arc(prop_name),
2072 PropertyStorage {
2073 name: arc(prop_name),
2074 ty: None,
2075 inferred_ty: None,
2076 visibility: Visibility::Public,
2077 is_static: false,
2078 is_readonly: false,
2079 default: None,
2080 location: None,
2081 },
2082 );
2083 let mut cls = bare_class(fqcn, mixins);
2084 cls.own_properties = props;
2085 cls
2086 }
2087
2088 #[test]
2089 fn get_method_two_way_mixin_cycle_returns_none() {
2090 let cb = Codebase::new();
2091 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2092 cb.classes.insert(arc("B"), bare_class("B", vec![arc("A")]));
2093 assert!(cb.get_method("A", "missing").is_none());
2094 }
2095
2096 #[test]
2097 fn get_method_self_mixin_returns_none() {
2098 let cb = Codebase::new();
2099 cb.classes.insert(arc("A"), bare_class("A", vec![arc("A")]));
2100 assert!(cb.get_method("A", "missing").is_none());
2101 }
2102
2103 #[test]
2104 fn get_method_three_way_cycle_returns_none() {
2105 let cb = Codebase::new();
2106 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2107 cb.classes.insert(arc("B"), bare_class("B", vec![arc("C")]));
2108 cb.classes.insert(arc("C"), bare_class("C", vec![arc("A")]));
2109 assert!(cb.get_method("A", "missing").is_none());
2110 }
2111
2112 #[test]
2113 fn get_method_resolves_through_mixin_when_no_cycle() {
2114 let cb = Codebase::new();
2115 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2116 cb.classes
2117 .insert(arc("B"), class_with_method("B", "fromB", vec![]));
2118 assert!(cb.get_method("A", "fromB").is_some());
2119 }
2120
2121 #[test]
2122 fn get_method_own_method_shadows_mixin() {
2123 let cb = Codebase::new();
2124 cb.classes
2125 .insert(arc("A"), class_with_method("A", "foo", vec![arc("B")]));
2126 cb.classes
2127 .insert(arc("B"), class_with_method("B", "foo", vec![]));
2128 let m = cb.get_method("A", "foo").unwrap();
2129 assert_eq!(m.fqcn.as_ref(), "A");
2130 }
2131
2132 #[test]
2133 fn get_method_mixin_nonexistent_class_returns_none() {
2134 let cb = Codebase::new();
2135 cb.classes
2136 .insert(arc("A"), bare_class("A", vec![arc("Ghost")]));
2137 assert!(cb.get_method("A", "foo").is_none());
2138 }
2139
2140 #[test]
2141 fn get_property_two_way_mixin_cycle_returns_none() {
2142 let cb = Codebase::new();
2143 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2144 cb.classes.insert(arc("B"), bare_class("B", vec![arc("A")]));
2145 assert!(cb.get_property("A", "missing").is_none());
2146 }
2147
2148 #[test]
2149 fn get_method_diamond_mixin_finds_method_via_first_path() {
2150 let cb = Codebase::new();
2154 cb.classes
2155 .insert(arc("A"), bare_class("A", vec![arc("B"), arc("C")]));
2156 cb.classes.insert(arc("B"), bare_class("B", vec![arc("D")]));
2157 cb.classes.insert(arc("C"), bare_class("C", vec![arc("D")]));
2158 cb.classes
2159 .insert(arc("D"), class_with_method("D", "foo", vec![]));
2160 assert!(cb.get_method("A", "foo").is_some());
2161 }
2162
2163 #[test]
2164 fn get_method_mixin_on_ancestor_is_followed() {
2165 let cb = Codebase::new();
2166 cb.classes.insert(
2167 arc("Child"),
2168 ClassStorage {
2169 all_parents: vec![arc("Parent")],
2170 ..bare_class("Child", vec![])
2171 },
2172 );
2173 cb.classes
2174 .insert(arc("Parent"), bare_class("Parent", vec![arc("Mixin")]));
2175 cb.classes.insert(
2176 arc("Mixin"),
2177 class_with_method("Mixin", "fromMixin", vec![]),
2178 );
2179 assert!(cb.get_method("Child", "fromMixin").is_some());
2180 assert!(cb.get_method("Parent", "fromMixin").is_some());
2181 }
2182
2183 #[test]
2184 fn get_method_mixin_on_transitive_ancestor_is_followed() {
2185 let cb = Codebase::new();
2186 cb.classes.insert(
2188 arc("A"),
2189 ClassStorage {
2190 all_parents: vec![arc("B"), arc("C")],
2191 ..bare_class("A", vec![])
2192 },
2193 );
2194 cb.classes.insert(
2195 arc("B"),
2196 ClassStorage {
2197 all_parents: vec![arc("C")],
2198 ..bare_class("B", vec![])
2199 },
2200 );
2201 cb.classes.insert(arc("C"), bare_class("C", vec![arc("D")]));
2202 cb.classes
2203 .insert(arc("D"), class_with_method("D", "foo", vec![]));
2204 assert!(cb.get_method("A", "foo").is_some());
2205 }
2206
2207 #[test]
2208 fn get_property_resolves_through_mixin_when_no_cycle() {
2209 let cb = Codebase::new();
2210 cb.classes.insert(arc("A"), bare_class("A", vec![arc("B")]));
2211 cb.classes
2212 .insert(arc("B"), class_with_property("B", "title", vec![]));
2213 assert!(cb.get_property("A", "title").is_some());
2214 }
2215}