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 compact_reference_index(&self) {
233 let mut entries: Vec<(u32, u32, u32, u32)> = self
235 .symbol_reference_locations
236 .iter()
237 .flat_map(|entry| {
238 let sym_id = *entry.key();
239 entry
240 .value()
241 .iter()
242 .map(move |&(file_id, start, end)| (sym_id, file_id, start, end))
243 .collect::<Vec<_>>()
244 })
245 .collect();
246
247 if entries.is_empty() {
248 return;
249 }
250
251 entries.sort_unstable();
253 entries.dedup();
254
255 let n = entries.len();
256
257 let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
259 let mut sym_offsets = vec![0u32; max_sym + 2];
260 for &(sym_id, ..) in &entries {
261 sym_offsets[sym_id as usize + 1] += 1;
262 }
263 for i in 1..sym_offsets.len() {
264 sym_offsets[i] += sym_offsets[i - 1];
265 }
266
267 let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
271 let mut by_file: Vec<u32> = (0..n as u32).collect();
272 by_file.sort_unstable_by_key(|&i| {
273 let (sym_id, file_id, start, end) = entries[i as usize];
274 (file_id, sym_id, start, end)
275 });
276
277 let mut file_offsets = vec![0u32; max_file + 2];
278 for &idx in &by_file {
279 let file_id = entries[idx as usize].1;
280 file_offsets[file_id as usize + 1] += 1;
281 }
282 for i in 1..file_offsets.len() {
283 file_offsets[i] += file_offsets[i - 1];
284 }
285
286 *self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
287 entries,
288 sym_offsets,
289 by_file,
290 file_offsets,
291 });
292 self.is_compacted
293 .store(true, std::sync::atomic::Ordering::Release);
294
295 self.symbol_reference_locations.clear();
297 self.file_symbol_references.clear();
298 }
299
300 fn ensure_expanded(&self) {
306 if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
308 return;
309 }
310 let mut guard = self.compact_ref_index.write().unwrap();
312 if let Some(ci) = guard.take() {
313 for &(sym_id, file_id, start, end) in &ci.entries {
314 record_ref(
315 &self.symbol_reference_locations,
316 &self.file_symbol_references,
317 sym_id,
318 file_id,
319 start,
320 end,
321 );
322 }
323 self.is_compacted
324 .store(false, std::sync::atomic::Ordering::Release);
325 }
326 }
328
329 pub fn invalidate_finalization(&self) {
335 self.finalized
336 .store(false, std::sync::atomic::Ordering::SeqCst);
337 }
338
339 pub fn remove_file_definitions(&self, file_path: &str) {
350 let symbols: Vec<Arc<str>> = self
352 .symbol_to_file
353 .iter()
354 .filter(|entry| entry.value().as_ref() == file_path)
355 .map(|entry| entry.key().clone())
356 .collect();
357
358 for sym in &symbols {
360 self.classes.remove(sym.as_ref());
361 self.interfaces.remove(sym.as_ref());
362 self.traits.remove(sym.as_ref());
363 self.enums.remove(sym.as_ref());
364 self.functions.remove(sym.as_ref());
365 self.constants.remove(sym.as_ref());
366 self.symbol_to_file.remove(sym.as_ref());
367 self.known_symbols.remove(sym.as_ref());
368 }
369
370 self.file_imports.remove(file_path);
372 self.file_namespaces.remove(file_path);
373
374 if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
376 for name in var_names {
377 self.global_vars.remove(name.as_ref());
378 }
379 }
380
381 self.ensure_expanded();
383
384 if let Some(file_id) = self.file_interner.get_id(file_path) {
387 if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
388 for sym_id in sym_ids {
389 if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
390 entries.retain(|&(fid, _, _)| fid != file_id);
391 }
392 }
393 }
394 }
395
396 self.invalidate_finalization();
397 }
398
399 pub fn file_structural_snapshot(&self, file_path: &str) -> StructuralSnapshot {
411 let symbols: Vec<Arc<str>> = self
412 .symbol_to_file
413 .iter()
414 .filter(|e| e.value().as_ref() == file_path)
415 .map(|e| e.key().clone())
416 .collect();
417
418 let mut classes = std::collections::HashMap::new();
419 let mut interfaces = std::collections::HashMap::new();
420
421 for sym in symbols {
422 if let Some(cls) = self.classes.get(sym.as_ref()) {
423 let mut ifaces = cls.interfaces.clone();
424 ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
425 let mut traits = cls.traits.clone();
426 traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
427 classes.insert(
428 sym,
429 ClassInheritance {
430 parent: cls.parent.clone(),
431 interfaces: ifaces,
432 traits,
433 all_parents: cls.all_parents.clone(),
434 },
435 );
436 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
437 let mut extends = iface.extends.clone();
438 extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
439 interfaces.insert(
440 sym,
441 InterfaceInheritance {
442 extends,
443 all_parents: iface.all_parents.clone(),
444 },
445 );
446 }
447 }
448
449 StructuralSnapshot {
450 classes,
451 interfaces,
452 }
453 }
454
455 pub fn structural_unchanged_after_pass1(
461 &self,
462 file_path: &str,
463 old: &StructuralSnapshot,
464 ) -> bool {
465 let symbols: Vec<Arc<str>> = self
466 .symbol_to_file
467 .iter()
468 .filter(|e| e.value().as_ref() == file_path)
469 .map(|e| e.key().clone())
470 .collect();
471
472 let mut seen_classes = 0usize;
473 let mut seen_interfaces = 0usize;
474
475 for sym in &symbols {
476 if let Some(cls) = self.classes.get(sym.as_ref()) {
477 seen_classes += 1;
478 let Some(old_cls) = old.classes.get(sym.as_ref()) else {
479 return false; };
481 if old_cls.parent != cls.parent {
482 return false;
483 }
484 let mut new_ifaces = cls.interfaces.clone();
485 new_ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
486 if old_cls.interfaces != new_ifaces {
487 return false;
488 }
489 let mut new_traits = cls.traits.clone();
490 new_traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
491 if old_cls.traits != new_traits {
492 return false;
493 }
494 } else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
495 seen_interfaces += 1;
496 let Some(old_iface) = old.interfaces.get(sym.as_ref()) else {
497 return false; };
499 let mut new_extends = iface.extends.clone();
500 new_extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
501 if old_iface.extends != new_extends {
502 return false;
503 }
504 }
505 }
507
508 seen_classes == old.classes.len() && seen_interfaces == old.interfaces.len()
510 }
511
512 pub fn restore_all_parents(&self, file_path: &str, snapshot: &StructuralSnapshot) {
519 let symbols: Vec<Arc<str>> = self
520 .symbol_to_file
521 .iter()
522 .filter(|e| e.value().as_ref() == file_path)
523 .map(|e| e.key().clone())
524 .collect();
525
526 for sym in &symbols {
527 if let Some(old_cls) = snapshot.classes.get(sym.as_ref()) {
528 if let Some(mut cls) = self.classes.get_mut(sym.as_ref()) {
529 cls.all_parents = old_cls.all_parents.clone();
530 }
531 } else if let Some(old_iface) = snapshot.interfaces.get(sym.as_ref()) {
532 if let Some(mut iface) = self.interfaces.get_mut(sym.as_ref()) {
533 iface.all_parents = old_iface.all_parents.clone();
534 }
535 }
536 }
537
538 self.finalized
539 .store(true, std::sync::atomic::Ordering::SeqCst);
540 }
541
542 pub fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
549 self.file_global_vars
550 .entry(file.clone())
551 .or_default()
552 .push(name.clone());
553 self.global_vars.insert(name, ty);
554 }
555
556 pub fn get_property(
562 &self,
563 fqcn: &str,
564 prop_name: &str,
565 ) -> Option<crate::storage::PropertyStorage> {
566 if let Some(cls) = self.classes.get(fqcn) {
568 if let Some(p) = cls.own_properties.get(prop_name) {
569 return Some(p.clone());
570 }
571 }
572
573 let all_parents = {
575 if let Some(cls) = self.classes.get(fqcn) {
576 cls.all_parents.clone()
577 } else {
578 return None;
579 }
580 };
581
582 for ancestor_fqcn in &all_parents {
583 if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
584 if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
585 return Some(p.clone());
586 }
587 }
588 }
589
590 let trait_list = {
592 if let Some(cls) = self.classes.get(fqcn) {
593 cls.traits.clone()
594 } else {
595 vec![]
596 }
597 };
598 for trait_fqcn in &trait_list {
599 if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
600 if let Some(p) = tr.own_properties.get(prop_name) {
601 return Some(p.clone());
602 }
603 }
604 }
605
606 None
607 }
608
609 pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
611 let method_lower = method_name.to_lowercase();
613 let method_name = method_lower.as_str();
614
615 if let Some(cls) = self.classes.get(fqcn) {
617 if let Some(m) = lookup_method(&cls.own_methods, method_name) {
619 return Some(Arc::clone(m));
620 }
621 let own_traits = cls.traits.clone();
623 let ancestors = cls.all_parents.clone();
624 drop(cls);
625
626 for tr_fqcn in &own_traits {
628 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
629 return Some(m);
630 }
631 }
632
633 for ancestor_fqcn in &ancestors {
635 if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
636 if let Some(m) = lookup_method(&anc.own_methods, method_name) {
637 return Some(Arc::clone(m));
638 }
639 let anc_traits = anc.traits.clone();
640 drop(anc);
641 for tr_fqcn in &anc_traits {
642 if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
643 return Some(m);
644 }
645 }
646 } else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
647 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
648 let mut ms = (**m).clone();
649 ms.is_abstract = true;
650 return Some(Arc::new(ms));
651 }
652 }
653 }
655 return None;
656 }
657
658 if let Some(iface) = self.interfaces.get(fqcn) {
660 if let Some(m) = lookup_method(&iface.own_methods, method_name) {
661 return Some(Arc::clone(m));
662 }
663 let parents = iface.all_parents.clone();
664 drop(iface);
665 for parent_fqcn in &parents {
666 if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
667 if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
668 return Some(Arc::clone(m));
669 }
670 }
671 }
672 return None;
673 }
674
675 if let Some(tr) = self.traits.get(fqcn) {
677 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
678 return Some(Arc::clone(m));
679 }
680 return None;
681 }
682
683 if let Some(e) = self.enums.get(fqcn) {
685 if let Some(m) = lookup_method(&e.own_methods, method_name) {
686 return Some(Arc::clone(m));
687 }
688 if matches!(method_name, "cases" | "from" | "tryfrom") {
690 return Some(Arc::new(crate::storage::MethodStorage {
691 fqcn: Arc::from(fqcn),
692 name: Arc::from(method_name),
693 params: vec![],
694 return_type: Some(mir_types::Union::mixed()),
695 inferred_return_type: None,
696 visibility: crate::storage::Visibility::Public,
697 is_static: true,
698 is_abstract: false,
699 is_constructor: false,
700 template_params: vec![],
701 assertions: vec![],
702 throws: vec![],
703 is_final: false,
704 is_internal: false,
705 is_pure: false,
706 is_deprecated: false,
707 location: None,
708 }));
709 }
710 }
711
712 None
713 }
714
715 pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
717 if child == ancestor {
718 return true;
719 }
720 if let Some(cls) = self.classes.get(child) {
721 return cls.implements_or_extends(ancestor);
722 }
723 if let Some(iface) = self.interfaces.get(child) {
724 return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
725 }
726 if let Some(en) = self.enums.get(child) {
729 if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
731 return true;
732 }
733 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
735 return true;
736 }
737 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
739 {
740 return true;
741 }
742 }
743 false
744 }
745
746 pub fn type_exists(&self, fqcn: &str) -> bool {
748 self.classes.contains_key(fqcn)
749 || self.interfaces.contains_key(fqcn)
750 || self.traits.contains_key(fqcn)
751 || self.enums.contains_key(fqcn)
752 }
753
754 pub fn function_exists(&self, fqn: &str) -> bool {
755 self.functions.contains_key(fqn)
756 }
757
758 pub fn is_abstract_class(&self, fqcn: &str) -> bool {
762 self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
763 }
764
765 pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
768 if let Some(cls) = self.classes.get(fqcn) {
769 return cls.template_params.clone();
770 }
771 if let Some(iface) = self.interfaces.get(fqcn) {
772 return iface.template_params.clone();
773 }
774 if let Some(tr) = self.traits.get(fqcn) {
775 return tr.template_params.clone();
776 }
777 vec![]
778 }
779
780 pub fn has_magic_get(&self, fqcn: &str) -> bool {
783 self.get_method(fqcn, "__get").is_some()
784 }
785
786 pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
794 if let Some(iface) = self.interfaces.get(fqcn) {
796 let parents = iface.all_parents.clone();
797 drop(iface);
798 for p in &parents {
799 if !self.type_exists(p.as_ref()) {
800 return true;
801 }
802 }
803 return false;
804 }
805
806 let (parent, interfaces, traits, all_parents) = {
808 let Some(cls) = self.classes.get(fqcn) else {
809 return false;
810 };
811 (
812 cls.parent.clone(),
813 cls.interfaces.clone(),
814 cls.traits.clone(),
815 cls.all_parents.clone(),
816 )
817 };
818
819 if let Some(ref p) = parent {
821 if !self.type_exists(p.as_ref()) {
822 return true;
823 }
824 }
825 for iface in &interfaces {
826 if !self.type_exists(iface.as_ref()) {
827 return true;
828 }
829 }
830 for tr in &traits {
831 if !self.type_exists(tr.as_ref()) {
832 return true;
833 }
834 }
835
836 for ancestor in &all_parents {
838 if !self.type_exists(ancestor.as_ref()) {
839 return true;
840 }
841 }
842
843 false
844 }
845
846 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
853 let name = name.trim_start_matches('\\');
854 if name.is_empty() {
855 return name.to_string();
856 }
857 if name.contains('\\') {
862 let first_segment = name.split('\\').next().unwrap_or(name);
864 if let Some(imports) = self.file_imports.get(file) {
865 if let Some(resolved_prefix) = imports.get(first_segment) {
866 let rest = &name[first_segment.len()..]; return format!("{}{}", resolved_prefix, rest);
868 }
869 }
870 if self.type_exists(name) {
872 return name.to_string();
873 }
874 if let Some(ns) = self.file_namespaces.get(file) {
876 let qualified = format!("{}\\{}", *ns, name);
877 if self.type_exists(&qualified) {
878 return qualified;
879 }
880 }
881 return name.to_string();
882 }
883 match name {
885 "self" | "parent" | "static" | "this" => return name.to_string(),
886 _ => {}
887 }
888 if let Some(imports) = self.file_imports.get(file) {
890 if let Some(resolved) = imports.get(name) {
891 return resolved.clone();
892 }
893 let name_lower = name.to_lowercase();
895 for (alias, resolved) in imports.iter() {
896 if alias.to_lowercase() == name_lower {
897 return resolved.clone();
898 }
899 }
900 }
901 if let Some(ns) = self.file_namespaces.get(file) {
903 let qualified = format!("{}\\{}", *ns, name);
904 if self.type_exists(&qualified) {
909 return qualified;
910 }
911 if self.type_exists(name) {
912 return name.to_string();
913 }
914 return qualified;
915 }
916 name.to_string()
917 }
918
919 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
926 if let Some(cls) = self.classes.get(fqcn) {
927 return cls.location.clone();
928 }
929 if let Some(iface) = self.interfaces.get(fqcn) {
930 return iface.location.clone();
931 }
932 if let Some(tr) = self.traits.get(fqcn) {
933 return tr.location.clone();
934 }
935 if let Some(en) = self.enums.get(fqcn) {
936 return en.location.clone();
937 }
938 if let Some(func) = self.functions.get(fqcn) {
939 return func.location.clone();
940 }
941 None
942 }
943
944 pub fn get_member_location(
946 &self,
947 fqcn: &str,
948 member_name: &str,
949 ) -> Option<crate::storage::Location> {
950 if let Some(method) = self.get_method(fqcn, member_name) {
952 return method.location.clone();
953 }
954 if let Some(prop) = self.get_property(fqcn, member_name) {
956 return prop.location.clone();
957 }
958 if let Some(cls) = self.classes.get(fqcn) {
960 if let Some(c) = cls.own_constants.get(member_name) {
961 return c.location.clone();
962 }
963 }
964 if let Some(iface) = self.interfaces.get(fqcn) {
966 if let Some(c) = iface.own_constants.get(member_name) {
967 return c.location.clone();
968 }
969 }
970 if let Some(tr) = self.traits.get(fqcn) {
972 if let Some(c) = tr.own_constants.get(member_name) {
973 return c.location.clone();
974 }
975 }
976 if let Some(en) = self.enums.get(fqcn) {
978 if let Some(c) = en.own_constants.get(member_name) {
979 return c.location.clone();
980 }
981 if let Some(case) = en.cases.get(member_name) {
982 return case.location.clone();
983 }
984 }
985 None
986 }
987
988 pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
994 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
995 let id = self.symbol_interner.intern_str(&key);
996 self.referenced_methods.insert(id);
997 }
998
999 pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
1001 let key = format!("{}::{}", fqcn, prop_name);
1002 let id = self.symbol_interner.intern_str(&key);
1003 self.referenced_properties.insert(id);
1004 }
1005
1006 pub fn mark_function_referenced(&self, fqn: &str) {
1008 let id = self.symbol_interner.intern_str(fqn);
1009 self.referenced_functions.insert(id);
1010 }
1011
1012 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
1013 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1014 match self.symbol_interner.get_id(&key) {
1015 Some(id) => self.referenced_methods.contains(&id),
1016 None => false,
1017 }
1018 }
1019
1020 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
1021 let key = format!("{}::{}", fqcn, prop_name);
1022 match self.symbol_interner.get_id(&key) {
1023 Some(id) => self.referenced_properties.contains(&id),
1024 None => false,
1025 }
1026 }
1027
1028 pub fn is_function_referenced(&self, fqn: &str) -> bool {
1029 match self.symbol_interner.get_id(fqn) {
1030 Some(id) => self.referenced_functions.contains(&id),
1031 None => false,
1032 }
1033 }
1034
1035 pub fn mark_method_referenced_at(
1038 &self,
1039 fqcn: &str,
1040 method_name: &str,
1041 file: Arc<str>,
1042 start: u32,
1043 end: u32,
1044 ) {
1045 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
1046 self.ensure_expanded();
1047 let sym_id = self.symbol_interner.intern_str(&key);
1048 let file_id = self.file_interner.intern(file);
1049 self.referenced_methods.insert(sym_id);
1050 record_ref(
1051 &self.symbol_reference_locations,
1052 &self.file_symbol_references,
1053 sym_id,
1054 file_id,
1055 start,
1056 end,
1057 );
1058 }
1059
1060 pub fn mark_property_referenced_at(
1063 &self,
1064 fqcn: &str,
1065 prop_name: &str,
1066 file: Arc<str>,
1067 start: u32,
1068 end: u32,
1069 ) {
1070 let key = format!("{}::{}", fqcn, prop_name);
1071 self.ensure_expanded();
1072 let sym_id = self.symbol_interner.intern_str(&key);
1073 let file_id = self.file_interner.intern(file);
1074 self.referenced_properties.insert(sym_id);
1075 record_ref(
1076 &self.symbol_reference_locations,
1077 &self.file_symbol_references,
1078 sym_id,
1079 file_id,
1080 start,
1081 end,
1082 );
1083 }
1084
1085 pub fn mark_function_referenced_at(&self, fqn: &str, file: Arc<str>, start: u32, end: u32) {
1088 self.ensure_expanded();
1089 let sym_id = self.symbol_interner.intern_str(fqn);
1090 let file_id = self.file_interner.intern(file);
1091 self.referenced_functions.insert(sym_id);
1092 record_ref(
1093 &self.symbol_reference_locations,
1094 &self.file_symbol_references,
1095 sym_id,
1096 file_id,
1097 start,
1098 end,
1099 );
1100 }
1101
1102 pub fn mark_class_referenced_at(&self, fqcn: &str, file: Arc<str>, start: u32, end: u32) {
1106 self.ensure_expanded();
1107 let sym_id = self.symbol_interner.intern_str(fqcn);
1108 let file_id = self.file_interner.intern(file);
1109 record_ref(
1110 &self.symbol_reference_locations,
1111 &self.file_symbol_references,
1112 sym_id,
1113 file_id,
1114 start,
1115 end,
1116 );
1117 }
1118
1119 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u32)]) {
1123 if locs.is_empty() {
1124 return;
1125 }
1126 self.ensure_expanded();
1127 let file_id = self.file_interner.intern(file);
1128 for (symbol_key, start, end) in locs {
1129 let sym_id = self.symbol_interner.intern_str(symbol_key);
1130 record_ref(
1131 &self.symbol_reference_locations,
1132 &self.file_symbol_references,
1133 sym_id,
1134 file_id,
1135 *start,
1136 *end,
1137 );
1138 }
1139 }
1140
1141 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u32)> {
1144 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
1145 return Vec::new();
1146 };
1147 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1149 let id = sym_id as usize;
1150 if id + 1 >= ci.sym_offsets.len() {
1151 return Vec::new();
1152 }
1153 let start = ci.sym_offsets[id] as usize;
1154 let end = ci.sym_offsets[id + 1] as usize;
1155 return ci.entries[start..end]
1156 .iter()
1157 .map(|&(_, file_id, s, e)| (self.file_interner.get(file_id), s, e))
1158 .collect();
1159 }
1160 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1162 return Vec::new();
1163 };
1164 entries
1165 .iter()
1166 .map(|&(file_id, start, end)| (self.file_interner.get(file_id), start, end))
1167 .collect()
1168 }
1169
1170 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u32)> {
1173 let Some(file_id) = self.file_interner.get_id(file) else {
1174 return Vec::new();
1175 };
1176 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1178 let id = file_id as usize;
1179 if id + 1 >= ci.file_offsets.len() {
1180 return Vec::new();
1181 }
1182 let start = ci.file_offsets[id] as usize;
1183 let end = ci.file_offsets[id + 1] as usize;
1184 return ci.by_file[start..end]
1185 .iter()
1186 .map(|&entry_idx| {
1187 let (sym_id, _, s, e) = ci.entries[entry_idx as usize];
1188 (self.symbol_interner.get(sym_id), s, e)
1189 })
1190 .collect();
1191 }
1192 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
1194 return Vec::new();
1195 };
1196 let mut out = Vec::new();
1197 for &sym_id in sym_ids.iter() {
1198 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
1199 continue;
1200 };
1201 let sym_key = self.symbol_interner.get(sym_id);
1202 for &(entry_file_id, start, end) in entries.iter() {
1203 if entry_file_id == file_id {
1204 out.push((sym_key.clone(), start, end));
1205 }
1206 }
1207 }
1208 out
1209 }
1210
1211 pub fn file_has_symbol_references(&self, file: &str) -> bool {
1213 let Some(file_id) = self.file_interner.get_id(file) else {
1214 return false;
1215 };
1216 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
1218 let id = file_id as usize;
1219 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
1220 }
1221 self.file_symbol_references.contains_key(&file_id)
1222 }
1223
1224 pub fn finalize(&self) {
1231 if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
1232 return;
1233 }
1234
1235 let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
1237 for fqcn in &class_keys {
1238 let parents = self.collect_class_ancestors(fqcn);
1239 if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
1240 cls.all_parents = parents;
1241 }
1242 }
1243
1244 let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
1246 for fqcn in &iface_keys {
1247 let parents = self.collect_interface_ancestors(fqcn);
1248 if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
1249 iface.all_parents = parents;
1250 }
1251 }
1252
1253 self.finalized
1254 .store(true, std::sync::atomic::Ordering::SeqCst);
1255 }
1256
1257 fn get_method_in_trait(
1265 &self,
1266 tr_fqcn: &Arc<str>,
1267 method_name: &str,
1268 ) -> Option<Arc<MethodStorage>> {
1269 let mut visited = std::collections::HashSet::new();
1270 self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
1271 }
1272
1273 fn get_method_in_trait_inner(
1274 &self,
1275 tr_fqcn: &Arc<str>,
1276 method_name: &str,
1277 visited: &mut std::collections::HashSet<String>,
1278 ) -> Option<Arc<MethodStorage>> {
1279 if !visited.insert(tr_fqcn.to_string()) {
1280 return None; }
1282 let tr = self.traits.get(tr_fqcn.as_ref())?;
1283 if let Some(m) = lookup_method(&tr.own_methods, method_name) {
1284 return Some(Arc::clone(m));
1285 }
1286 let used_traits = tr.traits.clone();
1287 drop(tr);
1288 for used_fqcn in &used_traits {
1289 if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
1290 return Some(m);
1291 }
1292 }
1293 None
1294 }
1295
1296 fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1297 let mut result = Vec::new();
1298 let mut visited = std::collections::HashSet::new();
1299 self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
1300 result
1301 }
1302
1303 fn collect_class_ancestors_inner(
1304 &self,
1305 fqcn: &str,
1306 out: &mut Vec<Arc<str>>,
1307 visited: &mut std::collections::HashSet<String>,
1308 ) {
1309 if !visited.insert(fqcn.to_string()) {
1310 return; }
1312 let (parent, interfaces, traits) = {
1313 if let Some(cls) = self.classes.get(fqcn) {
1314 (
1315 cls.parent.clone(),
1316 cls.interfaces.clone(),
1317 cls.traits.clone(),
1318 )
1319 } else {
1320 return;
1321 }
1322 };
1323
1324 if let Some(p) = parent {
1325 out.push(p.clone());
1326 self.collect_class_ancestors_inner(&p, out, visited);
1327 }
1328 for iface in interfaces {
1329 out.push(iface.clone());
1330 self.collect_interface_ancestors_inner(&iface, out, visited);
1331 }
1332 for t in traits {
1333 out.push(t);
1334 }
1335 }
1336
1337 fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
1338 let mut result = Vec::new();
1339 let mut visited = std::collections::HashSet::new();
1340 self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
1341 result
1342 }
1343
1344 fn collect_interface_ancestors_inner(
1345 &self,
1346 fqcn: &str,
1347 out: &mut Vec<Arc<str>>,
1348 visited: &mut std::collections::HashSet<String>,
1349 ) {
1350 if !visited.insert(fqcn.to_string()) {
1351 return;
1352 }
1353 let extends = {
1354 if let Some(iface) = self.interfaces.get(fqcn) {
1355 iface.extends.clone()
1356 } else {
1357 return;
1358 }
1359 };
1360 for e in extends {
1361 out.push(e.clone());
1362 self.collect_interface_ancestors_inner(&e, out, visited);
1363 }
1364 }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369 use super::*;
1370
1371 fn arc(s: &str) -> Arc<str> {
1372 Arc::from(s)
1373 }
1374
1375 #[test]
1376 fn method_referenced_at_groups_spans_by_file() {
1377 let cb = Codebase::new();
1378 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1379 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 10, 15);
1380 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 20, 25);
1381
1382 let locs = cb.get_reference_locations("Foo::bar");
1383 let files: std::collections::HashSet<&str> =
1384 locs.iter().map(|(f, _, _)| f.as_ref()).collect();
1385 assert_eq!(files.len(), 2, "two files, not three spans");
1386 assert!(locs.contains(&(arc("a.php"), 0, 5)));
1387 assert!(locs.contains(&(arc("a.php"), 10, 15)));
1388 assert_eq!(
1389 locs.iter()
1390 .filter(|(f, _, _)| f.as_ref() == "a.php")
1391 .count(),
1392 2
1393 );
1394 assert!(locs.contains(&(arc("b.php"), 20, 25)));
1395 assert!(
1396 cb.is_method_referenced("Foo", "bar"),
1397 "DashSet also updated"
1398 );
1399 }
1400
1401 #[test]
1402 fn duplicate_spans_are_deduplicated() {
1403 let cb = Codebase::new();
1404 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1406 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
1407
1408 let count = cb
1409 .get_reference_locations("Foo::bar")
1410 .iter()
1411 .filter(|(f, _, _)| f.as_ref() == "a.php")
1412 .count();
1413 assert_eq!(count, 1, "duplicate span deduplicated");
1414 }
1415
1416 #[test]
1417 fn method_key_is_lowercased() {
1418 let cb = Codebase::new();
1419 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 0, 3);
1420 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
1421 }
1422
1423 #[test]
1424 fn property_referenced_at_records_location() {
1425 let cb = Codebase::new();
1426 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 5, 10);
1427
1428 assert!(cb
1429 .get_reference_locations("Bar::count")
1430 .contains(&(arc("x.php"), 5, 10)));
1431 assert!(cb.is_property_referenced("Bar", "count"));
1432 }
1433
1434 #[test]
1435 fn function_referenced_at_records_location() {
1436 let cb = Codebase::new();
1437 cb.mark_function_referenced_at("my_fn", arc("a.php"), 10, 15);
1438
1439 assert!(cb
1440 .get_reference_locations("my_fn")
1441 .contains(&(arc("a.php"), 10, 15)));
1442 assert!(cb.is_function_referenced("my_fn"));
1443 }
1444
1445 #[test]
1446 fn class_referenced_at_records_location() {
1447 let cb = Codebase::new();
1448 cb.mark_class_referenced_at("Foo", arc("a.php"), 5, 8);
1449
1450 assert!(cb
1451 .get_reference_locations("Foo")
1452 .contains(&(arc("a.php"), 5, 8)));
1453 }
1454
1455 #[test]
1456 fn get_reference_locations_flattens_all_files() {
1457 let cb = Codebase::new();
1458 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1459 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1460
1461 let mut locs = cb.get_reference_locations("fn1");
1462 locs.sort_by_key(|(_, s, _)| *s);
1463 assert_eq!(locs.len(), 2);
1464 assert_eq!(locs[0], (arc("a.php"), 0, 5));
1465 assert_eq!(locs[1], (arc("b.php"), 10, 15));
1466 }
1467
1468 #[test]
1469 fn replay_reference_locations_restores_index() {
1470 let cb = Codebase::new();
1471 let locs = vec![
1472 ("Foo::bar".to_string(), 0u32, 5u32),
1473 ("Foo::bar".to_string(), 10, 15),
1474 ("greet".to_string(), 20, 25),
1475 ];
1476 cb.replay_reference_locations(arc("a.php"), &locs);
1477
1478 let bar_locs = cb.get_reference_locations("Foo::bar");
1479 assert!(bar_locs.contains(&(arc("a.php"), 0, 5)));
1480 assert!(bar_locs.contains(&(arc("a.php"), 10, 15)));
1481
1482 assert!(cb
1483 .get_reference_locations("greet")
1484 .contains(&(arc("a.php"), 20, 25)));
1485
1486 assert!(cb.file_has_symbol_references("a.php"));
1487 }
1488
1489 #[test]
1490 fn remove_file_clears_its_spans_only() {
1491 let cb = Codebase::new();
1492 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1493 cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
1494
1495 cb.remove_file_definitions("a.php");
1496
1497 let locs = cb.get_reference_locations("fn1");
1498 assert!(
1499 !locs.iter().any(|(f, _, _)| f.as_ref() == "a.php"),
1500 "a.php spans removed"
1501 );
1502 assert!(
1503 locs.contains(&(arc("b.php"), 10, 15)),
1504 "b.php spans untouched"
1505 );
1506 assert!(!cb.file_has_symbol_references("a.php"));
1507 }
1508
1509 #[test]
1510 fn remove_file_does_not_affect_other_files() {
1511 let cb = Codebase::new();
1512 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 4);
1513 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 7, 10);
1514
1515 cb.remove_file_definitions("x.php");
1516
1517 let locs = cb.get_reference_locations("Cls::prop");
1518 assert!(!locs.iter().any(|(f, _, _)| f.as_ref() == "x.php"));
1519 assert!(locs.contains(&(arc("y.php"), 7, 10)));
1520 }
1521
1522 #[test]
1523 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1524 let cb = Codebase::new();
1525 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1526
1527 cb.remove_file_definitions("ghost.php");
1529
1530 assert!(cb
1532 .get_reference_locations("fn1")
1533 .contains(&(arc("a.php"), 0, 5)));
1534 assert!(!cb.file_has_symbol_references("ghost.php"));
1535 }
1536
1537 #[test]
1538 fn replay_reference_locations_with_empty_list_is_noop() {
1539 let cb = Codebase::new();
1540 cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
1541
1542 cb.replay_reference_locations(arc("b.php"), &[]);
1544
1545 assert!(
1546 !cb.file_has_symbol_references("b.php"),
1547 "empty replay must not create a file entry"
1548 );
1549 assert!(
1550 cb.get_reference_locations("fn1")
1551 .contains(&(arc("a.php"), 0, 5)),
1552 "existing spans untouched"
1553 );
1554 }
1555
1556 #[test]
1557 fn replay_reference_locations_twice_does_not_duplicate_spans() {
1558 let cb = Codebase::new();
1559 let locs = vec![("fn1".to_string(), 0u32, 5u32)];
1560
1561 cb.replay_reference_locations(arc("a.php"), &locs);
1562 cb.replay_reference_locations(arc("a.php"), &locs);
1563
1564 let count = cb
1565 .get_reference_locations("fn1")
1566 .iter()
1567 .filter(|(f, _, _)| f.as_ref() == "a.php")
1568 .count();
1569 assert_eq!(
1570 count, 1,
1571 "replaying the same location twice must not create duplicate spans"
1572 );
1573 }
1574}