1use std::sync::Arc;
2
3use dashmap::{DashMap, DashSet};
4
5use crate::interner::Interner;
6
7type ReferenceLocations = DashMap<u32, Vec<(u32, u32, u16, u16)>>;
16
17use crate::storage::{ClassStorage, EnumStorage, FunctionStorage, InterfaceStorage, TraitStorage};
18use mir_types::Union;
19
20#[inline]
30fn record_ref(
31 sym_locs: &ReferenceLocations,
32 file_refs: &DashMap<u32, Vec<u32>>,
33 sym_id: u32,
34 file_id: u32,
35 line: u32,
36 col_start: u16,
37 col_end: u16,
38) {
39 {
40 let mut entries = sym_locs.entry(sym_id).or_default();
41 let span = (file_id, line, col_start, col_end);
42 if !entries.contains(&span) {
43 entries.push(span);
44 }
45 }
46 {
47 let mut refs = file_refs.entry(file_id).or_default();
48 if !refs.contains(&sym_id) {
49 refs.push(sym_id);
50 }
51 }
52}
53
54#[derive(Debug, Default)]
68struct CompactRefIndex {
69 entries: Vec<(u32, u32, u32, u16, u16)>,
72 sym_offsets: Vec<u32>,
74 by_file: Vec<u32>,
77 file_offsets: Vec<u32>,
79}
80
81#[derive(Debug, Default)]
86pub struct Codebase {
87 pub classes: DashMap<Arc<str>, ClassStorage>,
88 pub interfaces: DashMap<Arc<str>, InterfaceStorage>,
89 pub traits: DashMap<Arc<str>, TraitStorage>,
90 pub enums: DashMap<Arc<str>, EnumStorage>,
91 pub functions: DashMap<Arc<str>, FunctionStorage>,
92 pub constants: DashMap<Arc<str>, Union>,
93
94 pub global_vars: DashMap<Arc<str>, Union>,
97 file_global_vars: DashMap<Arc<str>, Vec<Arc<str>>>,
100
101 referenced_methods: DashSet<u32>,
104 referenced_properties: DashSet<u32>,
106 referenced_functions: DashSet<u32>,
108
109 pub symbol_interner: Interner,
112 pub file_interner: Interner,
114
115 symbol_reference_locations: ReferenceLocations,
118 file_symbol_references: DashMap<u32, Vec<u32>>,
123
124 compact_ref_index: std::sync::RwLock<Option<CompactRefIndex>>,
128 is_compacted: std::sync::atomic::AtomicBool,
131
132 pub symbol_to_file: DashMap<Arc<str>, Arc<str>>,
135
136 pub known_symbols: DashSet<Arc<str>>,
140
141 pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
149 pub file_namespaces: DashMap<Arc<str>, String>,
157}
158
159impl Codebase {
160 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn inject_stub_slice(&self, slice: crate::storage::StubSlice) {
183 let file = slice.file.clone();
184 for cls in slice.classes {
185 if let Some(f) = &file {
186 self.symbol_to_file.insert(cls.fqcn.clone(), f.clone());
187 }
188 self.classes.insert(cls.fqcn.clone(), cls);
189 }
190 for iface in slice.interfaces {
191 if let Some(f) = &file {
192 self.symbol_to_file.insert(iface.fqcn.clone(), f.clone());
193 }
194 self.interfaces.insert(iface.fqcn.clone(), iface);
195 }
196 for tr in slice.traits {
197 if let Some(f) = &file {
198 self.symbol_to_file.insert(tr.fqcn.clone(), f.clone());
199 }
200 self.traits.insert(tr.fqcn.clone(), tr);
201 }
202 for en in slice.enums {
203 if let Some(f) = &file {
204 self.symbol_to_file.insert(en.fqcn.clone(), f.clone());
205 }
206 self.enums.insert(en.fqcn.clone(), en);
207 }
208 for func in slice.functions {
209 if let Some(f) = &file {
210 self.symbol_to_file.insert(func.fqn.clone(), f.clone());
211 }
212 self.functions.insert(func.fqn.clone(), func);
213 }
214 for (name, ty) in slice.constants {
215 self.constants.insert(name, ty);
216 }
217 if let Some(f) = &file {
218 for (name, ty) in slice.global_vars {
219 self.register_global_var(f, name, ty);
220 }
221 if let Some(ns) = slice.namespace {
222 self.file_namespaces.insert(f.clone(), ns.to_string());
223 }
224 if !slice.imports.is_empty() {
225 self.file_imports.insert(f.clone(), slice.imports);
226 }
227 }
228 }
229
230 pub fn compact_reference_index(&self) {
247 let mut entries: Vec<(u32, u32, u32, u16, u16)> = self
249 .symbol_reference_locations
250 .iter()
251 .flat_map(|entry| {
252 let sym_id = *entry.key();
253 entry
254 .value()
255 .iter()
256 .map(move |&(file_id, line, col_start, col_end)| {
257 (sym_id, file_id, line, col_start, col_end)
258 })
259 .collect::<Vec<_>>()
260 })
261 .collect();
262
263 if entries.is_empty() {
264 return;
265 }
266
267 entries.sort_unstable();
269 entries.dedup();
270
271 let n = entries.len();
272
273 let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
275 let mut sym_offsets = vec![0u32; max_sym + 2];
276 for &(sym_id, ..) in &entries {
277 sym_offsets[sym_id as usize + 1] += 1;
278 }
279 for i in 1..sym_offsets.len() {
280 sym_offsets[i] += sym_offsets[i - 1];
281 }
282
283 let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
287 let mut by_file: Vec<u32> = (0..n as u32).collect();
288 by_file.sort_unstable_by_key(|&i| {
289 let (sym_id, file_id, line, col_start, col_end) = entries[i as usize];
290 (file_id, sym_id, line, col_start, col_end)
291 });
292
293 let mut file_offsets = vec![0u32; max_file + 2];
294 for &idx in &by_file {
295 let file_id = entries[idx as usize].1;
296 file_offsets[file_id as usize + 1] += 1;
297 }
298 for i in 1..file_offsets.len() {
299 file_offsets[i] += file_offsets[i - 1];
300 }
301
302 *self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
303 entries,
304 sym_offsets,
305 by_file,
306 file_offsets,
307 });
308 self.is_compacted
309 .store(true, std::sync::atomic::Ordering::Release);
310
311 self.symbol_reference_locations.clear();
313 self.file_symbol_references.clear();
314 }
315
316 fn ensure_expanded(&self) {
322 if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
324 return;
325 }
326 let mut guard = self.compact_ref_index.write().unwrap();
328 if let Some(ci) = guard.take() {
329 for &(sym_id, file_id, line, col_start, col_end) in &ci.entries {
330 record_ref(
331 &self.symbol_reference_locations,
332 &self.file_symbol_references,
333 sym_id,
334 file_id,
335 line,
336 col_start,
337 col_end,
338 );
339 }
340 self.is_compacted
341 .store(false, std::sync::atomic::Ordering::Release);
342 }
343 }
345
346 pub fn remove_file_definitions(&self, file_path: &str) {
355 let symbols: Vec<Arc<str>> = self
357 .symbol_to_file
358 .iter()
359 .filter(|entry| entry.value().as_ref() == file_path)
360 .map(|entry| entry.key().clone())
361 .collect();
362
363 for sym in &symbols {
365 self.classes.remove(sym.as_ref());
366 self.interfaces.remove(sym.as_ref());
367 self.traits.remove(sym.as_ref());
368 self.enums.remove(sym.as_ref());
369 self.functions.remove(sym.as_ref());
370 self.constants.remove(sym.as_ref());
371 self.symbol_to_file.remove(sym.as_ref());
372 self.known_symbols.remove(sym.as_ref());
373 }
374
375 self.file_imports.remove(file_path);
377 self.file_namespaces.remove(file_path);
378
379 if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
381 for name in var_names {
382 self.global_vars.remove(name.as_ref());
383 }
384 }
385
386 self.ensure_expanded();
388
389 if let Some(file_id) = self.file_interner.get_id(file_path) {
392 if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
393 for sym_id in sym_ids {
394 if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
395 entries.retain(|&(fid, ..)| fid != file_id);
396 }
397 }
398 }
399 }
400 }
401
402 fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
409 self.file_global_vars
410 .entry(file.clone())
411 .or_default()
412 .push(name.clone());
413 self.global_vars.insert(name, ty);
414 }
415
416 pub fn type_exists(&self, fqcn: &str) -> bool {
422 self.classes.contains_key(fqcn)
423 || self.interfaces.contains_key(fqcn)
424 || self.traits.contains_key(fqcn)
425 || self.enums.contains_key(fqcn)
426 }
427
428 pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
435 let name = name.trim_start_matches('\\');
436 if name.is_empty() {
437 return name.to_string();
438 }
439 if name.contains('\\') {
444 let first_segment = name.split('\\').next().unwrap_or(name);
446 if let Some(imports) = self.file_imports.get(file) {
447 if let Some(resolved_prefix) = imports.get(first_segment) {
448 let rest = &name[first_segment.len()..]; return format!("{resolved_prefix}{rest}");
450 }
451 }
452 if self.type_exists(name) {
454 return name.to_string();
455 }
456 if let Some(ns) = self.file_namespaces.get(file) {
458 let qualified = format!("{}\\{}", *ns, name);
459 if self.type_exists(&qualified) {
460 return qualified;
461 }
462 }
463 return name.to_string();
464 }
465 match name {
467 "self" | "parent" | "static" | "this" => return name.to_string(),
468 _ => {}
469 }
470 if let Some(imports) = self.file_imports.get(file) {
472 if let Some(resolved) = imports.get(name) {
473 return resolved.clone();
474 }
475 let name_lower = name.to_lowercase();
477 for (alias, resolved) in imports.iter() {
478 if alias.to_lowercase() == name_lower {
479 return resolved.clone();
480 }
481 }
482 }
483 if let Some(ns) = self.file_namespaces.get(file) {
485 let qualified = format!("{}\\{}", *ns, name);
486 if self.type_exists(&qualified) {
491 return qualified;
492 }
493 if self.type_exists(name) {
494 return name.to_string();
495 }
496 return qualified;
497 }
498 name.to_string()
499 }
500
501 pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
508 if let Some(cls) = self.classes.get(fqcn) {
509 return cls.location.clone();
510 }
511 if let Some(iface) = self.interfaces.get(fqcn) {
512 return iface.location.clone();
513 }
514 if let Some(tr) = self.traits.get(fqcn) {
515 return tr.location.clone();
516 }
517 if let Some(en) = self.enums.get(fqcn) {
518 return en.location.clone();
519 }
520 if let Some(func) = self.functions.get(fqcn) {
521 return func.location.clone();
522 }
523 None
524 }
525
526 pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
531 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
532 match self.symbol_interner.get_id(&key) {
533 Some(id) => self.referenced_methods.contains(&id),
534 None => false,
535 }
536 }
537
538 pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
539 let key = format!("{fqcn}::{prop_name}");
540 match self.symbol_interner.get_id(&key) {
541 Some(id) => self.referenced_properties.contains(&id),
542 None => false,
543 }
544 }
545
546 pub fn is_function_referenced(&self, fqn: &str) -> bool {
547 match self.symbol_interner.get_id(fqn) {
548 Some(id) => self.referenced_functions.contains(&id),
549 None => false,
550 }
551 }
552
553 pub fn mark_method_referenced_at(
556 &self,
557 fqcn: &str,
558 method_name: &str,
559 file: Arc<str>,
560 line: u32,
561 col_start: u16,
562 col_end: u16,
563 ) {
564 let key = format!("{}::{}", fqcn, method_name.to_lowercase());
565 self.ensure_expanded();
566 let sym_id = self.symbol_interner.intern_str(&key);
567 let file_id = self.file_interner.intern(file);
568 self.referenced_methods.insert(sym_id);
569 record_ref(
570 &self.symbol_reference_locations,
571 &self.file_symbol_references,
572 sym_id,
573 file_id,
574 line,
575 col_start,
576 col_end,
577 );
578 }
579
580 pub fn mark_property_referenced_at(
583 &self,
584 fqcn: &str,
585 prop_name: &str,
586 file: Arc<str>,
587 line: u32,
588 col_start: u16,
589 col_end: u16,
590 ) {
591 let key = format!("{fqcn}::{prop_name}");
592 self.ensure_expanded();
593 let sym_id = self.symbol_interner.intern_str(&key);
594 let file_id = self.file_interner.intern(file);
595 self.referenced_properties.insert(sym_id);
596 record_ref(
597 &self.symbol_reference_locations,
598 &self.file_symbol_references,
599 sym_id,
600 file_id,
601 line,
602 col_start,
603 col_end,
604 );
605 }
606
607 pub fn mark_function_referenced_at(
610 &self,
611 fqn: &str,
612 file: Arc<str>,
613 line: u32,
614 col_start: u16,
615 col_end: u16,
616 ) {
617 self.ensure_expanded();
618 let sym_id = self.symbol_interner.intern_str(fqn);
619 let file_id = self.file_interner.intern(file);
620 self.referenced_functions.insert(sym_id);
621 record_ref(
622 &self.symbol_reference_locations,
623 &self.file_symbol_references,
624 sym_id,
625 file_id,
626 line,
627 col_start,
628 col_end,
629 );
630 }
631
632 pub fn mark_class_referenced_at(
636 &self,
637 fqcn: &str,
638 file: Arc<str>,
639 line: u32,
640 col_start: u16,
641 col_end: u16,
642 ) {
643 self.ensure_expanded();
644 let sym_id = self.symbol_interner.intern_str(fqcn);
645 let file_id = self.file_interner.intern(file);
646 record_ref(
647 &self.symbol_reference_locations,
648 &self.file_symbol_references,
649 sym_id,
650 file_id,
651 line,
652 col_start,
653 col_end,
654 );
655 }
656
657 pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
661 if locs.is_empty() {
662 return;
663 }
664 self.ensure_expanded();
665 let file_id = self.file_interner.intern(file);
666 for (symbol_key, line, col_start, col_end) in locs {
667 let sym_id = self.symbol_interner.intern_str(symbol_key);
668 record_ref(
669 &self.symbol_reference_locations,
670 &self.file_symbol_references,
671 sym_id,
672 file_id,
673 *line,
674 *col_start,
675 *col_end,
676 );
677 }
678 }
679
680 pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
683 let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
684 return Vec::new();
685 };
686 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
688 let id = sym_id as usize;
689 if id + 1 >= ci.sym_offsets.len() {
690 return Vec::new();
691 }
692 let start = ci.sym_offsets[id] as usize;
693 let end = ci.sym_offsets[id + 1] as usize;
694 return ci.entries[start..end]
695 .iter()
696 .map(|&(_, file_id, line, col_start, col_end)| {
697 (self.file_interner.get(file_id), line, col_start, col_end)
698 })
699 .collect();
700 }
701 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
703 return Vec::new();
704 };
705 entries
706 .iter()
707 .map(|&(file_id, line, col_start, col_end)| {
708 (self.file_interner.get(file_id), line, col_start, col_end)
709 })
710 .collect()
711 }
712
713 pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
717 let Some(file_id) = self.file_interner.get_id(file) else {
718 return Vec::new();
719 };
720 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
722 let id = file_id as usize;
723 if id + 1 >= ci.file_offsets.len() {
724 return Vec::new();
725 }
726 let start = ci.file_offsets[id] as usize;
727 let end = ci.file_offsets[id + 1] as usize;
728 return ci.by_file[start..end]
729 .iter()
730 .map(|&entry_idx| {
731 let (sym_id, _, line, col_start, col_end) = ci.entries[entry_idx as usize];
732 (self.symbol_interner.get(sym_id), line, col_start, col_end)
733 })
734 .collect();
735 }
736 let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
738 return Vec::new();
739 };
740 let mut out = Vec::new();
741 for &sym_id in sym_ids.iter() {
742 let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
743 continue;
744 };
745 let sym_key = self.symbol_interner.get(sym_id);
746 for &(entry_file_id, line, col_start, col_end) in entries.iter() {
747 if entry_file_id == file_id {
748 out.push((sym_key.clone(), line, col_start, col_end));
749 }
750 }
751 }
752 out
753 }
754
755 pub fn file_has_symbol_references(&self, file: &str) -> bool {
757 let Some(file_id) = self.file_interner.get_id(file) else {
758 return false;
759 };
760 if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
762 let id = file_id as usize;
763 return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
764 }
765 self.file_symbol_references.contains_key(&file_id)
766 }
767
768 pub fn resolve_pending_import_types(&self) {
781 type PendingImports = Vec<(Arc<str>, Vec<(Arc<str>, Arc<str>, Arc<str>)>)>;
783 let pending: PendingImports = self
784 .classes
785 .iter()
786 .filter(|e| !e.pending_import_types.is_empty())
787 .map(|e| (e.key().clone(), e.pending_import_types.clone()))
788 .collect();
789 for (dst_fqcn, imports) in pending {
790 let mut resolved: std::collections::HashMap<Arc<str>, mir_types::Union> =
791 std::collections::HashMap::new();
792 for (local, original, from_class) in &imports {
793 if let Some(src_cls) = self.classes.get(from_class.as_ref()) {
794 if let Some(ty) = src_cls.type_aliases.get(original.as_ref()) {
795 resolved.insert(local.clone(), ty.clone());
796 }
797 }
798 }
799 if !resolved.is_empty() {
800 if let Some(mut dst_cls) = self.classes.get_mut(dst_fqcn.as_ref()) {
801 for (k, v) in resolved {
802 dst_cls.type_aliases.insert(k, v);
803 }
804 }
805 }
806 }
807 }
808}
809
810pub struct CodebaseBuilder {
822 cb: Codebase,
823}
824
825impl CodebaseBuilder {
826 pub fn new() -> Self {
827 Self {
828 cb: Codebase::new(),
829 }
830 }
831
832 pub fn add(&mut self, slice: crate::storage::StubSlice) {
835 self.cb.inject_stub_slice(slice);
836 }
837
838 pub fn finalize(self) -> Codebase {
840 self.cb.resolve_pending_import_types();
841 self.cb
842 }
843
844 pub fn codebase(&self) -> &Codebase {
846 &self.cb
847 }
848}
849
850impl Default for CodebaseBuilder {
851 fn default() -> Self {
852 Self::new()
853 }
854}
855
856pub fn codebase_from_parts(parts: Vec<crate::storage::StubSlice>) -> Codebase {
858 let mut b = CodebaseBuilder::new();
859 for p in parts {
860 b.add(p);
861 }
862 b.finalize()
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868
869 fn arc(s: &str) -> Arc<str> {
870 Arc::from(s)
871 }
872
873 #[test]
874 fn method_referenced_at_groups_spans_by_file() {
875 let cb = Codebase::new();
876 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
877 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 10, 15);
878 cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 2, 0, 5);
879
880 let locs = cb.get_reference_locations("Foo::bar");
881 let files: std::collections::HashSet<&str> =
882 locs.iter().map(|(f, ..)| f.as_ref()).collect();
883 assert_eq!(files.len(), 2, "two files, not three spans");
884 assert!(locs.contains(&(arc("a.php"), 1, 0, 5)));
885 assert!(locs.contains(&(arc("a.php"), 1, 10, 15)));
886 assert_eq!(
887 locs.iter().filter(|(f, ..)| f.as_ref() == "a.php").count(),
888 2
889 );
890 assert!(locs.contains(&(arc("b.php"), 2, 0, 5)));
891 assert!(
892 cb.is_method_referenced("Foo", "bar"),
893 "DashSet also updated"
894 );
895 }
896
897 #[test]
898 fn duplicate_spans_are_deduplicated() {
899 let cb = Codebase::new();
900 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
902 cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 1, 0, 5);
903
904 let count = cb
905 .get_reference_locations("Foo::bar")
906 .iter()
907 .filter(|(f, ..)| f.as_ref() == "a.php")
908 .count();
909 assert_eq!(count, 1, "duplicate span deduplicated");
910 }
911
912 #[test]
913 fn method_key_is_lowercased() {
914 let cb = Codebase::new();
915 cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 1, 0, 3);
916 assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
917 }
918
919 #[test]
920 fn property_referenced_at_records_location() {
921 let cb = Codebase::new();
922 cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 1, 5, 10);
923
924 assert!(cb
925 .get_reference_locations("Bar::count")
926 .contains(&(arc("x.php"), 1, 5, 10)));
927 assert!(cb.is_property_referenced("Bar", "count"));
928 }
929
930 #[test]
931 fn function_referenced_at_records_location() {
932 let cb = Codebase::new();
933 cb.mark_function_referenced_at("my_fn", arc("a.php"), 1, 10, 15);
934
935 assert!(cb
936 .get_reference_locations("my_fn")
937 .contains(&(arc("a.php"), 1, 10, 15)));
938 assert!(cb.is_function_referenced("my_fn"));
939 }
940
941 #[test]
942 fn class_referenced_at_records_location() {
943 let cb = Codebase::new();
944 cb.mark_class_referenced_at("Foo", arc("a.php"), 1, 5, 8);
945
946 assert!(cb
947 .get_reference_locations("Foo")
948 .contains(&(arc("a.php"), 1, 5, 8)));
949 }
950
951 #[test]
952 fn get_reference_locations_flattens_all_files() {
953 let cb = Codebase::new();
954 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
955 cb.mark_function_referenced_at("fn1", arc("b.php"), 2, 0, 5);
956
957 let mut locs = cb.get_reference_locations("fn1");
958 locs.sort_by_key(|&(_, line, col, _)| (line, col));
959 assert_eq!(locs.len(), 2);
960 assert_eq!(locs[0], (arc("a.php"), 1, 0, 5));
961 assert_eq!(locs[1], (arc("b.php"), 2, 0, 5));
962 }
963
964 #[test]
965 fn replay_reference_locations_restores_index() {
966 let cb = Codebase::new();
967 let locs = vec![
968 ("Foo::bar".to_string(), 1u32, 0u16, 5u16),
969 ("Foo::bar".to_string(), 1, 10, 15),
970 ("greet".to_string(), 2, 0, 5),
971 ];
972 cb.replay_reference_locations(arc("a.php"), &locs);
973
974 let bar_locs = cb.get_reference_locations("Foo::bar");
975 assert!(bar_locs.contains(&(arc("a.php"), 1, 0, 5)));
976 assert!(bar_locs.contains(&(arc("a.php"), 1, 10, 15)));
977
978 assert!(cb
979 .get_reference_locations("greet")
980 .contains(&(arc("a.php"), 2, 0, 5)));
981
982 assert!(cb.file_has_symbol_references("a.php"));
983 }
984
985 #[test]
986 fn remove_file_clears_its_spans_only() {
987 let cb = Codebase::new();
988 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
989 cb.mark_function_referenced_at("fn1", arc("b.php"), 1, 10, 15);
990
991 cb.remove_file_definitions("a.php");
992
993 let locs = cb.get_reference_locations("fn1");
994 assert!(
995 !locs.iter().any(|(f, ..)| f.as_ref() == "a.php"),
996 "a.php spans removed"
997 );
998 assert!(
999 locs.contains(&(arc("b.php"), 1, 10, 15)),
1000 "b.php spans untouched"
1001 );
1002 assert!(!cb.file_has_symbol_references("a.php"));
1003 }
1004
1005 #[test]
1006 fn remove_file_does_not_affect_other_files() {
1007 let cb = Codebase::new();
1008 cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 1, 4);
1009 cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 1, 7, 10);
1010
1011 cb.remove_file_definitions("x.php");
1012
1013 let locs = cb.get_reference_locations("Cls::prop");
1014 assert!(!locs.iter().any(|(f, ..)| f.as_ref() == "x.php"));
1015 assert!(locs.contains(&(arc("y.php"), 1, 7, 10)));
1016 }
1017
1018 #[test]
1019 fn remove_file_definitions_on_never_analyzed_file_is_noop() {
1020 let cb = Codebase::new();
1021 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1022
1023 cb.remove_file_definitions("ghost.php");
1025
1026 assert!(cb
1028 .get_reference_locations("fn1")
1029 .contains(&(arc("a.php"), 1, 0, 5)));
1030 assert!(!cb.file_has_symbol_references("ghost.php"));
1031 }
1032
1033 #[test]
1034 fn replay_reference_locations_with_empty_list_is_noop() {
1035 let cb = Codebase::new();
1036 cb.mark_function_referenced_at("fn1", arc("a.php"), 1, 0, 5);
1037
1038 cb.replay_reference_locations(arc("b.php"), &[]);
1040
1041 assert!(
1042 !cb.file_has_symbol_references("b.php"),
1043 "empty replay must not create a file entry"
1044 );
1045 assert!(
1046 cb.get_reference_locations("fn1")
1047 .contains(&(arc("a.php"), 1, 0, 5)),
1048 "existing spans untouched"
1049 );
1050 }
1051
1052 #[test]
1053 fn replay_reference_locations_twice_does_not_duplicate_spans() {
1054 let cb = Codebase::new();
1055 let locs = vec![("fn1".to_string(), 1u32, 0u16, 5u16)];
1056
1057 cb.replay_reference_locations(arc("a.php"), &locs);
1058 cb.replay_reference_locations(arc("a.php"), &locs);
1059
1060 let count = cb
1061 .get_reference_locations("fn1")
1062 .iter()
1063 .filter(|(f, ..)| f.as_ref() == "a.php")
1064 .count();
1065 assert_eq!(
1066 count, 1,
1067 "replaying the same location twice must not create duplicate spans"
1068 );
1069 }
1070
1071 fn make_fn(fqn: &str, short_name: &str) -> crate::storage::FunctionStorage {
1076 crate::storage::FunctionStorage {
1077 fqn: Arc::from(fqn),
1078 short_name: Arc::from(short_name),
1079 params: vec![],
1080 return_type: None,
1081 inferred_return_type: None,
1082 template_params: vec![],
1083 assertions: vec![],
1084 throws: vec![],
1085 deprecated: None,
1086 is_pure: false,
1087 location: None,
1088 }
1089 }
1090
1091 #[test]
1092 fn inject_stub_slice_later_injection_overwrites_earlier() {
1093 let cb = Codebase::new();
1094
1095 cb.inject_stub_slice(crate::storage::StubSlice {
1096 functions: vec![make_fn("strlen", "phpstorm_version")],
1097 file: Some(Arc::from("phpstorm/standard.php")),
1098 ..Default::default()
1099 });
1100 assert_eq!(
1101 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1102 "phpstorm_version"
1103 );
1104
1105 cb.inject_stub_slice(crate::storage::StubSlice {
1106 functions: vec![make_fn("strlen", "custom_version")],
1107 file: Some(Arc::from("stubs/standard/basic.php")),
1108 ..Default::default()
1109 });
1110
1111 assert_eq!(
1112 cb.functions.get("strlen").unwrap().short_name.as_ref(),
1113 "custom_version",
1114 "custom stub must overwrite phpstorm stub"
1115 );
1116 assert_eq!(
1117 cb.symbol_to_file.get("strlen").unwrap().as_ref(),
1118 "stubs/standard/basic.php",
1119 "symbol_to_file must point to the overriding file"
1120 );
1121 }
1122
1123 #[test]
1124 fn inject_stub_slice_constants_not_added_to_symbol_to_file() {
1125 let cb = Codebase::new();
1126
1127 cb.inject_stub_slice(crate::storage::StubSlice {
1128 constants: vec![(Arc::from("PHP_EOL"), mir_types::Union::empty())],
1129 file: Some(Arc::from("stubs/core/constants.php")),
1130 ..Default::default()
1131 });
1132
1133 assert!(
1134 cb.constants.contains_key("PHP_EOL"),
1135 "constant must be registered in constants map"
1136 );
1137 assert!(
1138 !cb.symbol_to_file.contains_key("PHP_EOL"),
1139 "constants must not appear in symbol_to_file — go-to-definition is not supported for them"
1140 );
1141 }
1142
1143 #[test]
1144 fn remove_file_definitions_purges_injected_global_vars() {
1145 let cb = Codebase::new();
1146
1147 cb.inject_stub_slice(crate::storage::StubSlice {
1148 global_vars: vec![(Arc::from("db_connection"), mir_types::Union::empty())],
1149 file: Some(Arc::from("src/bootstrap.php")),
1150 ..Default::default()
1151 });
1152 assert!(
1153 cb.global_vars.contains_key("db_connection"),
1154 "global var must be registered after injection"
1155 );
1156
1157 cb.remove_file_definitions("src/bootstrap.php");
1158
1159 assert!(
1160 !cb.global_vars.contains_key("db_connection"),
1161 "global var must be removed when its defining file is removed"
1162 );
1163 }
1164
1165 #[test]
1166 fn inject_stub_slice_without_file_discards_global_vars() {
1167 let cb = Codebase::new();
1168
1169 cb.inject_stub_slice(crate::storage::StubSlice {
1170 global_vars: vec![(Arc::from("orphan_var"), mir_types::Union::empty())],
1171 file: None,
1172 ..Default::default()
1173 });
1174
1175 assert!(
1176 !cb.global_vars.contains_key("orphan_var"),
1177 "global_vars must not be registered when slice.file is None"
1178 );
1179 }
1180
1181 #[test]
1192 fn inject_stub_slice_populates_file_namespace() {
1193 let cb = Codebase::new();
1197 cb.inject_stub_slice(crate::storage::StubSlice {
1198 file: Some(Arc::from("src/Service.php")),
1199 namespace: Some(Arc::from("App\\Service")),
1200 ..Default::default()
1201 });
1202 assert_eq!(
1203 cb.file_namespaces
1204 .get("src/Service.php")
1205 .as_deref()
1206 .map(|s| s.as_str()),
1207 Some("App\\Service"),
1208 "file_namespaces must be populated when slice carries a namespace"
1209 );
1210
1211 let cb2 = Codebase::new();
1213 cb2.inject_stub_slice(crate::storage::StubSlice {
1214 file: Some(Arc::from("src/global.php")),
1215 namespace: None,
1216 ..Default::default()
1217 });
1218 assert!(
1219 cb2.file_namespaces.is_empty(),
1220 "file_namespaces must not be written when slice.namespace is None"
1221 );
1222 }
1223
1224 #[test]
1225 fn inject_stub_slice_populates_file_imports() {
1226 let cb = Codebase::new();
1230 let mut imports = std::collections::HashMap::new();
1231 imports.insert("Entity".to_string(), "App\\Model\\Entity".to_string());
1232 imports.insert(
1233 "Repo".to_string(),
1234 "App\\Repository\\EntityRepo".to_string(),
1235 );
1236 cb.inject_stub_slice(crate::storage::StubSlice {
1237 file: Some(Arc::from("src/Handler.php")),
1238 imports,
1239 ..Default::default()
1240 });
1241 let stored = cb.file_imports.get("src/Handler.php").unwrap();
1242 assert_eq!(
1243 stored.get("Entity").map(|s| s.as_str()),
1244 Some("App\\Model\\Entity")
1245 );
1246 assert_eq!(
1247 stored.get("Repo").map(|s| s.as_str()),
1248 Some("App\\Repository\\EntityRepo")
1249 );
1250
1251 let cb2 = Codebase::new();
1253 cb2.inject_stub_slice(crate::storage::StubSlice {
1254 file: Some(Arc::from("src/no_imports.php")),
1255 imports: std::collections::HashMap::new(),
1256 ..Default::default()
1257 });
1258 assert!(
1259 cb2.file_imports.is_empty(),
1260 "file_imports must not be written when slice.imports is empty"
1261 );
1262 }
1263
1264 #[test]
1265 fn inject_stub_slice_skips_namespace_and_imports_when_no_file() {
1266 let cb = Codebase::new();
1270 let mut imports = std::collections::HashMap::new();
1271 imports.insert("Foo".to_string(), "Bar\\Foo".to_string());
1272 cb.inject_stub_slice(crate::storage::StubSlice {
1273 file: None,
1274 namespace: Some(Arc::from("Bar")),
1275 imports,
1276 ..Default::default()
1277 });
1278 assert!(
1279 cb.file_namespaces.is_empty(),
1280 "file_namespaces must not be written when slice.file is None"
1281 );
1282 assert!(
1283 cb.file_imports.is_empty(),
1284 "file_imports must not be written when slice.file is None"
1285 );
1286 }
1287
1288 #[test]
1289 fn remove_file_definitions_purges_file_namespaces_and_imports() {
1290 let cb = Codebase::new();
1296 let mut imports = std::collections::HashMap::new();
1297 imports.insert("Entity".to_string(), "App\\Model\\Entity".to_string());
1298 cb.inject_stub_slice(crate::storage::StubSlice {
1299 file: Some(Arc::from("src/Handler.php")),
1300 namespace: Some(Arc::from("App\\Service")),
1301 imports,
1302 ..Default::default()
1303 });
1304 assert!(
1305 cb.file_namespaces.contains_key("src/Handler.php"),
1306 "setup: namespace must be present"
1307 );
1308 assert!(
1309 cb.file_imports.contains_key("src/Handler.php"),
1310 "setup: imports must be present"
1311 );
1312
1313 cb.remove_file_definitions("src/Handler.php");
1314
1315 assert!(
1316 !cb.file_namespaces.contains_key("src/Handler.php"),
1317 "file_namespaces entry must be removed when its defining file is removed"
1318 );
1319 assert!(
1320 !cb.file_imports.contains_key("src/Handler.php"),
1321 "file_imports entry must be removed when its defining file is removed"
1322 );
1323 }
1324}