1use std::collections::{HashMap, HashSet};
2
3use rustc_hash::FxHashMap;
4use std::sync::Arc;
5
6use mir_codebase::storage::{
7 Assertion, ConstantStorage, FnParam, FunctionStorage, Location, MethodStorage, PropertyStorage,
8 TemplateParam, Visibility,
9};
10use mir_codebase::StubSlice;
11use mir_issues::Issue;
12use mir_types::Union;
13
14#[salsa::db]
20pub trait MirDatabase: salsa::Database {
21 fn php_version_str(&self) -> Arc<str>;
23
24 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode>;
31
32 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode>;
34
35 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode>;
41
42 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode>;
44
45 fn lookup_class_constant_node(&self, fqcn: &str, const_name: &str)
47 -> Option<ClassConstantNode>;
48
49 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode>;
51
52 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode>;
55
56 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode>;
59
60 fn active_class_node_fqcns(&self) -> Vec<Arc<str>>;
64
65 fn active_function_node_fqns(&self) -> Vec<Arc<str>>;
68
69 fn file_namespace(&self, file: &str) -> Option<Arc<str>>;
71
72 fn file_imports(&self, file: &str) -> HashMap<String, String>;
74
75 fn global_var_type(&self, name: &str) -> Option<Union>;
77
78 fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)>;
80
81 fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>>;
83
84 fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>>;
86
87 fn record_reference_location(&self, loc: RefLoc);
89
90 fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]);
92
93 fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)>;
95
96 fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)>;
98
99 fn has_reference(&self, symbol: &str) -> bool;
101
102 fn clear_file_references(&self, file: &str);
104}
105
106#[salsa::input]
114pub struct SourceFile {
115 pub path: Arc<str>,
116 pub text: Arc<str>,
117}
118
119#[derive(Clone, Debug)]
125pub struct FileDefinitions {
126 pub slice: Arc<StubSlice>,
127 pub issues: Arc<Vec<Issue>>,
128}
129
130impl PartialEq for FileDefinitions {
131 fn eq(&self, other: &Self) -> bool {
132 Arc::ptr_eq(&self.slice, &other.slice) && Arc::ptr_eq(&self.issues, &other.issues)
133 }
134}
135
136unsafe impl salsa::Update for FileDefinitions {
137 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
138 unsafe { *old_pointer = new_value };
139 true
140 }
141}
142
143pub type ImplementsTypeArgs = Arc<[(Arc<str>, Arc<[Union]>)]>;
150
151#[salsa::input]
160pub struct ClassNode {
161 pub fqcn: Arc<str>,
162 pub active: bool,
165 pub is_interface: bool,
166 pub is_trait: bool,
171 pub is_enum: bool,
173 pub is_abstract: bool,
176 pub parent: Option<Arc<str>>,
178 pub interfaces: Arc<[Arc<str>]>,
180 pub traits: Arc<[Arc<str>]>,
183 pub extends: Arc<[Arc<str>]>,
185 pub template_params: Arc<[TemplateParam]>,
188 pub require_extends: Arc<[Arc<str>]>,
192 pub require_implements: Arc<[Arc<str>]>,
196 pub is_backed_enum: bool,
201 pub mixins: Arc<[Arc<str>]>,
206 pub deprecated: Option<Arc<str>>,
211 pub enum_scalar_type: Option<Union>,
216 pub is_final: bool,
220 pub is_readonly: bool,
223 pub location: Option<Location>,
228 pub extends_type_args: Arc<[Union]>,
231 pub implements_type_args: ImplementsTypeArgs,
234}
235
236#[derive(Debug, Clone, Copy)]
243pub struct ClassKind {
244 pub is_interface: bool,
245 pub is_trait: bool,
246 pub is_enum: bool,
247 pub is_abstract: bool,
248}
249
250pub fn class_kind_via_db(db: &dyn MirDatabase, fqcn: &str) -> Option<ClassKind> {
256 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
257 Some(ClassKind {
258 is_interface: node.is_interface(db),
259 is_trait: node.is_trait(db),
260 is_enum: node.is_enum(db),
261 is_abstract: node.is_abstract(db),
262 })
263}
264
265pub fn type_exists_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
271 db.lookup_class_node(fqcn).is_some_and(|n| n.active(db))
272}
273
274pub fn function_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
275 db.lookup_function_node(fqn).is_some_and(|n| n.active(db))
276}
277
278pub fn constant_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
279 db.lookup_global_constant_node(fqn)
280 .is_some_and(|n| n.active(db))
281}
282
283pub fn resolve_name_via_db(db: &dyn MirDatabase, file: &str, name: &str) -> String {
284 if name.starts_with('\\') {
285 return name.trim_start_matches('\\').to_string();
286 }
287
288 let lower = name.to_ascii_lowercase();
289 if matches!(lower.as_str(), "self" | "static" | "parent") {
290 return name.to_string();
291 }
292
293 if name.contains('\\') {
294 if let Some(imports) = (!name.starts_with('\\')).then(|| db.file_imports(file)) {
295 if let Some((first, rest)) = name.split_once('\\') {
296 if let Some(base) = imports.get(first) {
297 return format!("{base}\\{rest}");
298 }
299 }
300 }
301 if type_exists_via_db(db, name) {
302 return name.to_string();
303 }
304 if let Some(ns) = db.file_namespace(file) {
305 let qualified = format!("{}\\{}", ns, name);
306 if type_exists_via_db(db, &qualified) {
307 return qualified;
308 }
309 }
310 return name.to_string();
311 }
312
313 let imports = db.file_imports(file);
314 if let Some(fqcn) = imports.get(name) {
315 return fqcn.clone();
316 }
317 if let Some((_, fqcn)) = imports
318 .iter()
319 .find(|(alias, _)| alias.eq_ignore_ascii_case(name))
320 {
321 return fqcn.clone();
322 }
323 if let Some(ns) = db.file_namespace(file) {
324 let qualified = format!("{}\\{}", ns, name);
325 if type_exists_via_db(db, &qualified) {
326 return qualified;
327 }
328 }
329 name.to_string()
330}
331
332pub fn class_template_params_via_db(
337 db: &dyn MirDatabase,
338 fqcn: &str,
339) -> Option<Arc<[TemplateParam]>> {
340 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
341 Some(node.template_params(db))
342}
343
344pub fn inherited_template_bindings_via_db(
351 db: &dyn MirDatabase,
352 fqcn: &str,
353) -> std::collections::HashMap<Arc<str>, Union> {
354 let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
355 let mut visited: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
356 let mut current: Arc<str> = Arc::from(fqcn);
357 loop {
358 if !visited.insert(current.clone()) {
359 break;
360 }
361 let node = match db
362 .lookup_class_node(current.as_ref())
363 .filter(|n| n.active(db))
364 {
365 Some(n) => n,
366 None => break,
367 };
368 let parent = match node.parent(db) {
369 Some(p) => p,
370 None => break,
371 };
372 let extends_type_args = node.extends_type_args(db);
373 if !extends_type_args.is_empty() {
374 if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
375 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
376 bindings
377 .entry(tp.name.clone())
378 .or_insert_with(|| ty.clone());
379 }
380 }
381 }
382 current = parent;
383 }
384 bindings
385}
386
387#[salsa::input]
404pub struct FunctionNode {
405 pub fqn: Arc<str>,
406 pub short_name: Arc<str>,
407 pub active: bool,
408 pub params: Arc<[FnParam]>,
409 pub return_type: Option<Union>,
410 pub inferred_return_type: Option<Union>,
411 pub template_params: Arc<[TemplateParam]>,
412 pub assertions: Arc<[Assertion]>,
413 pub throws: Arc<[Arc<str>]>,
414 pub deprecated: Option<Arc<str>>,
415 pub is_pure: bool,
416 pub location: Option<Location>,
419}
420
421#[salsa::input]
438pub struct MethodNode {
439 pub fqcn: Arc<str>,
440 pub name: Arc<str>,
441 pub active: bool,
442 pub params: Arc<[FnParam]>,
443 pub return_type: Option<Union>,
444 pub inferred_return_type: Option<Union>,
445 pub template_params: Arc<[TemplateParam]>,
446 pub assertions: Arc<[Assertion]>,
447 pub throws: Arc<[Arc<str>]>,
448 pub deprecated: Option<Arc<str>>,
449 pub visibility: Visibility,
450 pub is_static: bool,
451 pub is_abstract: bool,
452 pub is_final: bool,
453 pub is_constructor: bool,
454 pub is_pure: bool,
455 pub location: Option<Location>,
458}
459
460#[salsa::input]
471pub struct PropertyNode {
472 pub fqcn: Arc<str>,
473 pub name: Arc<str>,
474 pub active: bool,
475 pub ty: Option<Union>,
476 pub visibility: Visibility,
477 pub is_static: bool,
478 pub is_readonly: bool,
479 pub location: Option<Location>,
480}
481
482#[salsa::input]
490pub struct ClassConstantNode {
491 pub fqcn: Arc<str>,
492 pub name: Arc<str>,
493 pub active: bool,
494 pub ty: Union,
495 pub visibility: Option<Visibility>,
496 pub is_final: bool,
497 pub location: Option<Location>,
501}
502
503#[salsa::input]
510pub struct GlobalConstantNode {
511 pub fqn: Arc<str>,
512 pub active: bool,
513 pub ty: Union,
514}
515
516#[derive(Clone, Debug, Default)]
525pub struct Ancestors(pub Vec<Arc<str>>);
526
527impl PartialEq for Ancestors {
528 fn eq(&self, other: &Self) -> bool {
529 self.0.len() == other.0.len()
530 && self
531 .0
532 .iter()
533 .zip(&other.0)
534 .all(|(a, b)| a.as_ref() == b.as_ref())
535 }
536}
537
538unsafe impl salsa::Update for Ancestors {
539 unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
540 let old = unsafe { &mut *old_ptr };
541 if *old == new_val {
542 return false;
543 }
544 *old = new_val;
545 true
546 }
547}
548
549fn ancestors_initial(_db: &dyn MirDatabase, _id: salsa::Id, _node: ClassNode) -> Ancestors {
554 Ancestors(vec![])
555}
556
557fn ancestors_cycle(
558 _db: &dyn MirDatabase,
559 _cycle: &salsa::Cycle,
560 _last: &Ancestors,
561 _value: Ancestors,
562 _node: ClassNode,
563) -> Ancestors {
564 Ancestors(vec![])
567}
568
569#[salsa::tracked(cycle_fn = ancestors_cycle, cycle_initial = ancestors_initial)]
579pub fn class_ancestors(db: &dyn MirDatabase, node: ClassNode) -> Ancestors {
580 if !node.active(db) {
581 return Ancestors(vec![]);
582 }
583 if node.is_enum(db) || node.is_trait(db) {
593 return Ancestors(vec![]);
594 }
595
596 let mut all: Vec<Arc<str>> = Vec::new();
597 let mut seen: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
598
599 let add =
600 |fqcn: &Arc<str>, all: &mut Vec<Arc<str>>, seen: &mut rustc_hash::FxHashSet<Arc<str>>| {
601 if seen.insert(fqcn.clone()) {
602 all.push(fqcn.clone());
603 }
604 };
605
606 if node.is_interface(db) {
607 for e in node.extends(db).iter() {
608 add(e, &mut all, &mut seen);
609 if let Some(parent_node) = db.lookup_class_node(e) {
610 for a in class_ancestors(db, parent_node).0 {
611 add(&a, &mut all, &mut seen);
612 }
613 }
614 }
615 } else {
616 if let Some(ref p) = node.parent(db) {
617 add(p, &mut all, &mut seen);
618 if let Some(parent_node) = db.lookup_class_node(p) {
619 for a in class_ancestors(db, parent_node).0 {
620 add(&a, &mut all, &mut seen);
621 }
622 }
623 }
624 for iface in node.interfaces(db).iter() {
625 add(iface, &mut all, &mut seen);
626 if let Some(iface_node) = db.lookup_class_node(iface) {
627 for a in class_ancestors(db, iface_node).0 {
628 add(&a, &mut all, &mut seen);
629 }
630 }
631 }
632 for t in node.traits(db).iter() {
633 add(t, &mut all, &mut seen);
634 }
635 }
636
637 Ancestors(all)
638}
639
640pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
648 let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
649 return false;
650 };
651 class_ancestors(db, node)
652 .0
653 .iter()
654 .any(|ancestor| !type_exists_via_db(db, ancestor))
655}
656
657pub fn method_is_concretely_implemented(
668 db: &dyn MirDatabase,
669 fqcn: &str,
670 method_name: &str,
671) -> bool {
672 let lower = method_name.to_lowercase();
673 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
674 return false;
675 };
676 if self_node.is_interface(db) {
679 return false;
680 }
681 if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
683 if !m.is_abstract(db) {
684 return true;
685 }
686 }
687 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
689 for t in self_node.traits(db).iter() {
690 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
691 return true;
692 }
693 }
694 for ancestor in class_ancestors(db, self_node).0.iter() {
697 let Some(anc_node) = db
698 .lookup_class_node(ancestor.as_ref())
699 .filter(|n| n.active(db))
700 else {
701 continue;
702 };
703 if anc_node.is_interface(db) {
704 continue;
705 }
706 if !anc_node.is_trait(db) {
708 if let Some(m) = db
709 .lookup_method_node(ancestor.as_ref(), &lower)
710 .filter(|m| m.active(db))
711 {
712 if !m.is_abstract(db) {
713 return true;
714 }
715 }
716 }
717 if anc_node.is_trait(db) {
720 if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
721 return true;
722 }
723 } else {
724 for t in anc_node.traits(db).iter() {
725 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
726 return true;
727 }
728 }
729 }
730 }
731 false
732}
733
734fn trait_provides_method(
738 db: &dyn MirDatabase,
739 trait_fqcn: &str,
740 method_lower: &str,
741 visited: &mut rustc_hash::FxHashSet<String>,
742) -> bool {
743 if !visited.insert(trait_fqcn.to_string()) {
744 return false;
745 }
746 if let Some(m) = db
747 .lookup_method_node(trait_fqcn, method_lower)
748 .filter(|m| m.active(db))
749 {
750 if !m.is_abstract(db) {
751 return true;
752 }
753 }
754 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
755 return false;
756 };
757 if !node.is_trait(db) {
758 return false;
759 }
760 for t in node.traits(db).iter() {
761 if trait_provides_method(db, t.as_ref(), method_lower, visited) {
762 return true;
763 }
764 }
765 false
766}
767
768pub fn lookup_method_in_chain(
783 db: &dyn MirDatabase,
784 fqcn: &str,
785 method_name: &str,
786) -> Option<MethodNode> {
787 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
788 lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
789}
790
791fn lookup_method_in_chain_inner(
792 db: &dyn MirDatabase,
793 fqcn: &str,
794 lower: &str,
795 visited_mixins: &mut rustc_hash::FxHashSet<String>,
796) -> Option<MethodNode> {
797 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
798
799 if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
801 return Some(node);
802 }
803 for m in self_node.mixins(db).iter() {
807 if visited_mixins.insert(m.to_string()) {
808 if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
809 {
810 return Some(node);
811 }
812 }
813 }
814 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
817 for t in self_node.traits(db).iter() {
818 if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
819 return Some(node);
820 }
821 }
822 for ancestor in class_ancestors(db, self_node).0.iter() {
824 if let Some(node) = db
825 .lookup_method_node(ancestor.as_ref(), lower)
826 .filter(|n| n.active(db))
827 {
828 return Some(node);
829 }
830 if let Some(anc_node) = db
831 .lookup_class_node(ancestor.as_ref())
832 .filter(|n| n.active(db))
833 {
834 if anc_node.is_trait(db) {
835 if let Some(node) =
836 trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
837 {
838 return Some(node);
839 }
840 } else {
841 for t in anc_node.traits(db).iter() {
842 if let Some(node) =
843 trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
844 {
845 return Some(node);
846 }
847 }
848 for m in anc_node.mixins(db).iter() {
849 if visited_mixins.insert(m.to_string()) {
850 if let Some(node) =
851 lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
852 {
853 return Some(node);
854 }
855 }
856 }
857 }
858 }
859 }
860 None
861}
862
863fn trait_provides_method_node(
867 db: &dyn MirDatabase,
868 trait_fqcn: &str,
869 method_lower: &str,
870 visited: &mut rustc_hash::FxHashSet<String>,
871) -> Option<MethodNode> {
872 if !visited.insert(trait_fqcn.to_string()) {
873 return None;
874 }
875 if let Some(node) = db
876 .lookup_method_node(trait_fqcn, method_lower)
877 .filter(|n| n.active(db))
878 {
879 return Some(node);
880 }
881 let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
882 if !node.is_trait(db) {
883 return None;
884 }
885 for t in node.traits(db).iter() {
886 if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
887 return Some(found);
888 }
889 }
890 None
891}
892
893pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
894 let lower = method_name.to_lowercase();
895 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
896 return false;
897 };
898 if db
900 .lookup_method_node(fqcn, &lower)
901 .is_some_and(|m| m.active(db))
902 {
903 return true;
904 }
905 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
907 for t in self_node.traits(db).iter() {
908 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
909 return true;
910 }
911 }
912 for ancestor in class_ancestors(db, self_node).0.iter() {
914 if db
915 .lookup_method_node(ancestor.as_ref(), &lower)
916 .is_some_and(|m| m.active(db))
917 {
918 return true;
919 }
920 if let Some(anc_node) = db
921 .lookup_class_node(ancestor.as_ref())
922 .filter(|n| n.active(db))
923 {
924 if anc_node.is_trait(db) {
925 if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
926 return true;
927 }
928 } else {
929 for t in anc_node.traits(db).iter() {
930 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
931 return true;
932 }
933 }
934 }
935 }
936 }
937 false
938}
939
940fn trait_declares_method(
944 db: &dyn MirDatabase,
945 trait_fqcn: &str,
946 method_lower: &str,
947 visited: &mut rustc_hash::FxHashSet<String>,
948) -> bool {
949 if !visited.insert(trait_fqcn.to_string()) {
950 return false;
951 }
952 if db
953 .lookup_method_node(trait_fqcn, method_lower)
954 .is_some_and(|m| m.active(db))
955 {
956 return true;
957 }
958 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
959 return false;
960 };
961 if !node.is_trait(db) {
962 return false;
963 }
964 for t in node.traits(db).iter() {
965 if trait_declares_method(db, t.as_ref(), method_lower, visited) {
966 return true;
967 }
968 }
969 false
970}
971
972pub fn lookup_property_in_chain(
982 db: &dyn MirDatabase,
983 fqcn: &str,
984 prop_name: &str,
985) -> Option<PropertyNode> {
986 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
987 lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
988}
989
990fn lookup_property_in_chain_inner(
991 db: &dyn MirDatabase,
992 fqcn: &str,
993 prop_name: &str,
994 visited_mixins: &mut rustc_hash::FxHashSet<String>,
995) -> Option<PropertyNode> {
996 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
997
998 if let Some(node) = db
1000 .lookup_property_node(fqcn, prop_name)
1001 .filter(|n| n.active(db))
1002 {
1003 return Some(node);
1004 }
1005 for m in self_node.mixins(db).iter() {
1008 if visited_mixins.insert(m.to_string()) {
1009 if let Some(node) =
1010 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
1011 {
1012 return Some(node);
1013 }
1014 }
1015 }
1016 for ancestor in class_ancestors(db, self_node).0.iter() {
1020 if let Some(node) = db
1021 .lookup_property_node(ancestor.as_ref(), prop_name)
1022 .filter(|n| n.active(db))
1023 {
1024 return Some(node);
1025 }
1026 if let Some(anc_node) = db
1027 .lookup_class_node(ancestor.as_ref())
1028 .filter(|n| n.active(db))
1029 {
1030 for m in anc_node.mixins(db).iter() {
1031 if visited_mixins.insert(m.to_string()) {
1032 if let Some(node) =
1033 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
1034 {
1035 return Some(node);
1036 }
1037 }
1038 }
1039 }
1040 }
1041 None
1042}
1043
1044pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
1054 if db
1055 .lookup_class_constant_node(fqcn, const_name)
1056 .is_some_and(|n| n.active(db))
1057 {
1058 return true;
1059 }
1060 let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
1061 return false;
1062 };
1063 for ancestor in class_ancestors(db, class_node).0.iter() {
1064 if db
1065 .lookup_class_constant_node(ancestor.as_ref(), const_name)
1066 .is_some_and(|n| n.active(db))
1067 {
1068 return true;
1069 }
1070 }
1071 false
1072}
1073
1074pub fn member_location_via_db(
1083 db: &dyn MirDatabase,
1084 fqcn: &str,
1085 member_name: &str,
1086) -> Option<Location> {
1087 if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
1088 if let Some(loc) = node.location(db) {
1089 return Some(loc);
1090 }
1091 }
1092 if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
1093 if let Some(loc) = node.location(db) {
1094 return Some(loc);
1095 }
1096 }
1097 if let Some(node) = db
1099 .lookup_class_constant_node(fqcn, member_name)
1100 .filter(|n| n.active(db))
1101 {
1102 if let Some(loc) = node.location(db) {
1103 return Some(loc);
1104 }
1105 }
1106 let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
1107 for ancestor in class_ancestors(db, class_node).0.iter() {
1108 if let Some(node) = db
1109 .lookup_class_constant_node(ancestor.as_ref(), member_name)
1110 .filter(|n| n.active(db))
1111 {
1112 if let Some(loc) = node.location(db) {
1113 return Some(loc);
1114 }
1115 }
1116 }
1117 None
1118}
1119
1120pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
1133 if child == ancestor {
1134 return true;
1135 }
1136 let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
1137 return false;
1138 };
1139 if node.is_enum(db) {
1140 if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
1144 return true;
1145 }
1146 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
1147 return true;
1148 }
1149 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
1150 return true;
1151 }
1152 return false;
1153 }
1154 class_ancestors(db, node)
1155 .0
1156 .iter()
1157 .any(|p| p.as_ref() == ancestor)
1158}
1159
1160#[salsa::tracked]
1165pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
1166 let path = file.path(db);
1167 let text = file.text(db);
1168
1169 let arena = bumpalo::Bump::new();
1170 let parsed = php_rs_parser::parse(&arena, &text);
1171
1172 let mut all_issues: Vec<Issue> = parsed
1173 .errors
1174 .iter()
1175 .map(|err| {
1176 Issue::new(
1177 mir_issues::IssueKind::ParseError {
1178 message: err.to_string(),
1179 },
1180 mir_issues::Location {
1181 file: path.clone(),
1182 line: 1,
1183 line_end: 1,
1184 col_start: 0,
1185 col_end: 0,
1186 },
1187 )
1188 })
1189 .collect();
1190
1191 let collector =
1192 crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
1193 let (slice, collector_issues) = collector.collect_slice(&parsed.program);
1194 all_issues.extend(collector_issues);
1195
1196 FileDefinitions {
1197 slice: Arc::new(slice),
1198 issues: Arc::new(all_issues),
1199 }
1200}
1201
1202type MemberRegistry<V> = Arc<FxHashMap<Arc<str>, FxHashMap<Arc<str>, V>>>;
1214type ReferenceLocations =
1215 Arc<std::sync::Mutex<FxHashMap<Arc<str>, Vec<(Arc<str>, u32, u16, u16)>>>>;
1216
1217#[salsa::db]
1218#[derive(Default, Clone)]
1219pub struct MirDb {
1220 storage: salsa::Storage<Self>,
1221 class_nodes: Arc<FxHashMap<Arc<str>, ClassNode>>,
1228 function_nodes: Arc<FxHashMap<Arc<str>, FunctionNode>>,
1230 method_nodes: MemberRegistry<MethodNode>,
1232 property_nodes: MemberRegistry<PropertyNode>,
1234 class_constant_nodes: MemberRegistry<ClassConstantNode>,
1236 global_constant_nodes: Arc<FxHashMap<Arc<str>, GlobalConstantNode>>,
1238 file_namespaces: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1240 file_imports: Arc<FxHashMap<Arc<str>, HashMap<String, String>>>,
1242 global_vars: Arc<FxHashMap<Arc<str>, Union>>,
1244 symbol_to_file: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1246 reference_locations: ReferenceLocations,
1248}
1249
1250#[salsa::db]
1251impl salsa::Database for MirDb {}
1252
1253#[salsa::db]
1254impl MirDatabase for MirDb {
1255 fn php_version_str(&self) -> Arc<str> {
1256 Arc::from("8.2")
1257 }
1258
1259 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
1260 self.class_nodes.get(fqcn).copied()
1261 }
1262
1263 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
1264 self.function_nodes.get(fqn).copied()
1265 }
1266
1267 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
1268 self.method_nodes
1269 .get(fqcn)
1270 .and_then(|m| m.get(method_name_lower).copied())
1271 }
1272
1273 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
1274 self.property_nodes
1275 .get(fqcn)
1276 .and_then(|m| m.get(prop_name).copied())
1277 }
1278
1279 fn lookup_class_constant_node(
1280 &self,
1281 fqcn: &str,
1282 const_name: &str,
1283 ) -> Option<ClassConstantNode> {
1284 self.class_constant_nodes
1285 .get(fqcn)
1286 .and_then(|m| m.get(const_name).copied())
1287 }
1288
1289 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
1290 self.global_constant_nodes.get(fqn).copied()
1291 }
1292
1293 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
1294 self.method_nodes
1295 .get(fqcn)
1296 .map(|m| m.values().copied().collect())
1297 .unwrap_or_default()
1298 }
1299
1300 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
1301 self.property_nodes
1302 .get(fqcn)
1303 .map(|m| m.values().copied().collect())
1304 .unwrap_or_default()
1305 }
1306
1307 fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
1308 self.class_nodes
1309 .iter()
1310 .filter_map(|(fqcn, node)| {
1311 if node.active(self) {
1312 Some(fqcn.clone())
1313 } else {
1314 None
1315 }
1316 })
1317 .collect()
1318 }
1319
1320 fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
1321 self.function_nodes
1322 .iter()
1323 .filter_map(|(fqn, node)| {
1324 if node.active(self) {
1325 Some(fqn.clone())
1326 } else {
1327 None
1328 }
1329 })
1330 .collect()
1331 }
1332
1333 fn file_namespace(&self, file: &str) -> Option<Arc<str>> {
1334 self.file_namespaces.get(file).cloned()
1335 }
1336
1337 fn file_imports(&self, file: &str) -> HashMap<String, String> {
1338 self.file_imports.get(file).cloned().unwrap_or_default()
1339 }
1340
1341 fn global_var_type(&self, name: &str) -> Option<Union> {
1342 self.global_vars.get(name).cloned()
1343 }
1344
1345 fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)> {
1346 self.file_imports
1347 .iter()
1348 .map(|(file, imports)| (file.clone(), imports.clone()))
1349 .collect()
1350 }
1351
1352 fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>> {
1353 self.symbol_to_file.get(symbol).cloned()
1354 }
1355
1356 fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>> {
1357 self.symbol_to_file
1358 .iter()
1359 .filter_map(|(sym, defining_file)| {
1360 if defining_file.as_ref() == file {
1361 Some(sym.clone())
1362 } else {
1363 None
1364 }
1365 })
1366 .collect()
1367 }
1368
1369 fn record_reference_location(&self, loc: RefLoc) {
1370 let mut refs = self
1371 .reference_locations
1372 .lock()
1373 .expect("reference lock poisoned");
1374 let entry = refs.entry(loc.symbol_key).or_default();
1375 let tuple = (loc.file, loc.line, loc.col_start, loc.col_end);
1376 if !entry.iter().any(|existing| existing == &tuple) {
1377 entry.push(tuple);
1378 }
1379 }
1380
1381 fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
1382 for (symbol, line, col_start, col_end) in locs {
1383 self.record_reference_location(RefLoc {
1384 symbol_key: Arc::from(symbol.as_str()),
1385 file: file.clone(),
1386 line: *line,
1387 col_start: *col_start,
1388 col_end: *col_end,
1389 });
1390 }
1391 }
1392
1393 fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1394 let refs = self
1395 .reference_locations
1396 .lock()
1397 .expect("reference lock poisoned");
1398 let mut out = Vec::new();
1399 for (symbol, locs) in refs.iter() {
1400 for (loc_file, line, col_start, col_end) in locs {
1401 if loc_file.as_ref() == file {
1402 out.push((symbol.clone(), *line, *col_start, *col_end));
1403 }
1404 }
1405 }
1406 out
1407 }
1408
1409 fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1410 let refs = self
1411 .reference_locations
1412 .lock()
1413 .expect("reference lock poisoned");
1414 refs.get(symbol).cloned().unwrap_or_default()
1415 }
1416
1417 fn has_reference(&self, symbol: &str) -> bool {
1418 let refs = self
1419 .reference_locations
1420 .lock()
1421 .expect("reference lock poisoned");
1422 refs.get(symbol).is_some_and(|locs| !locs.is_empty())
1423 }
1424
1425 fn clear_file_references(&self, file: &str) {
1426 let mut refs = self
1427 .reference_locations
1428 .lock()
1429 .expect("reference lock poisoned");
1430 for locs in refs.values_mut() {
1431 locs.retain(|(loc_file, _, _, _)| loc_file.as_ref() != file);
1432 }
1433 }
1434}
1435
1436#[derive(Default)]
1452#[allow(clippy::type_complexity)]
1453pub struct InferredReturnTypes {
1454 functions: std::sync::Mutex<Vec<(Arc<str>, Union)>>,
1456 methods: std::sync::Mutex<Vec<(Arc<str>, Arc<str>, Union)>>,
1460}
1461
1462impl InferredReturnTypes {
1463 pub fn new() -> Self {
1464 Self::default()
1465 }
1466
1467 pub fn push_function(&self, fqn: Arc<str>, inferred: Union) {
1468 if let Ok(mut g) = self.functions.lock() {
1469 g.push((fqn, inferred));
1470 }
1471 }
1472
1473 pub fn push_method(&self, fqcn: Arc<str>, name: Arc<str>, inferred: Union) {
1474 if let Ok(mut g) = self.methods.lock() {
1475 g.push((fqcn, name, inferred));
1476 }
1477 }
1478}
1479
1480#[derive(Debug, Clone, Default)]
1488pub struct ClassNodeFields {
1489 pub fqcn: Arc<str>,
1490 pub is_interface: bool,
1491 pub is_trait: bool,
1492 pub is_enum: bool,
1493 pub is_abstract: bool,
1494 pub parent: Option<Arc<str>>,
1495 pub interfaces: Arc<[Arc<str>]>,
1496 pub traits: Arc<[Arc<str>]>,
1497 pub extends: Arc<[Arc<str>]>,
1498 pub template_params: Arc<[TemplateParam]>,
1499 pub require_extends: Arc<[Arc<str>]>,
1500 pub require_implements: Arc<[Arc<str>]>,
1501 pub is_backed_enum: bool,
1502 pub mixins: Arc<[Arc<str>]>,
1503 pub deprecated: Option<Arc<str>>,
1504 pub enum_scalar_type: Option<Union>,
1505 pub is_final: bool,
1506 pub is_readonly: bool,
1507 pub location: Option<Location>,
1508 pub extends_type_args: Arc<[Union]>,
1509 pub implements_type_args: ImplementsTypeArgs,
1510}
1511
1512impl ClassNodeFields {
1513 pub fn for_class(fqcn: Arc<str>) -> Self {
1514 Self {
1515 fqcn,
1516 ..Self::default()
1517 }
1518 }
1519
1520 pub fn for_interface(fqcn: Arc<str>) -> Self {
1521 Self {
1522 fqcn,
1523 is_interface: true,
1524 ..Self::default()
1525 }
1526 }
1527
1528 pub fn for_trait(fqcn: Arc<str>) -> Self {
1529 Self {
1530 fqcn,
1531 is_trait: true,
1532 ..Self::default()
1533 }
1534 }
1535
1536 pub fn for_enum(fqcn: Arc<str>) -> Self {
1537 Self {
1538 fqcn,
1539 is_enum: true,
1540 ..Self::default()
1541 }
1542 }
1543}
1544
1545impl MirDb {
1546 pub fn remove_file_definitions(&mut self, file: &str) {
1547 let symbols = self.symbols_defined_in_file(file);
1548 for symbol in &symbols {
1549 self.deactivate_class_node(symbol);
1550 self.deactivate_function_node(symbol);
1551 self.deactivate_class_methods(symbol);
1552 self.deactivate_class_properties(symbol);
1553 self.deactivate_class_constants(symbol);
1554 self.deactivate_global_constant_node(symbol);
1555 }
1556 let symbol_set: HashSet<Arc<str>> = symbols.into_iter().collect();
1557 Arc::make_mut(&mut self.symbol_to_file).retain(|sym, defining_file| {
1558 defining_file.as_ref() != file && !symbol_set.contains(sym)
1559 });
1560 Arc::make_mut(&mut self.file_namespaces).retain(|path, _| path.as_ref() != file);
1561 Arc::make_mut(&mut self.file_imports).retain(|path, _| path.as_ref() != file);
1562 Arc::make_mut(&mut self.global_vars).retain(|name, _| !symbol_set.contains(name));
1563 self.clear_file_references(file);
1564 }
1565
1566 pub fn type_count(&self) -> usize {
1567 self.class_nodes
1568 .values()
1569 .filter(|node| node.active(self))
1570 .count()
1571 }
1572
1573 pub fn function_count(&self) -> usize {
1574 self.function_nodes
1575 .values()
1576 .filter(|node| node.active(self))
1577 .count()
1578 }
1579
1580 pub fn constant_count(&self) -> usize {
1581 self.global_constant_nodes
1582 .values()
1583 .filter(|node| node.active(self))
1584 .count()
1585 }
1586
1587 pub fn ingest_stub_slice(&mut self, slice: &StubSlice) {
1593 use std::collections::HashSet;
1594
1595 if let Some(file) = &slice.file {
1596 if let Some(namespace) = &slice.namespace {
1597 Arc::make_mut(&mut self.file_namespaces).insert(file.clone(), namespace.clone());
1598 }
1599 if !slice.imports.is_empty() {
1600 Arc::make_mut(&mut self.file_imports).insert(file.clone(), slice.imports.clone());
1601 }
1602 for (name, _) in &slice.global_vars {
1603 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1604 Arc::make_mut(&mut self.symbol_to_file)
1605 .insert(Arc::from(global_name), file.clone());
1606 }
1607 }
1608 for (name, ty) in &slice.global_vars {
1609 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1610 Arc::make_mut(&mut self.global_vars).insert(Arc::from(global_name), ty.clone());
1611 }
1612
1613 for cls in &slice.classes {
1614 if let Some(file) = &slice.file {
1615 Arc::make_mut(&mut self.symbol_to_file).insert(cls.fqcn.clone(), file.clone());
1616 }
1617 self.upsert_class_node(ClassNodeFields {
1618 is_abstract: cls.is_abstract,
1619 parent: cls.parent.clone(),
1620 interfaces: Arc::from(cls.interfaces.as_slice()),
1621 traits: Arc::from(cls.traits.as_slice()),
1622 template_params: Arc::from(cls.template_params.as_slice()),
1623 mixins: Arc::from(cls.mixins.as_slice()),
1624 deprecated: cls.deprecated.clone(),
1625 is_final: cls.is_final,
1626 is_readonly: cls.is_readonly,
1627 location: cls.location.clone(),
1628 extends_type_args: Arc::from(cls.extends_type_args.as_slice()),
1629 implements_type_args: Arc::from(
1630 cls.implements_type_args
1631 .iter()
1632 .map(|(iface, args)| (iface.clone(), Arc::from(args.as_slice())))
1633 .collect::<Vec<_>>(),
1634 ),
1635 ..ClassNodeFields::for_class(cls.fqcn.clone())
1636 });
1637 if self.method_nodes.contains_key(cls.fqcn.as_ref()) {
1638 let method_keep: HashSet<&str> =
1639 cls.own_methods.keys().map(|m| m.as_ref()).collect();
1640 self.prune_class_methods(&cls.fqcn, &method_keep);
1641 }
1642 for method in cls.own_methods.values() {
1643 self.upsert_method_node(method.as_ref());
1644 }
1645 if self.property_nodes.contains_key(cls.fqcn.as_ref()) {
1646 let prop_keep: HashSet<&str> =
1647 cls.own_properties.keys().map(|p| p.as_ref()).collect();
1648 self.prune_class_properties(&cls.fqcn, &prop_keep);
1649 }
1650 for prop in cls.own_properties.values() {
1651 self.upsert_property_node(&cls.fqcn, prop);
1652 }
1653 if self.class_constant_nodes.contains_key(cls.fqcn.as_ref()) {
1654 let const_keep: HashSet<&str> =
1655 cls.own_constants.keys().map(|c| c.as_ref()).collect();
1656 self.prune_class_constants(&cls.fqcn, &const_keep);
1657 }
1658 for constant in cls.own_constants.values() {
1659 self.upsert_class_constant_node(&cls.fqcn, constant);
1660 }
1661 }
1662
1663 for iface in &slice.interfaces {
1664 if let Some(file) = &slice.file {
1665 Arc::make_mut(&mut self.symbol_to_file).insert(iface.fqcn.clone(), file.clone());
1666 }
1667 self.upsert_class_node(ClassNodeFields {
1668 extends: Arc::from(iface.extends.as_slice()),
1669 template_params: Arc::from(iface.template_params.as_slice()),
1670 location: iface.location.clone(),
1671 ..ClassNodeFields::for_interface(iface.fqcn.clone())
1672 });
1673 if self.method_nodes.contains_key(iface.fqcn.as_ref()) {
1674 let method_keep: HashSet<&str> =
1675 iface.own_methods.keys().map(|m| m.as_ref()).collect();
1676 self.prune_class_methods(&iface.fqcn, &method_keep);
1677 }
1678 for method in iface.own_methods.values() {
1679 self.upsert_method_node(method.as_ref());
1680 }
1681 if self.class_constant_nodes.contains_key(iface.fqcn.as_ref()) {
1682 let const_keep: HashSet<&str> =
1683 iface.own_constants.keys().map(|c| c.as_ref()).collect();
1684 self.prune_class_constants(&iface.fqcn, &const_keep);
1685 }
1686 for constant in iface.own_constants.values() {
1687 self.upsert_class_constant_node(&iface.fqcn, constant);
1688 }
1689 }
1690
1691 for tr in &slice.traits {
1692 if let Some(file) = &slice.file {
1693 Arc::make_mut(&mut self.symbol_to_file).insert(tr.fqcn.clone(), file.clone());
1694 }
1695 self.upsert_class_node(ClassNodeFields {
1696 traits: Arc::from(tr.traits.as_slice()),
1697 template_params: Arc::from(tr.template_params.as_slice()),
1698 require_extends: Arc::from(tr.require_extends.as_slice()),
1699 require_implements: Arc::from(tr.require_implements.as_slice()),
1700 location: tr.location.clone(),
1701 ..ClassNodeFields::for_trait(tr.fqcn.clone())
1702 });
1703 if self.method_nodes.contains_key(tr.fqcn.as_ref()) {
1704 let method_keep: HashSet<&str> =
1705 tr.own_methods.keys().map(|m| m.as_ref()).collect();
1706 self.prune_class_methods(&tr.fqcn, &method_keep);
1707 }
1708 for method in tr.own_methods.values() {
1709 self.upsert_method_node(method.as_ref());
1710 }
1711 if self.property_nodes.contains_key(tr.fqcn.as_ref()) {
1712 let prop_keep: HashSet<&str> =
1713 tr.own_properties.keys().map(|p| p.as_ref()).collect();
1714 self.prune_class_properties(&tr.fqcn, &prop_keep);
1715 }
1716 for prop in tr.own_properties.values() {
1717 self.upsert_property_node(&tr.fqcn, prop);
1718 }
1719 if self.class_constant_nodes.contains_key(tr.fqcn.as_ref()) {
1720 let const_keep: HashSet<&str> =
1721 tr.own_constants.keys().map(|c| c.as_ref()).collect();
1722 self.prune_class_constants(&tr.fqcn, &const_keep);
1723 }
1724 for constant in tr.own_constants.values() {
1725 self.upsert_class_constant_node(&tr.fqcn, constant);
1726 }
1727 }
1728
1729 for en in &slice.enums {
1730 if let Some(file) = &slice.file {
1731 Arc::make_mut(&mut self.symbol_to_file).insert(en.fqcn.clone(), file.clone());
1732 }
1733 self.upsert_class_node(ClassNodeFields {
1734 interfaces: Arc::from(en.interfaces.as_slice()),
1735 is_backed_enum: en.scalar_type.is_some(),
1736 enum_scalar_type: en.scalar_type.clone(),
1737 location: en.location.clone(),
1738 ..ClassNodeFields::for_enum(en.fqcn.clone())
1739 });
1740 if self.method_nodes.contains_key(en.fqcn.as_ref()) {
1741 let mut method_keep: HashSet<&str> =
1742 en.own_methods.keys().map(|m| m.as_ref()).collect();
1743 method_keep.insert("cases");
1744 if en.scalar_type.is_some() {
1745 method_keep.insert("from");
1746 method_keep.insert("tryfrom");
1747 }
1748 self.prune_class_methods(&en.fqcn, &method_keep);
1749 }
1750 for method in en.own_methods.values() {
1751 self.upsert_method_node(method.as_ref());
1752 }
1753 let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
1754 fqcn: en.fqcn.clone(),
1755 name: Arc::from(name),
1756 params: vec![],
1757 return_type: Some(Union::mixed()),
1758 inferred_return_type: None,
1759 visibility: Visibility::Public,
1760 is_static: true,
1761 is_abstract: false,
1762 is_constructor: false,
1763 template_params: vec![],
1764 assertions: vec![],
1765 throws: vec![],
1766 is_final: false,
1767 is_internal: false,
1768 is_pure: false,
1769 deprecated: None,
1770 location: None,
1771 };
1772 let already = |name: &str| {
1773 en.own_methods
1774 .keys()
1775 .any(|k| k.as_ref().eq_ignore_ascii_case(name))
1776 };
1777 if !already("cases") {
1778 self.upsert_method_node(&synth_method("cases"));
1779 }
1780 if en.scalar_type.is_some() {
1781 if !already("from") {
1782 self.upsert_method_node(&synth_method("from"));
1783 }
1784 if !already("tryFrom") {
1785 self.upsert_method_node(&synth_method("tryFrom"));
1786 }
1787 }
1788 if self.class_constant_nodes.contains_key(en.fqcn.as_ref()) {
1789 let mut const_keep: HashSet<&str> =
1790 en.own_constants.keys().map(|c| c.as_ref()).collect();
1791 for case in en.cases.values() {
1792 const_keep.insert(case.name.as_ref());
1793 }
1794 self.prune_class_constants(&en.fqcn, &const_keep);
1795 }
1796 for constant in en.own_constants.values() {
1797 self.upsert_class_constant_node(&en.fqcn, constant);
1798 }
1799 for case in en.cases.values() {
1800 let case_const = ConstantStorage {
1801 name: case.name.clone(),
1802 ty: mir_types::Union::mixed(),
1803 visibility: None,
1804 is_final: false,
1805 location: case.location.clone(),
1806 };
1807 self.upsert_class_constant_node(&en.fqcn, &case_const);
1808 }
1809 }
1810
1811 for func in &slice.functions {
1812 if let Some(file) = &slice.file {
1813 Arc::make_mut(&mut self.symbol_to_file).insert(func.fqn.clone(), file.clone());
1814 }
1815 self.upsert_function_node(func);
1816 }
1817 for (fqn, ty) in &slice.constants {
1818 self.upsert_global_constant_node(fqn.clone(), ty.clone());
1819 }
1820 }
1821
1822 #[allow(clippy::too_many_arguments)]
1827 pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
1828 use salsa::Setter as _;
1829 let ClassNodeFields {
1830 fqcn,
1831 is_interface,
1832 is_trait,
1833 is_enum,
1834 is_abstract,
1835 parent,
1836 interfaces,
1837 traits,
1838 extends,
1839 template_params,
1840 require_extends,
1841 require_implements,
1842 is_backed_enum,
1843 mixins,
1844 deprecated,
1845 enum_scalar_type,
1846 is_final,
1847 is_readonly,
1848 location,
1849 extends_type_args,
1850 implements_type_args,
1851 } = fields;
1852 if let Some(&node) = self.class_nodes.get(&fqcn) {
1853 if node.active(self)
1866 && node.is_interface(self) == is_interface
1867 && node.is_trait(self) == is_trait
1868 && node.is_enum(self) == is_enum
1869 && node.is_abstract(self) == is_abstract
1870 && node.is_backed_enum(self) == is_backed_enum
1871 && node.parent(self) == parent
1872 && *node.interfaces(self) == *interfaces
1873 && *node.traits(self) == *traits
1874 && *node.extends(self) == *extends
1875 && *node.template_params(self) == *template_params
1876 && *node.require_extends(self) == *require_extends
1877 && *node.require_implements(self) == *require_implements
1878 && *node.mixins(self) == *mixins
1879 && node.deprecated(self) == deprecated
1880 && node.enum_scalar_type(self) == enum_scalar_type
1881 && node.is_final(self) == is_final
1882 && node.is_readonly(self) == is_readonly
1883 && node.location(self) == location
1884 && *node.extends_type_args(self) == *extends_type_args
1885 && *node.implements_type_args(self) == *implements_type_args
1886 {
1887 return node;
1888 }
1889 node.set_active(self).to(true);
1890 node.set_is_interface(self).to(is_interface);
1891 node.set_is_trait(self).to(is_trait);
1892 node.set_is_enum(self).to(is_enum);
1893 node.set_is_abstract(self).to(is_abstract);
1894 node.set_parent(self).to(parent);
1895 node.set_interfaces(self).to(interfaces);
1896 node.set_traits(self).to(traits);
1897 node.set_extends(self).to(extends);
1898 node.set_template_params(self).to(template_params);
1899 node.set_require_extends(self).to(require_extends);
1900 node.set_require_implements(self).to(require_implements);
1901 node.set_is_backed_enum(self).to(is_backed_enum);
1902 node.set_mixins(self).to(mixins);
1903 node.set_deprecated(self).to(deprecated);
1904 node.set_enum_scalar_type(self).to(enum_scalar_type);
1905 node.set_is_final(self).to(is_final);
1906 node.set_is_readonly(self).to(is_readonly);
1907 node.set_location(self).to(location);
1908 node.set_extends_type_args(self).to(extends_type_args);
1909 node.set_implements_type_args(self).to(implements_type_args);
1910 node
1911 } else {
1912 let node = ClassNode::new(
1913 self,
1914 fqcn.clone(),
1915 true,
1916 is_interface,
1917 is_trait,
1918 is_enum,
1919 is_abstract,
1920 parent,
1921 interfaces,
1922 traits,
1923 extends,
1924 template_params,
1925 require_extends,
1926 require_implements,
1927 is_backed_enum,
1928 mixins,
1929 deprecated,
1930 enum_scalar_type,
1931 is_final,
1932 is_readonly,
1933 location,
1934 extends_type_args,
1935 implements_type_args,
1936 );
1937 Arc::make_mut(&mut self.class_nodes).insert(fqcn, node);
1938 node
1939 }
1940 }
1941
1942 pub fn deactivate_class_node(&mut self, fqcn: &str) {
1947 use salsa::Setter as _;
1948 if let Some(&node) = self.class_nodes.get(fqcn) {
1949 node.set_active(self).to(false);
1950 }
1951 }
1952
1953 pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
1955 use salsa::Setter as _;
1956 let fqn = &storage.fqn;
1957 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1958 if node.active(self)
1964 && node.short_name(self) == storage.short_name
1965 && node.is_pure(self) == storage.is_pure
1966 && node.deprecated(self) == storage.deprecated
1967 && node.return_type(self) == storage.return_type
1968 && node.location(self) == storage.location
1969 && *node.params(self) == *storage.params.as_slice()
1970 && *node.template_params(self) == *storage.template_params.as_slice()
1971 && *node.assertions(self) == *storage.assertions.as_slice()
1972 && *node.throws(self) == *storage.throws.as_slice()
1973 {
1974 return node;
1975 }
1976 node.set_active(self).to(true);
1977 node.set_short_name(self).to(storage.short_name.clone());
1978 node.set_params(self)
1979 .to(Arc::from(storage.params.as_slice()));
1980 node.set_return_type(self).to(storage.return_type.clone());
1981 node.set_template_params(self)
1982 .to(Arc::from(storage.template_params.as_slice()));
1983 node.set_assertions(self)
1984 .to(Arc::from(storage.assertions.as_slice()));
1985 node.set_throws(self)
1986 .to(Arc::from(storage.throws.as_slice()));
1987 node.set_deprecated(self).to(storage.deprecated.clone());
1988 node.set_is_pure(self).to(storage.is_pure);
1989 node.set_location(self).to(storage.location.clone());
1990 node
1991 } else {
1992 let node = FunctionNode::new(
1993 self,
1994 fqn.clone(),
1995 storage.short_name.clone(),
1996 true,
1997 Arc::from(storage.params.as_slice()),
1998 storage.return_type.clone(),
1999 storage.inferred_return_type.clone(),
2000 Arc::from(storage.template_params.as_slice()),
2001 Arc::from(storage.assertions.as_slice()),
2002 Arc::from(storage.throws.as_slice()),
2003 storage.deprecated.clone(),
2004 storage.is_pure,
2005 storage.location.clone(),
2006 );
2007 Arc::make_mut(&mut self.function_nodes).insert(fqn.clone(), node);
2008 node
2009 }
2010 }
2011
2012 pub fn commit_inferred_return_types(&mut self, buf: &InferredReturnTypes) {
2024 use salsa::Setter as _;
2025 let funcs = std::mem::take(&mut *buf.functions.lock().expect("inferred buffer poisoned"));
2026 for (fqn, inferred) in funcs {
2027 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
2028 if !node.active(self) {
2029 continue;
2030 }
2031 let new = Some(inferred);
2032 if node.inferred_return_type(self) == new {
2033 continue;
2034 }
2035 node.set_inferred_return_type(self).to(new);
2036 }
2037 }
2038 let methods = std::mem::take(&mut *buf.methods.lock().expect("inferred buffer poisoned"));
2039 for (fqcn, name, inferred) in methods {
2040 let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
2041 name.clone()
2042 } else {
2043 Arc::from(name.to_lowercase().as_str())
2044 };
2045 let node = self
2046 .method_nodes
2047 .get(fqcn.as_ref())
2048 .and_then(|m| m.get(&name_lower))
2049 .copied();
2050 if let Some(node) = node {
2051 if !node.active(self) {
2052 continue;
2053 }
2054 let new = Some(inferred);
2055 if node.inferred_return_type(self) == new {
2056 continue;
2057 }
2058 node.set_inferred_return_type(self).to(new);
2059 }
2060 }
2061 }
2062
2063 pub fn deactivate_function_node(&mut self, fqn: &str) {
2065 use salsa::Setter as _;
2066 if let Some(&node) = self.function_nodes.get(fqn) {
2067 node.set_active(self).to(false);
2068 }
2069 }
2070
2071 pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
2073 use salsa::Setter as _;
2074 let fqcn = &storage.fqcn;
2075 let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
2076 let existing = self
2079 .method_nodes
2080 .get(fqcn.as_ref())
2081 .and_then(|m| m.get(&name_lower))
2082 .copied();
2083 if let Some(node) = existing {
2084 if node.active(self)
2088 && node.visibility(self) == storage.visibility
2089 && node.is_static(self) == storage.is_static
2090 && node.is_abstract(self) == storage.is_abstract
2091 && node.is_final(self) == storage.is_final
2092 && node.is_constructor(self) == storage.is_constructor
2093 && node.is_pure(self) == storage.is_pure
2094 && node.deprecated(self) == storage.deprecated
2095 && node.return_type(self) == storage.return_type
2096 && node.location(self) == storage.location
2097 && *node.params(self) == *storage.params.as_slice()
2098 && *node.template_params(self) == *storage.template_params.as_slice()
2099 && *node.assertions(self) == *storage.assertions.as_slice()
2100 && *node.throws(self) == *storage.throws.as_slice()
2101 {
2102 return node;
2103 }
2104 node.set_active(self).to(true);
2105 node.set_params(self)
2106 .to(Arc::from(storage.params.as_slice()));
2107 node.set_return_type(self).to(storage.return_type.clone());
2108 node.set_template_params(self)
2109 .to(Arc::from(storage.template_params.as_slice()));
2110 node.set_assertions(self)
2111 .to(Arc::from(storage.assertions.as_slice()));
2112 node.set_throws(self)
2113 .to(Arc::from(storage.throws.as_slice()));
2114 node.set_deprecated(self).to(storage.deprecated.clone());
2115 node.set_visibility(self).to(storage.visibility);
2116 node.set_is_static(self).to(storage.is_static);
2117 node.set_is_abstract(self).to(storage.is_abstract);
2118 node.set_is_final(self).to(storage.is_final);
2119 node.set_is_constructor(self).to(storage.is_constructor);
2120 node.set_is_pure(self).to(storage.is_pure);
2121 node.set_location(self).to(storage.location.clone());
2122 node
2123 } else {
2124 let node = MethodNode::new(
2126 self,
2127 fqcn.clone(),
2128 storage.name.clone(),
2129 true,
2130 Arc::from(storage.params.as_slice()),
2131 storage.return_type.clone(),
2132 storage.inferred_return_type.clone(),
2133 Arc::from(storage.template_params.as_slice()),
2134 Arc::from(storage.assertions.as_slice()),
2135 Arc::from(storage.throws.as_slice()),
2136 storage.deprecated.clone(),
2137 storage.visibility,
2138 storage.is_static,
2139 storage.is_abstract,
2140 storage.is_final,
2141 storage.is_constructor,
2142 storage.is_pure,
2143 storage.location.clone(),
2144 );
2145 Arc::make_mut(&mut self.method_nodes)
2146 .entry(fqcn.clone())
2147 .or_default()
2148 .insert(name_lower, node);
2149 node
2150 }
2151 }
2152
2153 pub fn deactivate_class_methods(&mut self, fqcn: &str) {
2155 use salsa::Setter as _;
2156 let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
2157 Some(methods) => methods.values().copied().collect(),
2158 None => return,
2159 };
2160 for node in nodes {
2161 node.set_active(self).to(false);
2162 }
2163 }
2164
2165 pub fn prune_class_methods<T>(&mut self, fqcn: &str, keep_lower: &std::collections::HashSet<T>)
2171 where
2172 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2173 {
2174 use salsa::Setter as _;
2175 let candidates: Vec<MethodNode> = self
2176 .method_nodes
2177 .get(fqcn)
2178 .map(|m| {
2179 m.iter()
2180 .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
2181 .map(|(_, n)| *n)
2182 .collect()
2183 })
2184 .unwrap_or_default();
2185 for node in candidates {
2186 if node.active(self) {
2187 node.set_active(self).to(false);
2188 }
2189 }
2190 }
2191
2192 pub fn prune_class_properties<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2194 where
2195 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2196 {
2197 use salsa::Setter as _;
2198 let candidates: Vec<PropertyNode> = self
2199 .property_nodes
2200 .get(fqcn)
2201 .map(|m| {
2202 m.iter()
2203 .filter(|(k, _)| !keep.contains(k.as_ref()))
2204 .map(|(_, n)| *n)
2205 .collect()
2206 })
2207 .unwrap_or_default();
2208 for node in candidates {
2209 if node.active(self) {
2210 node.set_active(self).to(false);
2211 }
2212 }
2213 }
2214
2215 pub fn prune_class_constants<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2217 where
2218 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2219 {
2220 use salsa::Setter as _;
2221 let candidates: Vec<ClassConstantNode> = self
2222 .class_constant_nodes
2223 .get(fqcn)
2224 .map(|m| {
2225 m.iter()
2226 .filter(|(k, _)| !keep.contains(k.as_ref()))
2227 .map(|(_, n)| *n)
2228 .collect()
2229 })
2230 .unwrap_or_default();
2231 for node in candidates {
2232 if node.active(self) {
2233 node.set_active(self).to(false);
2234 }
2235 }
2236 }
2237
2238 pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
2240 use salsa::Setter as _;
2241 let existing = self
2242 .property_nodes
2243 .get(fqcn.as_ref())
2244 .and_then(|m| m.get(storage.name.as_ref()))
2245 .copied();
2246 if let Some(node) = existing {
2247 if node.active(self)
2249 && node.visibility(self) == storage.visibility
2250 && node.is_static(self) == storage.is_static
2251 && node.is_readonly(self) == storage.is_readonly
2252 && node.ty(self) == storage.ty
2253 && node.location(self) == storage.location
2254 {
2255 return;
2256 }
2257 node.set_active(self).to(true);
2258 node.set_ty(self).to(storage.ty.clone());
2259 node.set_visibility(self).to(storage.visibility);
2260 node.set_is_static(self).to(storage.is_static);
2261 node.set_is_readonly(self).to(storage.is_readonly);
2262 node.set_location(self).to(storage.location.clone());
2263 } else {
2264 let node = PropertyNode::new(
2265 self,
2266 fqcn.clone(),
2267 storage.name.clone(),
2268 true,
2269 storage.ty.clone(),
2270 storage.visibility,
2271 storage.is_static,
2272 storage.is_readonly,
2273 storage.location.clone(),
2274 );
2275 Arc::make_mut(&mut self.property_nodes)
2276 .entry(fqcn.clone())
2277 .or_default()
2278 .insert(storage.name.clone(), node);
2279 }
2280 }
2281
2282 pub fn deactivate_class_properties(&mut self, fqcn: &str) {
2284 use salsa::Setter as _;
2285 let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
2286 Some(props) => props.values().copied().collect(),
2287 None => return,
2288 };
2289 for node in nodes {
2290 node.set_active(self).to(false);
2291 }
2292 }
2293
2294 pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
2296 use salsa::Setter as _;
2297 let existing = self
2298 .class_constant_nodes
2299 .get(fqcn.as_ref())
2300 .and_then(|m| m.get(storage.name.as_ref()))
2301 .copied();
2302 if let Some(node) = existing {
2303 if node.active(self)
2305 && node.visibility(self) == storage.visibility
2306 && node.is_final(self) == storage.is_final
2307 && node.ty(self) == storage.ty
2308 && node.location(self) == storage.location
2309 {
2310 return;
2311 }
2312 node.set_active(self).to(true);
2313 node.set_ty(self).to(storage.ty.clone());
2314 node.set_visibility(self).to(storage.visibility);
2315 node.set_is_final(self).to(storage.is_final);
2316 node.set_location(self).to(storage.location.clone());
2317 } else {
2318 let node = ClassConstantNode::new(
2319 self,
2320 fqcn.clone(),
2321 storage.name.clone(),
2322 true,
2323 storage.ty.clone(),
2324 storage.visibility,
2325 storage.is_final,
2326 storage.location.clone(),
2327 );
2328 Arc::make_mut(&mut self.class_constant_nodes)
2329 .entry(fqcn.clone())
2330 .or_default()
2331 .insert(storage.name.clone(), node);
2332 }
2333 }
2334
2335 pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
2337 use salsa::Setter as _;
2338 if let Some(&node) = self.global_constant_nodes.get(&fqn) {
2339 if node.active(self) && node.ty(self) == ty {
2341 return node;
2342 }
2343 node.set_active(self).to(true);
2344 node.set_ty(self).to(ty);
2345 node
2346 } else {
2347 let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
2348 Arc::make_mut(&mut self.global_constant_nodes).insert(fqn, node);
2349 node
2350 }
2351 }
2352
2353 pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
2355 use salsa::Setter as _;
2356 if let Some(&node) = self.global_constant_nodes.get(fqn) {
2357 node.set_active(self).to(false);
2358 }
2359 }
2360
2361 pub fn deactivate_class_constants(&mut self, fqcn: &str) {
2363 use salsa::Setter as _;
2364 let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
2365 Some(consts) => consts.values().copied().collect(),
2366 None => return,
2367 };
2368 for node in nodes {
2369 node.set_active(self).to(false);
2370 }
2371 }
2372}
2373
2374#[salsa::accumulator]
2400#[derive(Clone, Debug)]
2401pub struct IssueAccumulator(pub Issue);
2402
2403#[derive(Clone, Debug, PartialEq, Eq)]
2412pub struct RefLoc {
2413 pub symbol_key: Arc<str>,
2414 pub file: Arc<str>,
2415 pub line: u32,
2416 pub col_start: u16,
2417 pub col_end: u16,
2418}
2419
2420#[salsa::accumulator]
2427#[derive(Clone, Debug)]
2428pub struct RefLocAccumulator(pub RefLoc);
2429
2430#[salsa::input]
2435pub struct AnalyzeFileInput {
2436 pub php_version: Arc<str>,
2439}
2440
2441#[salsa::tracked]
2456pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, _input: AnalyzeFileInput) {
2457 use salsa::Accumulator as _;
2458 let path = file.path(db);
2459 let text = file.text(db);
2460
2461 let arena = bumpalo::Bump::new();
2462 let parsed = php_rs_parser::parse(&arena, &text);
2463
2464 for err in &parsed.errors {
2465 let issue = Issue::new(
2466 mir_issues::IssueKind::ParseError {
2467 message: err.to_string(),
2468 },
2469 mir_issues::Location {
2470 file: path.clone(),
2471 line: 1,
2472 line_end: 1,
2473 col_start: 0,
2474 col_end: 0,
2475 },
2476 );
2477 IssueAccumulator(issue).accumulate(db);
2478 }
2479}
2480
2481#[cfg(test)]
2486mod tests {
2487 use super::*;
2488 use salsa::Setter as _;
2489
2490 fn upsert_class(
2491 db: &mut MirDb,
2492 fqcn: &str,
2493 parent: Option<Arc<str>>,
2494 extends: Arc<[Arc<str>]>,
2495 is_interface: bool,
2496 ) -> ClassNode {
2497 db.upsert_class_node(ClassNodeFields {
2498 is_interface,
2499 parent,
2500 extends,
2501 ..ClassNodeFields::for_class(Arc::from(fqcn))
2502 })
2503 }
2504
2505 #[test]
2506 fn mirdb_constructs() {
2507 let _db = MirDb::default();
2508 }
2509
2510 #[test]
2511 fn source_file_input_roundtrip() {
2512 let db = MirDb::default();
2513 let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
2514 assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
2515 assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
2516 }
2517
2518 #[test]
2519 fn collect_file_definitions_basic() {
2520 let db = MirDb::default();
2521 let src = Arc::from("<?php class Foo {}");
2522 let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
2523 let defs = collect_file_definitions(&db, file);
2524 assert!(defs.issues.is_empty());
2525 assert_eq!(defs.slice.classes.len(), 1);
2526 assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
2527 }
2528
2529 #[test]
2530 fn collect_file_definitions_memoized() {
2531 let db = MirDb::default();
2532 let file = SourceFile::new(
2533 &db,
2534 Arc::from("/tmp/memo.php"),
2535 Arc::from("<?php class Bar {}"),
2536 );
2537
2538 let defs1 = collect_file_definitions(&db, file);
2539 let defs2 = collect_file_definitions(&db, file);
2540 assert!(
2541 Arc::ptr_eq(&defs1.slice, &defs2.slice),
2542 "unchanged file must return the memoized result"
2543 );
2544 }
2545
2546 #[test]
2547 fn analyze_file_accumulates_parse_errors() {
2548 let db = MirDb::default();
2549 let file = SourceFile::new(
2551 &db,
2552 Arc::from("/tmp/parse_err.php"),
2553 Arc::from("<?php $x = \"unterminated"),
2554 );
2555 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2556 analyze_file(&db, file, input);
2557 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2558 assert!(
2559 !issues.is_empty(),
2560 "expected parse error to surface as accumulated IssueAccumulator"
2561 );
2562 assert!(matches!(
2563 issues[0].0.kind,
2564 mir_issues::IssueKind::ParseError { .. }
2565 ));
2566 }
2567
2568 #[test]
2569 fn analyze_file_clean_input_accumulates_nothing() {
2570 let db = MirDb::default();
2571 let file = SourceFile::new(
2572 &db,
2573 Arc::from("/tmp/clean.php"),
2574 Arc::from("<?php class Foo {}"),
2575 );
2576 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2577 analyze_file(&db, file, input);
2578 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2579 let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
2580 assert!(issues.is_empty());
2581 assert!(refs.is_empty());
2582 }
2583
2584 #[test]
2585 fn collect_file_definitions_recomputes_on_change() {
2586 let mut db = MirDb::default();
2587 let file = SourceFile::new(
2588 &db,
2589 Arc::from("/tmp/memo2.php"),
2590 Arc::from("<?php class Foo {}"),
2591 );
2592
2593 let defs1 = collect_file_definitions(&db, file);
2594 file.set_text(&mut db)
2595 .to(Arc::from("<?php class Foo {} class Bar {}"));
2596 let defs2 = collect_file_definitions(&db, file);
2597
2598 assert!(
2599 !Arc::ptr_eq(&defs1.slice, &defs2.slice),
2600 "changed file must produce a new result"
2601 );
2602 assert_eq!(defs2.slice.classes.len(), 2);
2603 }
2604
2605 #[test]
2606 fn class_ancestors_empty_for_root_class() {
2607 let mut db = MirDb::default();
2608 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2609 let ancestors = class_ancestors(&db, node);
2610 assert!(ancestors.0.is_empty(), "root class has no ancestors");
2611 }
2612
2613 #[test]
2614 fn class_ancestors_single_parent() {
2615 let mut db = MirDb::default();
2616 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2617 let child = upsert_class(
2618 &mut db,
2619 "Child",
2620 Some(Arc::from("Base")),
2621 Arc::from([]),
2622 false,
2623 );
2624 let ancestors = class_ancestors(&db, child);
2625 assert_eq!(ancestors.0.len(), 1);
2626 assert_eq!(ancestors.0[0].as_ref(), "Base");
2627 }
2628
2629 #[test]
2630 fn class_ancestors_transitive() {
2631 let mut db = MirDb::default();
2632 upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
2633 upsert_class(
2634 &mut db,
2635 "Parent",
2636 Some(Arc::from("GrandParent")),
2637 Arc::from([]),
2638 false,
2639 );
2640 let child = upsert_class(
2641 &mut db,
2642 "Child",
2643 Some(Arc::from("Parent")),
2644 Arc::from([]),
2645 false,
2646 );
2647 let ancestors = class_ancestors(&db, child);
2648 assert_eq!(ancestors.0.len(), 2);
2649 assert_eq!(ancestors.0[0].as_ref(), "Parent");
2650 assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
2651 }
2652
2653 #[test]
2654 fn class_ancestors_cycle_returns_empty() {
2655 let mut db = MirDb::default();
2656 let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
2658 let ancestors = class_ancestors(&db, node_a);
2659 assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
2661 }
2662
2663 #[test]
2664 fn class_ancestors_inactive_node_returns_empty() {
2665 let mut db = MirDb::default();
2666 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2667 db.deactivate_class_node("Foo");
2668 let ancestors = class_ancestors(&db, node);
2669 assert!(ancestors.0.is_empty(), "inactive node must yield empty");
2670 }
2671
2672 #[test]
2673 fn class_ancestors_recomputes_on_parent_change() {
2674 let mut db = MirDb::default();
2675 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2676 let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
2677
2678 let before = class_ancestors(&db, child);
2679 assert!(before.0.is_empty());
2680
2681 child.set_parent(&mut db).to(Some(Arc::from("Base")));
2683
2684 let after = class_ancestors(&db, child);
2685 assert_eq!(after.0.len(), 1);
2686 assert_eq!(after.0[0].as_ref(), "Base");
2687 }
2688
2689 #[test]
2690 fn interface_ancestors_via_extends() {
2691 let mut db = MirDb::default();
2692 upsert_class(&mut db, "Countable", None, Arc::from([]), true);
2693 let child_iface = upsert_class(
2694 &mut db,
2695 "Collection",
2696 None,
2697 Arc::from([Arc::from("Countable")]),
2698 true,
2699 );
2700 let ancestors = class_ancestors(&db, child_iface);
2701 assert_eq!(ancestors.0.len(), 1);
2702 assert_eq!(ancestors.0[0].as_ref(), "Countable");
2703 }
2704
2705 #[test]
2706 fn type_exists_via_db_tracks_active_state() {
2707 let mut db = MirDb::default();
2708 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2709 assert!(type_exists_via_db(&db, "Foo"));
2710 assert!(!type_exists_via_db(&db, "Bar"));
2711 db.deactivate_class_node("Foo");
2712 assert!(!type_exists_via_db(&db, "Foo"));
2713 }
2714
2715 #[test]
2716 fn clone_preserves_class_node_lookups() {
2717 let mut db = MirDb::default();
2720 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2721 let cloned = db.clone();
2722 assert!(
2723 type_exists_via_db(&cloned, "Foo"),
2724 "clone must observe nodes registered before clone()"
2725 );
2726 assert!(
2727 !type_exists_via_db(&cloned, "Bar"),
2728 "clone must not observe nodes that were never registered"
2729 );
2730 let foo_node = cloned.lookup_class_node("Foo").expect("registered");
2732 let ancestors = class_ancestors(&cloned, foo_node);
2733 assert!(ancestors.0.is_empty(), "Foo has no ancestors");
2734 }
2735
2736 fn upsert_class_with_traits(
2741 db: &mut MirDb,
2742 fqcn: &str,
2743 parent: Option<Arc<str>>,
2744 traits: &[&str],
2745 is_interface: bool,
2746 is_trait: bool,
2747 ) -> ClassNode {
2748 db.upsert_class_node(ClassNodeFields {
2749 is_interface,
2750 is_trait,
2751 parent,
2752 traits: Arc::from(
2753 traits
2754 .iter()
2755 .map(|t| Arc::<str>::from(*t))
2756 .collect::<Vec<_>>(),
2757 ),
2758 ..ClassNodeFields::for_class(Arc::from(fqcn))
2759 })
2760 }
2761
2762 fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
2763 let storage = MethodStorage {
2764 name: Arc::from(name),
2765 fqcn: Arc::from(fqcn),
2766 params: vec![],
2767 return_type: None,
2768 inferred_return_type: None,
2769 visibility: Visibility::Public,
2770 is_static: false,
2771 is_abstract,
2772 is_final: false,
2773 is_constructor: name == "__construct",
2774 template_params: vec![],
2775 assertions: vec![],
2776 throws: vec![],
2777 deprecated: None,
2778 is_internal: false,
2779 is_pure: false,
2780 location: None,
2781 };
2782 db.upsert_method_node(&storage)
2783 }
2784
2785 fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
2786 db.upsert_class_node(ClassNodeFields {
2787 interfaces: Arc::from(
2788 interfaces
2789 .iter()
2790 .map(|i| Arc::<str>::from(*i))
2791 .collect::<Vec<_>>(),
2792 ),
2793 is_backed_enum: is_backed,
2794 ..ClassNodeFields::for_enum(Arc::from(fqcn))
2795 })
2796 }
2797
2798 #[test]
2803 fn method_exists_via_db_finds_own_method() {
2804 let mut db = MirDb::default();
2805 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2806 upsert_method(&mut db, "Foo", "bar", false);
2807 assert!(method_exists_via_db(&db, "Foo", "bar"));
2808 assert!(!method_exists_via_db(&db, "Foo", "missing"));
2809 }
2810
2811 #[test]
2812 fn method_exists_via_db_walks_parent() {
2813 let mut db = MirDb::default();
2814 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2815 upsert_method(&mut db, "Base", "inherited", false);
2816 upsert_class(
2817 &mut db,
2818 "Child",
2819 Some(Arc::from("Base")),
2820 Arc::from([]),
2821 false,
2822 );
2823 assert!(method_exists_via_db(&db, "Child", "inherited"));
2824 }
2825
2826 #[test]
2827 fn method_exists_via_db_walks_traits_transitively() {
2828 let mut db = MirDb::default();
2829 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2830 upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
2831 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2832 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2833 assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
2834 }
2835
2836 #[test]
2837 fn method_exists_via_db_is_case_insensitive() {
2838 let mut db = MirDb::default();
2839 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2840 upsert_method(&mut db, "Foo", "doStuff", false);
2841 assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
2843 assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
2844 }
2845
2846 #[test]
2847 fn method_exists_via_db_unknown_class_returns_false() {
2848 let db = MirDb::default();
2849 assert!(!method_exists_via_db(&db, "Nope", "anything"));
2850 }
2851
2852 #[test]
2853 fn method_exists_via_db_inactive_class_returns_false() {
2854 let mut db = MirDb::default();
2855 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2856 upsert_method(&mut db, "Foo", "bar", false);
2857 db.deactivate_class_node("Foo");
2858 assert!(!method_exists_via_db(&db, "Foo", "bar"));
2859 }
2860
2861 #[test]
2862 fn method_exists_via_db_finds_abstract_methods() {
2863 let mut db = MirDb::default();
2866 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2867 upsert_method(&mut db, "Foo", "abstr", true);
2868 assert!(method_exists_via_db(&db, "Foo", "abstr"));
2869 }
2870
2871 #[test]
2876 fn method_is_concretely_implemented_skips_abstract() {
2877 let mut db = MirDb::default();
2878 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2879 upsert_method(&mut db, "Foo", "abstr", true);
2880 assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
2881 }
2882
2883 #[test]
2884 fn method_is_concretely_implemented_finds_concrete_in_trait() {
2885 let mut db = MirDb::default();
2886 upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
2887 upsert_method(&mut db, "MyTrait", "provided", false);
2888 upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
2889 assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
2890 }
2891
2892 #[test]
2893 fn method_is_concretely_implemented_skips_interface_definitions() {
2894 let mut db = MirDb::default();
2897 upsert_class(&mut db, "I", None, Arc::from([]), true);
2898 upsert_method(&mut db, "I", "m", false);
2899 upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
2900 assert!(!method_is_concretely_implemented(&db, "C", "m"));
2902 }
2903
2904 #[test]
2909 fn extends_or_implements_via_db_self_match() {
2910 let mut db = MirDb::default();
2911 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2912 assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
2913 }
2914
2915 #[test]
2916 fn extends_or_implements_via_db_transitive() {
2917 let mut db = MirDb::default();
2918 upsert_class(&mut db, "Animal", None, Arc::from([]), false);
2919 upsert_class(
2920 &mut db,
2921 "Mammal",
2922 Some(Arc::from("Animal")),
2923 Arc::from([]),
2924 false,
2925 );
2926 upsert_class(
2927 &mut db,
2928 "Dog",
2929 Some(Arc::from("Mammal")),
2930 Arc::from([]),
2931 false,
2932 );
2933 assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
2934 assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
2935 assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
2936 }
2937
2938 #[test]
2939 fn extends_or_implements_via_db_unknown_returns_false() {
2940 let db = MirDb::default();
2941 assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
2942 }
2943
2944 #[test]
2945 fn extends_or_implements_via_db_unit_enum_implicit() {
2946 let mut db = MirDb::default();
2947 upsert_enum(&mut db, "Status", &[], false);
2948 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2949 assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
2950 assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2952 }
2953
2954 #[test]
2955 fn extends_or_implements_via_db_backed_enum_implicit() {
2956 let mut db = MirDb::default();
2957 upsert_enum(&mut db, "Status", &[], true);
2958 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2959 assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2960 assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
2961 }
2962
2963 #[test]
2964 fn extends_or_implements_via_db_enum_declared_interface() {
2965 let mut db = MirDb::default();
2966 upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
2967 upsert_enum(&mut db, "Status", &["Stringable"], false);
2968 assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
2969 }
2970
2971 #[test]
2976 fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
2977 let mut db = MirDb::default();
2978 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2979 upsert_class(
2980 &mut db,
2981 "Child",
2982 Some(Arc::from("Base")),
2983 Arc::from([]),
2984 false,
2985 );
2986 assert!(!has_unknown_ancestor_via_db(&db, "Child"));
2987 }
2988
2989 #[test]
2990 fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
2991 let mut db = MirDb::default();
2992 upsert_class(
2994 &mut db,
2995 "Child",
2996 Some(Arc::from("Missing")),
2997 Arc::from([]),
2998 false,
2999 );
3000 assert!(has_unknown_ancestor_via_db(&db, "Child"));
3001 }
3002
3003 #[test]
3004 fn class_template_params_via_db_returns_registered_params() {
3005 use mir_types::Variance;
3006 let mut db = MirDb::default();
3007 let tp = TemplateParam {
3008 name: Arc::from("T"),
3009 bound: None,
3010 defining_entity: Arc::from("Box"),
3011 variance: Variance::Invariant,
3012 };
3013 db.upsert_class_node(ClassNodeFields {
3014 template_params: Arc::from([tp.clone()]),
3015 ..ClassNodeFields::for_class(Arc::from("Box"))
3016 });
3017 let got = class_template_params_via_db(&db, "Box").expect("registered");
3018 assert_eq!(got.len(), 1);
3019 assert_eq!(got[0].name.as_ref(), "T");
3020
3021 assert!(class_template_params_via_db(&db, "Missing").is_none());
3022 db.deactivate_class_node("Box");
3023 assert!(class_template_params_via_db(&db, "Box").is_none());
3024 }
3025
3026 fn upsert_class_with_mixins(
3031 db: &mut MirDb,
3032 fqcn: &str,
3033 parent: Option<Arc<str>>,
3034 mixins: &[&str],
3035 ) -> ClassNode {
3036 db.upsert_class_node(ClassNodeFields {
3037 parent,
3038 mixins: Arc::from(
3039 mixins
3040 .iter()
3041 .map(|m| Arc::<str>::from(*m))
3042 .collect::<Vec<_>>(),
3043 ),
3044 ..ClassNodeFields::for_class(Arc::from(fqcn))
3045 })
3046 }
3047
3048 #[test]
3049 fn lookup_method_in_chain_finds_own_then_ancestor() {
3050 let mut db = MirDb::default();
3051 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3052 upsert_method(&mut db, "Base", "shared", false);
3053 upsert_class(
3054 &mut db,
3055 "Child",
3056 Some(Arc::from("Base")),
3057 Arc::from([]),
3058 false,
3059 );
3060 upsert_method(&mut db, "Child", "shared", false);
3061 let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
3063 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3064 upsert_method(&mut db, "Base", "only_in_base", false);
3066 let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
3067 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3068 }
3069
3070 #[test]
3071 fn lookup_method_in_chain_walks_trait_of_traits() {
3072 let mut db = MirDb::default();
3073 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
3074 upsert_method(&mut db, "InnerTrait", "deep", false);
3075 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
3076 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
3077 let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
3078 assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
3079 }
3080
3081 #[test]
3082 fn lookup_method_in_chain_walks_mixins() {
3083 let mut db = MirDb::default();
3084 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3085 upsert_method(&mut db, "MixinTarget", "magic", false);
3086 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3087 let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
3088 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3089 }
3090
3091 #[test]
3092 fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
3093 let mut db = MirDb::default();
3094 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3096 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3097 assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
3098 }
3099
3100 #[test]
3101 fn lookup_method_in_chain_is_case_insensitive() {
3102 let mut db = MirDb::default();
3103 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3104 upsert_method(&mut db, "Foo", "doStuff", false);
3105 assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
3106 assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
3107 }
3108
3109 #[test]
3110 fn lookup_method_in_chain_unknown_returns_none() {
3111 let db = MirDb::default();
3112 assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
3113 }
3114
3115 fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
3120 let storage = PropertyStorage {
3121 name: Arc::from(name),
3122 ty: None,
3123 inferred_ty: None,
3124 visibility: Visibility::Public,
3125 is_static: false,
3126 is_readonly,
3127 default: None,
3128 location: None,
3129 };
3130 let owner = Arc::<str>::from(fqcn);
3131 db.upsert_property_node(&owner, &storage);
3132 db.lookup_property_node(fqcn, name).expect("registered")
3133 }
3134
3135 #[test]
3136 fn lookup_property_in_chain_own_then_ancestor() {
3137 let mut db = MirDb::default();
3138 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3139 upsert_property(&mut db, "Base", "x", false);
3140 upsert_class(
3141 &mut db,
3142 "Child",
3143 Some(Arc::from("Base")),
3144 Arc::from([]),
3145 false,
3146 );
3147 let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
3149 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3150 upsert_property(&mut db, "Child", "x", true);
3152 let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
3153 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3154 assert!(found.is_readonly(&db));
3155 }
3156
3157 #[test]
3158 fn lookup_property_in_chain_walks_mixins() {
3159 let mut db = MirDb::default();
3160 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3161 upsert_property(&mut db, "MixinTarget", "exposed", false);
3162 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3163 let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
3164 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3165 }
3166
3167 #[test]
3168 fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
3169 let mut db = MirDb::default();
3170 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3171 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3172 assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
3173 }
3174
3175 #[test]
3176 fn lookup_property_in_chain_is_case_sensitive() {
3177 let mut db = MirDb::default();
3178 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3179 upsert_property(&mut db, "Foo", "myProp", false);
3180 assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
3181 assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
3183 }
3184
3185 #[test]
3186 fn lookup_property_in_chain_inactive_returns_none() {
3187 let mut db = MirDb::default();
3188 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3189 upsert_property(&mut db, "Foo", "x", false);
3190 db.deactivate_class_node("Foo");
3191 assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
3192 }
3193
3194 fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
3199 let storage = ConstantStorage {
3200 name: Arc::from(name),
3201 ty: mir_types::Union::mixed(),
3202 visibility: None,
3203 is_final: false,
3204 location: None,
3205 };
3206 let owner = Arc::<str>::from(fqcn);
3207 db.upsert_class_constant_node(&owner, &storage);
3208 }
3209
3210 #[test]
3211 fn class_constant_exists_in_chain_finds_own() {
3212 let mut db = MirDb::default();
3213 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3214 upsert_constant(&mut db, "Foo", "MAX");
3215 assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
3216 assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
3217 }
3218
3219 #[test]
3220 fn class_constant_exists_in_chain_walks_parent() {
3221 let mut db = MirDb::default();
3222 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3223 upsert_constant(&mut db, "Base", "VERSION");
3224 upsert_class(
3225 &mut db,
3226 "Child",
3227 Some(Arc::from("Base")),
3228 Arc::from([]),
3229 false,
3230 );
3231 assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
3232 }
3233
3234 #[test]
3235 fn class_constant_exists_in_chain_walks_interface() {
3236 let mut db = MirDb::default();
3237 upsert_class(&mut db, "I", None, Arc::from([]), true);
3238 upsert_constant(&mut db, "I", "TYPE");
3239 db.upsert_class_node(ClassNodeFields {
3242 interfaces: Arc::from([Arc::from("I")]),
3243 ..ClassNodeFields::for_class(Arc::from("Impl"))
3244 });
3245 assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
3246 }
3247
3248 #[test]
3249 fn class_constant_exists_in_chain_walks_direct_trait() {
3250 let mut db = MirDb::default();
3251 upsert_class_with_traits(&mut db, "T", None, &[], false, true);
3252 upsert_constant(&mut db, "T", "FROM_TRAIT");
3253 upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
3254 assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
3255 }
3256
3257 #[test]
3258 fn class_constant_exists_in_chain_unknown_class_returns_false() {
3259 let db = MirDb::default();
3260 assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
3261 }
3262
3263 #[test]
3264 fn class_constant_exists_in_chain_inactive_returns_false() {
3265 let mut db = MirDb::default();
3266 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3267 upsert_constant(&mut db, "Foo", "X");
3268 db.deactivate_class_node("Foo");
3269 db.deactivate_class_constants("Foo");
3270 assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
3271 }
3272
3273 #[test]
3279 fn parallel_reads_then_serial_write_does_not_deadlock() {
3280 use rayon::prelude::*;
3281 use std::sync::mpsc;
3282 use std::time::Duration;
3283
3284 let (tx, rx) = mpsc::channel::<()>();
3285 std::thread::spawn(move || {
3286 let mut db = MirDb::default();
3287 let storage = mir_codebase::storage::FunctionStorage {
3288 fqn: Arc::from("foo"),
3289 short_name: Arc::from("foo"),
3290 params: vec![],
3291 return_type: None,
3292 inferred_return_type: None,
3293 template_params: vec![],
3294 assertions: vec![],
3295 throws: vec![],
3296 deprecated: None,
3297 is_pure: false,
3298 location: None,
3299 };
3300 let node = db.upsert_function_node(&storage);
3301
3302 let db_for_sweep = db.clone();
3304 (0..256u32)
3305 .into_par_iter()
3306 .for_each_with(db_for_sweep, |db, _| {
3307 let _ = node.return_type(&*db as &dyn MirDatabase);
3308 });
3309
3310 node.set_return_type(&mut db).to(Some(Union::mixed()));
3314 assert_eq!(node.return_type(&db), Some(Union::mixed()));
3315 tx.send(()).unwrap();
3316 });
3317
3318 match rx.recv_timeout(Duration::from_secs(30)) {
3319 Ok(()) => {}
3320 Err(_) => {
3321 panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
3322 }
3323 }
3324 }
3325
3326 #[test]
3337 fn sibling_clone_blocks_setter_until_dropped() {
3338 use std::sync::mpsc;
3339 use std::time::Duration;
3340
3341 let mut db = MirDb::default();
3342 let storage = mir_codebase::storage::FunctionStorage {
3343 fqn: Arc::from("foo"),
3344 short_name: Arc::from("foo"),
3345 params: vec![],
3346 return_type: None,
3347 inferred_return_type: None,
3348 template_params: vec![],
3349 assertions: vec![],
3350 throws: vec![],
3351 deprecated: None,
3352 is_pure: false,
3353 location: None,
3354 };
3355 let node = db.upsert_function_node(&storage);
3356
3357 let sibling = db.clone();
3358
3359 let (tx, rx) = mpsc::channel::<()>();
3362 let writer = std::thread::spawn(move || {
3363 node.set_return_type(&mut db).to(Some(Union::mixed()));
3364 tx.send(()).unwrap();
3365 });
3366
3367 match rx.recv_timeout(Duration::from_millis(500)) {
3370 Err(mpsc::RecvTimeoutError::Timeout) => { }
3371 Ok(()) => panic!(
3372 "setter completed while sibling clone was alive — strong-count==1 \
3373 invariant of `cancel_others` is broken; commit_inferred_return_types \
3374 cannot rely on tight-scoping clones"
3375 ),
3376 Err(e) => panic!("unexpected channel error: {e:?}"),
3377 }
3378
3379 drop(sibling);
3381
3382 match rx.recv_timeout(Duration::from_secs(5)) {
3383 Ok(()) => {}
3384 Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
3385 }
3386 writer.join().expect("writer thread panicked");
3387 }
3388}