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 for cls in slice.classes {
227 self.classes.insert(cls.fqcn.clone(), cls);
228 }
229 for iface in slice.interfaces {
230 self.interfaces.insert(iface.fqcn.clone(), iface);
231 }
232 for tr in slice.traits {
233 self.traits.insert(tr.fqcn.clone(), tr);
234 }
235 for en in slice.enums {
236 self.enums.insert(en.fqcn.clone(), en);
237 }
238 for func in slice.functions {
239 self.functions.insert(func.fqn.clone(), func);
240 }
241 for (name, ty) in slice.constants {
242 self.constants.insert(name, ty);
243 }
244 }
245
246 pub fn compact_reference_index(&self) {
263 let mut entries: Vec<(u32, u32, u32, u32)> = self
265 .symbol_reference_locations
266 .iter()
267 .flat_map(|entry| {
268 let sym_id = *entry.key();
269 entry
270 .value()
271 .iter()
272 .map(move |&(file_id, start, end)| (sym_id, file_id, start, end))
273 .collect::<Vec<_>>()
274 })
275 .collect();
276
277 if entries.is_empty() {
278 return;
279 }
280
281 entries.sort_unstable();
283 entries.dedup();
284
285 let n = entries.len();
286
287 let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
289 let mut sym_offsets = vec![0u32; max_sym + 2];
290 for &(sym_id, ..) in &entries {
291 sym_offsets[sym_id as usize + 1] += 1;
292 }
293 for i in 1..sym_offsets.len() {
294 sym_offsets[i] += sym_offsets[i - 1];
295 }
296
297 let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
301 let mut by_file: Vec<u32> = (0..n as u32).collect();
302 by_file.sort_unstable_by_key(|&i| {
303 let (sym_id, file_id, start, end) = entries[i as usize];
304 (file_id, sym_id, start, end)
305 });
306
307 let mut file_offsets = vec![0u32; max_file + 2];
308 for &idx in &by_file {
309 let file_id = entries[idx as usize].1;
310 file_offsets[file_id as usize + 1] += 1;
311 }
312 for i in 1..file_offsets.len() {
313 file_offsets[i] += file_offsets[i - 1];
314 }
315
316 *self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
317 entries,
318 sym_offsets,
319 by_file,
320 file_offsets,
321 });
322 self.is_compacted
323 .store(true, std::sync::atomic::Ordering::Release);
324
325 self.symbol_reference_locations.clear();
327 self.file_symbol_references.clear();
328 }
329
330 fn ensure_expanded(&self) {
336 if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
338 return;
339 }
340 let mut guard = self.compact_ref_index.write().unwrap();
342 if let Some(ci) = guard.take() {
343 for &(sym_id, file_id, start, end) in &ci.entries {
344 record_ref(
345 &self.symbol_reference_locations,
346 &self.file_symbol_references,
347 sym_id,
348 file_id,
349 start,
350 end,
351 );
352 }
353 self.is_compacted
354 .store(false, std::sync::atomic::Ordering::Release);
355 }
356 }
358
359 pub fn invalidate_finalization(&self) {
365 self.finalized
366 .store(false, std::sync::atomic::Ordering::SeqCst);
367 }
368
369 pub fn remove_file_definitions(&self, file_path: &str) {
380 let symbols: Vec<Arc<str>> = self
382 .symbol_to_file
383 .iter()
384 .filter(|entry| entry.value().as_ref() == file_path)
385 .map(|entry| entry.key().clone())
386 .collect();
387
388 for sym in &symbols {
390 self.classes.remove(sym.as_ref());
391 self.interfaces.remove(sym.as_ref());
392 self.traits.remove(sym.as_ref());
393 self.enums.remove(sym.as_ref());
394 self.functions.remove(sym.as_ref());
395 self.constants.remove(sym.as_ref());
396 self.symbol_to_file.remove(sym.as_ref());
397 self.known_symbols.remove(sym.as_ref());
398 }
399
400 self.file_imports.remove(file_path);
402 self.file_namespaces.remove(file_path);
403
404 if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
406 for name in var_names {
407 self.global_vars.remove(name.as_ref());
408 }
409 }
410
411 self.ensure_expanded();
413
414 if let Some(file_id) = self.file_interner.get_id(file_path) {
417 if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
418 for sym_id in sym_ids {
419 if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
420 entries.retain(|&(fid, _, _)| fid != file_id);
421 }
422 }
423 }
424 }
425
426 self.invalidate_finalization();
427 }
428
429 pub fn file_structural_snapshot(&self, file_path: &str) -> StructuralSnapshot {
441 let symbols: Vec<Arc<str>> = self
442 .symbol_to_file
443 .iter()
444 .filter(|e| e.value().as_ref() == file_path)
445 .map(|e| e.key().clone())
446 .collect();
447
448 let mut classes = std::collections::HashMap::new();
449 let mut interfaces = std::collections::HashMap::new();
450
451 for sym in symbols {
452 if let Some(cls) = self.classes.get(sym.as_ref()) {
453 let mut ifaces = cls.interfaces.clone();
454 ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
455 let mut traits = cls.traits.clone();
456 traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
457 classes.insert(
458 sym,
459 ClassInheritance {
460 parent: cls.parent.clone(),
461 interfaces: ifaces,
462 traits,
463 all_parents: cls.all_parents.clone(),
464 },
465 );
466 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
467 let mut extends = iface.extends.clone();
468 extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
469 interfaces.insert(
470 sym,
471 InterfaceInheritance {
472 extends,
473 all_parents: iface.all_parents.clone(),
474 },
475 );
476 }
477 }
478
479 StructuralSnapshot {
480 classes,
481 interfaces,
482 }
483 }
484
485 pub fn structural_unchanged_after_pass1(
491 &self,
492 file_path: &str,
493 old: &StructuralSnapshot,
494 ) -> bool {
495 let symbols: Vec<Arc<str>> = self
496 .symbol_to_file
497 .iter()
498 .filter(|e| e.value().as_ref() == file_path)
499 .map(|e| e.key().clone())
500 .collect();
501
502 let mut seen_classes = 0usize;
503 let mut seen_interfaces = 0usize;
504
505 for sym in &symbols {
506 if let Some(cls) = self.classes.get(sym.as_ref()) {
507 seen_classes += 1;
508 let Some(old_cls) = old.classes.get(sym.as_ref()) else {
509 return false; };
511 if old_cls.parent != cls.parent {
512 return false;
513 }
514 let mut new_ifaces = cls.interfaces.clone();
515 new_ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
516 if old_cls.interfaces != new_ifaces {
517 return false;
518 }
519 let mut new_traits = cls.traits.clone();
520 new_traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
521 if old_cls.traits != new_traits {
522 return false;
523 }
524 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
525 seen_interfaces += 1;
526 let Some(old_iface) = old.interfaces.get(sym.as_ref()) else {
527 return false; };
529 let mut new_extends = iface.extends.clone();
530 new_extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
531 if old_iface.extends != new_extends {
532 return false;
533 }
534 }
535 }
537
538 seen_classes == old.classes.len() && seen_interfaces == old.interfaces.len()
540 }
541
542 pub fn restore_all_parents(&self, file_path: &str, snapshot: &StructuralSnapshot) {
549 let symbols: Vec<Arc<str>> = self
550 .symbol_to_file
551 .iter()
552 .filter(|e| e.value().as_ref() == file_path)
553 .map(|e| e.key().clone())
554 .collect();
555
556 for sym in &symbols {
557 if let Some(old_cls) = snapshot.classes.get(sym.as_ref()) {
558 if let Some(mut cls) = self.classes.get_mut(sym.as_ref()) {
559 cls.all_parents = old_cls.all_parents.clone();
560 }
561 } else if let Some(old_iface) = snapshot.interfaces.get(sym.as_ref()) {
562 if let Some(mut iface) = self.interfaces.get_mut(sym.as_ref()) {
563 iface.all_parents = old_iface.all_parents.clone();
564 }
565 }
566 }
567
568 self.finalized
569 .store(true, std::sync::atomic::Ordering::SeqCst);
570 }
571
572 pub fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
579 self.file_global_vars
580 .entry(file.clone())
581 .or_default()
582 .push(name.clone());
583 self.global_vars.insert(name, ty);
584 }
585
586 pub fn get_property(
592 &self,
593 fqcn: &str,
594 prop_name: &str,
595 ) -> Option<crate::storage::PropertyStorage> {
596 if let Some(cls) = self.classes.get(fqcn) {
598 if let Some(p) = cls.own_properties.get(prop_name) {
599 return Some(p.clone());
600 }
601 }
602
603 let all_parents = {
605 if let Some(cls) = self.classes.get(fqcn) {
606 cls.all_parents.clone()
607 } else {
608 return None;
609 }
610 };
611
612 for ancestor_fqcn in &all_parents {
613 if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
614 if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
615 return Some(p.clone());
616 }
617 }
618 }
619
620 let trait_list = {
622 if let Some(cls) = self.classes.get(fqcn) {
623 cls.traits.clone()
624 } else {
625 vec![]
626 }
627 };
628 for trait_fqcn in &trait_list {
629 if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
630 if let Some(p) = tr.own_properties.get(prop_name) {
631 return Some(p.clone());
632 }
633 }
634 }
635
636 None
637 }
638
639 pub fn get_class_constant(
641 &self,
642 fqcn: &str,
643 const_name: &str,
644 ) -> Option<crate::storage::ConstantStorage> {
645 if let Some(cls) = self.classes.get(fqcn) {
647 if let Some(c) = cls.own_constants.get(const_name) {
648 return Some(c.clone());
649 }
650 let all_parents = cls.all_parents.clone();
651 let interfaces = cls.interfaces.clone();
652 let traits = cls.traits.clone();
653 drop(cls);
654
655 for tr_fqcn in &traits {
656 if let Some(tr) = self.traits.get(tr_fqcn.as_ref()) {
657 if let Some(c) = tr.own_constants.get(const_name) {
658 return Some(c.clone());
659 }
660 }
661 }
662
663 for ancestor_fqcn in &all_parents {
664 if let Some(ancestor) = self.classes.get(ancestor_fqcn.as_ref()) {
665 if let Some(c) = ancestor.own_constants.get(const_name) {
666 return Some(c.clone());
667 }
668 }
669 if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
670 if let Some(c) = iface.own_constants.get(const_name) {
671 return Some(c.clone());
672 }
673 }
674 }
675
676 for iface_fqcn in &interfaces {
677 if let Some(iface) = self.interfaces.get(iface_fqcn.as_ref()) {
678 if let Some(c) = iface.own_constants.get(const_name) {
679 return Some(c.clone());
680 }
681 }
682 }
683
684 return None;
685 }
686
687 if let Some(iface) = self.interfaces.get(fqcn) {
689 if let Some(c) = iface.own_constants.get(const_name) {
690 return Some(c.clone());
691 }
692 let parents = iface.all_parents.clone();
693 drop(iface);
694 for p in &parents {
695 if let Some(parent_iface) = self.interfaces.get(p.as_ref()) {
696 if let Some(c) = parent_iface.own_constants.get(const_name) {
697 return Some(c.clone());
698 }
699 }
700 }
701 return None;
702 }
703
704 if let Some(en) = self.enums.get(fqcn) {
706 if let Some(c) = en.own_constants.get(const_name) {
707 return Some(c.clone());
708 }
709 if en.cases.contains_key(const_name) {
710 return Some(crate::storage::ConstantStorage {
711 name: Arc::from(const_name),
712 ty: mir_types::Union::mixed(),
713 visibility: None,
714 location: None,
715 });
716 }
717 return None;
718 }
719
720 if let Some(tr) = self.traits.get(fqcn) {
722 if let Some(c) = tr.own_constants.get(const_name) {
723 return Some(c.clone());
724 }
725 return None;
726 }
727
728 None
729 }
730
731 pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
733 let method_lower = method_name.to_lowercase();
735 let method_name = method_lower.as_str();
736
737 if let Some(cls) = self.classes.get(fqcn) {
739 if let Some(m) = lookup_method(&cls.own_methods, method_name) {
741 return Some(Arc::clone(m));
742 }
743 let own_traits = cls.traits.clone();
745 let ancestors = cls.all_parents.clone();
746 drop(cls);
747
748 for tr_fqcn in &own_traits {
750 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
751 return Some(m);
752 }
753 }
754
755 for ancestor_fqcn in &ancestors {
757 if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
758 if let Some(m) = lookup_method(&anc.own_methods, method_name) {
759 return Some(Arc::clone(m));
760 }
761 let anc_traits = anc.traits.clone();
762 drop(anc);
763 for tr_fqcn in &anc_traits {
764 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
765 return Some(m);
766 }
767 }
768 } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
769 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
770 let mut ms = (**m).clone();
771 ms.is_abstract = true;
772 return Some(Arc::new(ms));
773 }
774 }
775 }
777 return None;
778 }
779
780 if let Some(iface) = self.interfaces.get(fqcn) {
782 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
783 return Some(Arc::clone(m));
784 }
785 let parents = iface.all_parents.clone();
786 drop(iface);
787 for parent_fqcn in &parents {
788 if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
789 if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
790 return Some(Arc::clone(m));
791 }
792 }
793 }
794 return None;
795 }
796
797 if let Some(tr) = self.traits.get(fqcn) {
799 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
800 return Some(Arc::clone(m));
801 }
802 return None;
803 }
804
805 if let Some(e) = self.enums.get(fqcn) {
807 if let Some(m) = lookup_method(&e.own_methods, method_name) {
808 return Some(Arc::clone(m));
809 }
810 if matches!(method_name, "cases" | "from" | "tryfrom") {
812 return Some(Arc::new(crate::storage::MethodStorage {
813 fqcn: Arc::from(fqcn),
814 name: Arc::from(method_name),
815 params: vec![],
816 return_type: Some(mir_types::Union::mixed()),
817 inferred_return_type: None,
818 visibility: crate::storage::Visibility::Public,
819 is_static: true,
820 is_abstract: false,
821 is_constructor: false,
822 template_params: vec![],
823 assertions: vec![],
824 throws: vec![],
825 is_final: false,
826 is_internal: false,
827 is_pure: false,
828 is_deprecated: false,
829 location: None,
830 }));
831 }
832 }
833
834 None
835 }
836
837 pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
839 if child == ancestor {
840 return true;
841 }
842 if let Some(cls) = self.classes.get(child) {
843 return cls.implements_or_extends(ancestor);
844 }
845 if let Some(iface) = self.interfaces.get(child) {
846 return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
847 }
848 if let Some(en) = self.enums.get(child) {
851 if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
853 return true;
854 }
855 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
857 return true;
858 }
859 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
861 {
862 return true;
863 }
864 }
865 false
866 }
867
868 pub fn type_exists(&self, fqcn: &str) -> bool {
870 self.classes.contains_key(fqcn)
871 || self.interfaces.contains_key(fqcn)
872 || self.traits.contains_key(fqcn)
873 || self.enums.contains_key(fqcn)
874 }
875
876 pub fn function_exists(&self, fqn: &str) -> bool {
877 self.functions.contains_key(fqn)
878 }
879
880 pub fn is_abstract_class(&self, fqcn: &str) -> bool {
884 self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
885 }
886
887 pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
890 if let Some(cls) = self.classes.get(fqcn) {
891 return cls.template_params.clone();
892 }
893 if let Some(iface) = self.interfaces.get(fqcn) {
894 return iface.template_params.clone();
895 }
896 if let Some(tr) = self.traits.get(fqcn) {
897 return tr.template_params.clone();
898 }
899 vec![]
900 }
901
902 pub fn get_inherited_template_bindings(
907 &self,
908 fqcn: &str,
909 ) -> std::collections::HashMap<Arc<str>, Union> {
910 let mut bindings = std::collections::HashMap::new();
911 let mut current = fqcn.to_string();
912
913 loop {
914 let (parent_fqcn, extends_type_args) = {
915 let cls = match self.classes.get(current.as_str()) {
916 Some(c) => c,
917 None => break,
918 };
919 let parent = match &cls.parent {
920 Some(p) => p.clone(),
921 None => break,
922 };
923 let args = cls.extends_type_args.clone();
924 (parent, args)
925 };
926
927 if !extends_type_args.is_empty() {
928 let parent_tps = self.get_class_template_params(&parent_fqcn);
929 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
930 bindings
931 .entry(tp.name.clone())
932 .or_insert_with(|| ty.clone());
933 }
934 }
935
936 current = parent_fqcn.to_string();
937 }
938
939 bindings
940 }
941
942 pub fn has_magic_get(&self, fqcn: &str) -> bool {
945 self.get_method(fqcn, "__get").is_some()
946 }
947
948 pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
956 if let Some(iface) = self.interfaces.get(fqcn) {
958 let parents = iface.all_parents.clone();
959 drop(iface);
960 for p in &parents {
961 if !self.type_exists(p.as_ref()) {
962 return true;
963 }
964 }
965 return false;
966 }
967
968 let (parent, interfaces, traits, all_parents) = {
970 let Some(cls) = self.classes.get(fqcn) else {
971 return false;
972 };
973 (
974 cls.parent.clone(),
975 cls.interfaces.clone(),
976 cls.traits.clone(),
977 cls.all_parents.clone(),
978 )
979 };
980
981 if let Some(ref p) = parent {
983 if !self.type_exists(p.as_ref()) {
984 return true;
985 }
986 }
987 for iface in &interfaces {
988 if !self.type_exists(iface.as_ref()) {
989 return true;
990 }
991 }
992 for tr in &traits {
993 if !self.type_exists(tr.as_ref()) {
994 return true;
995 }
996 }
997
998 for ancestor in &all_parents {
1000 if !self.type_exists(ancestor.as_ref()) {
1001 return true;
1002 }
1003 }
1004
1005 false
1006 }
1007
1008 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
1015 let name = name.trim_start_matches('\\');
1016 if name.is_empty() {
1017 return name.to_string();
1018 }
1019 if name.contains('\\') {
1024 let first_segment = name.split('\\').next().unwrap_or(name);
1026 if let Some(imports) = self.file_imports.get(file) {
1027 if let Some(resolved_prefix) = imports.get(first_segment) {
1028 let rest = &name[first_segment.len()..]; return format!("{}{}", resolved_prefix, rest);
1030 }
1031 }
1032 if self.type_exists(name) {
1034 return name.to_string();
1035 }
1036 if let Some(ns) = self.file_namespaces.get(file) {
1038 let qualified = format!("{}\\{}", *ns, name);
1039 if self.type_exists(&qualified) {
1040 return qualified;
1041 }
1042 }
1043 return name.to_string();
1044 }
1045 match name {
1047 "self" | "parent" | "static" | "this" => return name.to_string(),
1048 _ => {}
1049 }
1050 if let Some(imports) = self.file_imports.get(file) {
1052 if let Some(resolved) = imports.get(name) {
1053 return resolved.clone();
1054 }
1055 let name_lower = name.to_lowercase();
1057 for (alias, resolved) in imports.iter() {
1058 if alias.to_lowercase() == name_lower {
1059 return resolved.clone();
1060 }
1061 }
1062 }
1063 if let Some(ns) = self.file_namespaces.get(file) {
1065 let qualified = format!("{}\\{}", *ns, name);
1066 if self.type_exists(&qualified) {
1071 return qualified;
1072 }
1073 if self.type_exists(name) {
1074 return name.to_string();
1075 }
1076 return qualified;
1077 }
1078 name.to_string()
1079 }
1080
1081 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
1088 if let Some(cls) = self.classes.get(fqcn) {
1089 return cls.location.clone();
1090 }
1091 if let Some(iface) = self.interfaces.get(fqcn) {
1092 return iface.location.clone();
1093 }
1094 if let Some(tr) = self.traits.get(fqcn) {
1095 return tr.location.clone();
1096 }
1097 if let Some(en) = self.enums.get(fqcn) {
1098 return en.location.clone();
1099 }
1100 if let Some(func) = self.functions.get(fqcn) {
1101 return func.location.clone();
1102 }
1103 None
1104 }
1105
1106 pub fn get_member_location(
1108 &self,
1109 fqcn: &str,
1110 member_name: &str,
1111 ) -> Option<crate::storage::Location> {
1112 if let Some(method) = self.get_method(fqcn, member_name) {
1114 return method.location.clone();
1115 }
1116 if let Some(prop) = self.get_property(fqcn, member_name) {
1118 return prop.location.clone();
1119 }
1120 if let Some(cls) = self.classes.get(fqcn) {
1122 if let Some(c) = cls.own_constants.get(member_name) {
1123 return c.location.clone();
1124 }
1125 }
1126 if let Some(iface) = self.interfaces.get(fqcn) {
1128 if let Some(c) = iface.own_constants.get(member_name) {
1129 return c.location.clone();
1130 }
1131 }
1132 if let Some(tr) = self.traits.get(fqcn) {
1134 if let Some(c) = tr.own_constants.get(member_name) {
1135 return c.location.clone();
1136 }
1137 }
1138 if let Some(en) = self.enums.get(fqcn) {
1140 if let Some(c) = en.own_constants.get(member_name) {
1141 return c.location.clone();
1142 }
1143 if let Some(case) = en.cases.get(member_name) {
1144 return case.location.clone();
1145 }
1146 }
1147 None
1148 }
1149
1150 pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
1156 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1157 let id = self.symbol_interner.intern_str(&key);
1158 self.referenced_methods.insert(id);
1159 }
1160
1161 pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
1163 let key = format!("{}::{}", fqcn, prop_name);
1164 let id = self.symbol_interner.intern_str(&key);
1165 self.referenced_properties.insert(id);
1166 }
1167
1168 pub fn mark_function_referenced(&self, fqn: &str) {
1170 let id = self.symbol_interner.intern_str(fqn);
1171 self.referenced_functions.insert(id);
1172 }
1173
1174 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
1175 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1176 match self.symbol_interner.get_id(&key) {
1177 Some(id) => self.referenced_methods.contains(&id),
1178 None => false,
1179 }
1180 }
1181
1182 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
1183 let key = format!("{}::{}", fqcn, prop_name);
1184 match self.symbol_interner.get_id(&key) {
1185 Some(id) => self.referenced_properties.contains(&id),
1186 None => false,
1187 }
1188 }
1189
1190 pub fn is_function_referenced(&self, fqn: &str) -> bool {
1191 match self.symbol_interner.get_id(fqn) {
1192 Some(id) => self.referenced_functions.contains(&id),
1193 None => false,
1194 }
1195 }
1196
1197 pub fn mark_method_referenced_at(
1200 &self,
1201 fqcn: &str,
1202 method_name: &str,
1203 file: Arc<str>,
1204 start: u32,
1205 end: u32,
1206 ) {
1207 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1208 self.ensure_expanded();
1209 let sym_id = self.symbol_interner.intern_str(&key);
1210 let file_id = self.file_interner.intern(file);
1211 self.referenced_methods.insert(sym_id);
1212 record_ref(
1213 &self.symbol_reference_locations,
1214 &self.file_symbol_references,
1215 sym_id,
1216 file_id,
1217 start,
1218 end,
1219 );
1220 }
1221
1222 pub fn mark_property_referenced_at(
1225 &self,
1226 fqcn: &str,
1227 prop_name: &str,
1228 file: Arc<str>,
1229 start: u32,
1230 end: u32,
1231 ) {
1232 let key = format!("{}::{}", fqcn, prop_name);
1233 self.ensure_expanded();
1234 let sym_id = self.symbol_interner.intern_str(&key);
1235 let file_id = self.file_interner.intern(file);
1236 self.referenced_properties.insert(sym_id);
1237 record_ref(
1238 &self.symbol_reference_locations,
1239 &self.file_symbol_references,
1240 sym_id,
1241 file_id,
1242 start,
1243 end,
1244 );
1245 }
1246
1247 pub fn mark_function_referenced_at(&self, fqn: &str, file: Arc<str>, start: u32, end: u32) {
1250 self.ensure_expanded();
1251 let sym_id = self.symbol_interner.intern_str(fqn);
1252 let file_id = self.file_interner.intern(file);
1253 self.referenced_functions.insert(sym_id);
1254 record_ref(
1255 &self.symbol_reference_locations,
1256 &self.file_symbol_references,
1257 sym_id,
1258 file_id,
1259 start,
1260 end,
1261 );
1262 }
1263
1264 pub fn mark_class_referenced_at(&self, fqcn: &str, file: Arc<str>, start: u32, end: u32) {
1268 self.ensure_expanded();
1269 let sym_id = self.symbol_interner.intern_str(fqcn);
1270 let file_id = self.file_interner.intern(file);
1271 record_ref(
1272 &self.symbol_reference_locations,
1273 &self.file_symbol_references,
1274 sym_id,
1275 file_id,
1276 start,
1277 end,
1278 );
1279 }
1280
1281 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u32)]) {
1285 if locs.is_empty() {
1286 return;
1287 }
1288 self.ensure_expanded();
1289 let file_id = self.file_interner.intern(file);
1290 for (symbol_key, start, end) in locs {
1291 let sym_id = self.symbol_interner.intern_str(symbol_key);
1292 record_ref(
1293 &self.symbol_reference_locations,
1294 &self.file_symbol_references,
1295 sym_id,
1296 file_id,
1297 *start,
1298 *end,
1299 );
1300 }
1301 }
1302
1303 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u32)> {
1306 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
1307 return Vec::new();
1308 };
1309 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1311 let id = sym_id as usize;
1312 if id + 1 >= ci.sym_offsets.len() {
1313 return Vec::new();
1314 }
1315 let start = ci.sym_offsets[id] as usize;
1316 let end = ci.sym_offsets[id + 1] as usize;
1317 return ci.entries[start..end]
1318 .iter()
1319 .map(|&(_, file_id, s, e)| (self.file_interner.get(file_id), s, e))
1320 .collect();
1321 }
1322 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1324 return Vec::new();
1325 };
1326 entries
1327 .iter()
1328 .map(|&(file_id, start, end)| (self.file_interner.get(file_id), start, end))
1329 .collect()
1330 }
1331
1332 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u32)> {
1335 let Some(file_id) = self.file_interner.get_id(file) else {
1336 return Vec::new();
1337 };
1338 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1340 let id = file_id as usize;
1341 if id + 1 >= ci.file_offsets.len() {
1342 return Vec::new();
1343 }
1344 let start = ci.file_offsets[id] as usize;
1345 let end = ci.file_offsets[id + 1] as usize;
1346 return ci.by_file[start..end]
1347 .iter()
1348 .map(|&entry_idx| {
1349 let (sym_id, _, s, e) = ci.entries[entry_idx as usize];
1350 (self.symbol_interner.get(sym_id), s, e)
1351 })
1352 .collect();
1353 }
1354 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
1356 return Vec::new();
1357 };
1358 let mut out = Vec::new();
1359 for &sym_id in sym_ids.iter() {
1360 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1361 continue;
1362 };
1363 let sym_key = self.symbol_interner.get(sym_id);
1364 for &(entry_file_id, start, end) in entries.iter() {
1365 if entry_file_id == file_id {
1366 out.push((sym_key.clone(), start, end));
1367 }
1368 }
1369 }
1370 out
1371 }
1372
1373 pub fn file_has_symbol_references(&self, file: &str) -> bool {
1375 let Some(file_id) = self.file_interner.get_id(file) else {
1376 return false;
1377 };
1378 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1380 let id = file_id as usize;
1381 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
1382 }
1383 self.file_symbol_references.contains_key(&file_id)
1384 }
1385
1386 pub fn finalize(&self) {
1393 if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
1394 return;
1395 }
1396
1397 let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
1399 for fqcn in &class_keys {
1400 let parents = self.collect_class_ancestors(fqcn);
1401 if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
1402 cls.all_parents = parents;
1403 }
1404 }
1405
1406 let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
1408 for fqcn in &iface_keys {
1409 let parents = self.collect_interface_ancestors(fqcn);
1410 if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
1411 iface.all_parents = parents;
1412 }
1413 }
1414
1415 self.finalized
1416 .store(true, std::sync::atomic::Ordering::SeqCst);
1417 }
1418
1419 fn get_method_in_trait(
1427 &self,
1428 tr_fqcn: &Arc<str>,
1429 method_name: &str,
1430 ) -> Option<Arc<MethodStorage>> {
1431 let mut visited = std::collections::HashSet::new();
1432 self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
1433 }
1434
1435 fn get_method_in_trait_inner(
1436 &self,
1437 tr_fqcn: &Arc<str>,
1438 method_name: &str,
1439 visited: &mut std::collections::HashSet<String>,
1440 ) -> Option<Arc<MethodStorage>> {
1441 if !visited.insert(tr_fqcn.to_string()) {
1442 return None; }
1444 let tr = self.traits.get(tr_fqcn.as_ref())?;
1445 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
1446 return Some(Arc::clone(m));
1447 }
1448 let used_traits = tr.traits.clone();
1449 drop(tr);
1450 for used_fqcn in &used_traits {
1451 if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
1452 return Some(m);
1453 }
1454 }
1455 None
1456 }
1457
1458 fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1459 let mut result = Vec::new();
1460 let mut visited = std::collections::HashSet::new();
1461 self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
1462 result
1463 }
1464
1465 fn collect_class_ancestors_inner(
1466 &self,
1467 fqcn: &str,
1468 out: &mut Vec<Arc<str>>,
1469 visited: &mut std::collections::HashSet<String>,
1470 ) {
1471 if !visited.insert(fqcn.to_string()) {
1472 return; }
1474 let (parent, interfaces, traits) = {
1475 if let Some(cls) = self.classes.get(fqcn) {
1476 (
1477 cls.parent.clone(),
1478 cls.interfaces.clone(),
1479 cls.traits.clone(),
1480 )
1481 } else {
1482 return;
1483 }
1484 };
1485
1486 if let Some(p) = parent {
1487 out.push(p.clone());
1488 self.collect_class_ancestors_inner(&p, out, visited);
1489 }
1490 for iface in interfaces {
1491 out.push(iface.clone());
1492 self.collect_interface_ancestors_inner(&iface, out, visited);
1493 }
1494 for t in traits {
1495 out.push(t);
1496 }
1497 }
1498
1499 fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1500 let mut result = Vec::new();
1501 let mut visited = std::collections::HashSet::new();
1502 self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
1503 result
1504 }
1505
1506 fn collect_interface_ancestors_inner(
1507 &self,
1508 fqcn: &str,
1509 out: &mut Vec<Arc<str>>,
1510 visited: &mut std::collections::HashSet<String>,
1511 ) {
1512 if !visited.insert(fqcn.to_string()) {
1513 return;
1514 }
1515 let extends = {
1516 if let Some(iface) = self.interfaces.get(fqcn) {
1517 iface.extends.clone()
1518 } else {
1519 return;
1520 }
1521 };
1522 for e in extends {
1523 out.push(e.clone());
1524 self.collect_interface_ancestors_inner(&e, out, visited);
1525 }
1526 }
1527}
1528
1529#[cfg(test)]
1530mod tests {
1531 use super::*;
1532
1533 fn arc(s: &str) -> Arc<str> {
1534 Arc::from(s)
1535 }
1536
1537 #[test]
1538 fn method_referenced_at_groups_spans_by_file() {
1539 let cb = Codebase::new();
1540 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1541 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 10, 15);
1542 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 20, 25);
1543
1544 let locs = cb.get_reference_locations("Foo::bar");
1545 let files: std::collections::HashSet<&str> =
1546 locs.iter().map(|(f, _, _)| f.as_ref()).collect();
1547 assert_eq!(files.len(), 2, "two files, not three spans");
1548 assert!(locs.contains(&(arc("a.php"), 0, 5)));
1549 assert!(locs.contains(&(arc("a.php"), 10, 15)));
1550 assert_eq!(
1551 locs.iter()
1552 .filter(|(f, _, _)| f.as_ref() == "a.php")
1553 .count(),
1554 2
1555 );
1556 assert!(locs.contains(&(arc("b.php"), 20, 25)));
1557 assert!(
1558 cb.is_method_referenced("Foo", "bar"),
1559 "DashSet also updated"
1560 );
1561 }
1562
1563 #[test]
1564 fn duplicate_spans_are_deduplicated() {
1565 let cb = Codebase::new();
1566 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1568 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1569
1570 let count = cb
1571 .get_reference_locations("Foo::bar")
1572 .iter()
1573 .filter(|(f, _, _)| f.as_ref() == "a.php")
1574 .count();
1575 assert_eq!(count, 1, "duplicate span deduplicated");
1576 }
1577
1578 #[test]
1579 fn method_key_is_lowercased() {
1580 let cb = Codebase::new();
1581 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 0, 3);
1582 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
1583 }
1584
1585 #[test]
1586 fn property_referenced_at_records_location() {
1587 let cb = Codebase::new();
1588 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 5, 10);
1589
1590 assert!(cb
1591 .get_reference_locations("Bar::count")
1592 .contains(&(arc("x.php"), 5, 10)));
1593 assert!(cb.is_property_referenced("Bar", "count"));
1594 }
1595
1596 #[test]
1597 fn function_referenced_at_records_location() {
1598 let cb = Codebase::new();
1599 cb.mark_function_referenced_at("my_fn", arc("a.php"), 10, 15);
1600
1601 assert!(cb
1602 .get_reference_locations("my_fn")
1603 .contains(&(arc("a.php"), 10, 15)));
1604 assert!(cb.is_function_referenced("my_fn"));
1605 }
1606
1607 #[test]
1608 fn class_referenced_at_records_location() {
1609 let cb = Codebase::new();
1610 cb.mark_class_referenced_at("Foo", arc("a.php"), 5, 8);
1611
1612 assert!(cb
1613 .get_reference_locations("Foo")
1614 .contains(&(arc("a.php"), 5, 8)));
1615 }
1616
1617 #[test]
1618 fn get_reference_locations_flattens_all_files() {
1619 let cb = Codebase::new();
1620 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1621 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1622
1623 let mut locs = cb.get_reference_locations("fn1");
1624 locs.sort_by_key(|(_, s, _)| *s);
1625 assert_eq!(locs.len(), 2);
1626 assert_eq!(locs[0], (arc("a.php"), 0, 5));
1627 assert_eq!(locs[1], (arc("b.php"), 10, 15));
1628 }
1629
1630 #[test]
1631 fn replay_reference_locations_restores_index() {
1632 let cb = Codebase::new();
1633 let locs = vec![
1634 ("Foo::bar".to_string(), 0u32, 5u32),
1635 ("Foo::bar".to_string(), 10, 15),
1636 ("greet".to_string(), 20, 25),
1637 ];
1638 cb.replay_reference_locations(arc("a.php"), &locs);
1639
1640 let bar_locs = cb.get_reference_locations("Foo::bar");
1641 assert!(bar_locs.contains(&(arc("a.php"), 0, 5)));
1642 assert!(bar_locs.contains(&(arc("a.php"), 10, 15)));
1643
1644 assert!(cb
1645 .get_reference_locations("greet")
1646 .contains(&(arc("a.php"), 20, 25)));
1647
1648 assert!(cb.file_has_symbol_references("a.php"));
1649 }
1650
1651 #[test]
1652 fn remove_file_clears_its_spans_only() {
1653 let cb = Codebase::new();
1654 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1655 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1656
1657 cb.remove_file_definitions("a.php");
1658
1659 let locs = cb.get_reference_locations("fn1");
1660 assert!(
1661 !locs.iter().any(|(f, _, _)| f.as_ref() == "a.php"),
1662 "a.php spans removed"
1663 );
1664 assert!(
1665 locs.contains(&(arc("b.php"), 10, 15)),
1666 "b.php spans untouched"
1667 );
1668 assert!(!cb.file_has_symbol_references("a.php"));
1669 }
1670
1671 #[test]
1672 fn remove_file_does_not_affect_other_files() {
1673 let cb = Codebase::new();
1674 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 4);
1675 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 7, 10);
1676
1677 cb.remove_file_definitions("x.php");
1678
1679 let locs = cb.get_reference_locations("Cls::prop");
1680 assert!(!locs.iter().any(|(f, _, _)| f.as_ref() == "x.php"));
1681 assert!(locs.contains(&(arc("y.php"), 7, 10)));
1682 }
1683
1684 #[test]
1685 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1686 let cb = Codebase::new();
1687 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1688
1689 cb.remove_file_definitions("ghost.php");
1691
1692 assert!(cb
1694 .get_reference_locations("fn1")
1695 .contains(&(arc("a.php"), 0, 5)));
1696 assert!(!cb.file_has_symbol_references("ghost.php"));
1697 }
1698
1699 #[test]
1700 fn replay_reference_locations_with_empty_list_is_noop() {
1701 let cb = Codebase::new();
1702 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1703
1704 cb.replay_reference_locations(arc("b.php"), &[]);
1706
1707 assert!(
1708 !cb.file_has_symbol_references("b.php"),
1709 "empty replay must not create a file entry"
1710 );
1711 assert!(
1712 cb.get_reference_locations("fn1")
1713 .contains(&(arc("a.php"), 0, 5)),
1714 "existing spans untouched"
1715 );
1716 }
1717
1718 #[test]
1719 fn replay_reference_locations_twice_does_not_duplicate_spans() {
1720 let cb = Codebase::new();
1721 let locs = vec![("fn1".to_string(), 0u32, 5u32)];
1722
1723 cb.replay_reference_locations(arc("a.php"), &locs);
1724 cb.replay_reference_locations(arc("a.php"), &locs);
1725
1726 let count = cb
1727 .get_reference_locations("fn1")
1728 .iter()
1729 .filter(|(f, _, _)| f.as_ref() == "a.php")
1730 .count();
1731 assert_eq!(
1732 count, 1,
1733 "replaying the same location twice must not create duplicate spans"
1734 );
1735 }
1736}