1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use mir_codebase::storage::{
5 Assertion, ConstantStorage, FnParam, FunctionStorage, Location, MethodStorage, PropertyStorage,
6 TemplateParam, Visibility,
7};
8use mir_codebase::{Codebase, StubSlice};
9use mir_issues::Issue;
10use mir_types::Union;
11
12#[salsa::db]
18pub trait MirDatabase: salsa::Database {
19 fn php_version_str(&self) -> Arc<str>;
21
22 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode>;
29
30 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode>;
32
33 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode>;
39
40 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode>;
42
43 fn lookup_class_constant_node(&self, fqcn: &str, const_name: &str)
45 -> Option<ClassConstantNode>;
46
47 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode>;
49
50 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode>;
53
54 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode>;
57
58 fn active_class_node_fqcns(&self) -> Vec<Arc<str>>;
62
63 fn active_function_node_fqns(&self) -> Vec<Arc<str>>;
66}
67
68#[salsa::input]
76pub struct SourceFile {
77 pub path: Arc<str>,
78 pub text: Arc<str>,
79}
80
81#[derive(Clone, Debug)]
87pub struct FileDefinitions {
88 pub slice: Arc<StubSlice>,
89 pub issues: Arc<Vec<Issue>>,
90}
91
92impl PartialEq for FileDefinitions {
93 fn eq(&self, other: &Self) -> bool {
94 Arc::ptr_eq(&self.slice, &other.slice) && Arc::ptr_eq(&self.issues, &other.issues)
95 }
96}
97
98unsafe impl salsa::Update for FileDefinitions {
99 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
100 unsafe { *old_pointer = new_value };
101 true
102 }
103}
104
105pub type ImplementsTypeArgs = Arc<[(Arc<str>, Arc<[Union]>)]>;
112
113#[salsa::input]
122pub struct ClassNode {
123 pub fqcn: Arc<str>,
124 pub active: bool,
127 pub is_interface: bool,
128 pub is_trait: bool,
133 pub is_enum: bool,
135 pub is_abstract: bool,
138 pub parent: Option<Arc<str>>,
140 pub interfaces: Arc<[Arc<str>]>,
142 pub traits: Arc<[Arc<str>]>,
145 pub extends: Arc<[Arc<str>]>,
147 pub template_params: Arc<[TemplateParam]>,
150 pub require_extends: Arc<[Arc<str>]>,
154 pub require_implements: Arc<[Arc<str>]>,
158 pub is_backed_enum: bool,
163 pub mixins: Arc<[Arc<str>]>,
168 pub deprecated: Option<Arc<str>>,
173 pub enum_scalar_type: Option<Union>,
178 pub is_final: bool,
182 pub is_readonly: bool,
185 pub location: Option<Location>,
190 pub extends_type_args: Arc<[Union]>,
193 pub implements_type_args: ImplementsTypeArgs,
196}
197
198#[derive(Debug, Clone, Copy)]
205pub struct ClassKind {
206 pub is_interface: bool,
207 pub is_trait: bool,
208 pub is_enum: bool,
209 pub is_abstract: bool,
210}
211
212pub fn class_kind_via_db(db: &dyn MirDatabase, fqcn: &str) -> Option<ClassKind> {
218 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
219 Some(ClassKind {
220 is_interface: node.is_interface(db),
221 is_trait: node.is_trait(db),
222 is_enum: node.is_enum(db),
223 is_abstract: node.is_abstract(db),
224 })
225}
226
227pub fn type_exists_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
232 db.lookup_class_node(fqcn).is_some_and(|n| n.active(db))
233}
234
235pub fn class_template_params_via_db(
239 db: &dyn MirDatabase,
240 fqcn: &str,
241) -> Option<Arc<[TemplateParam]>> {
242 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
243 Some(node.template_params(db))
244}
245
246pub fn inherited_template_bindings_via_db(
253 db: &dyn MirDatabase,
254 fqcn: &str,
255) -> std::collections::HashMap<Arc<str>, Union> {
256 let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
257 let mut visited: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
258 let mut current: Arc<str> = Arc::from(fqcn);
259 loop {
260 if !visited.insert(current.clone()) {
261 break;
262 }
263 let node = match db
264 .lookup_class_node(current.as_ref())
265 .filter(|n| n.active(db))
266 {
267 Some(n) => n,
268 None => break,
269 };
270 let parent = match node.parent(db) {
271 Some(p) => p,
272 None => break,
273 };
274 let extends_type_args = node.extends_type_args(db);
275 if !extends_type_args.is_empty() {
276 if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
277 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
278 bindings
279 .entry(tp.name.clone())
280 .or_insert_with(|| ty.clone());
281 }
282 }
283 }
284 current = parent;
285 }
286 bindings
287}
288
289#[salsa::input]
306pub struct FunctionNode {
307 pub fqn: Arc<str>,
308 pub short_name: Arc<str>,
309 pub active: bool,
310 pub params: Arc<[FnParam]>,
311 pub return_type: Option<Union>,
312 pub inferred_return_type: Option<Union>,
313 pub template_params: Arc<[TemplateParam]>,
314 pub assertions: Arc<[Assertion]>,
315 pub throws: Arc<[Arc<str>]>,
316 pub deprecated: Option<Arc<str>>,
317 pub is_pure: bool,
318 pub location: Option<Location>,
321}
322
323#[salsa::input]
340pub struct MethodNode {
341 pub fqcn: Arc<str>,
342 pub name: Arc<str>,
343 pub active: bool,
344 pub params: Arc<[FnParam]>,
345 pub return_type: Option<Union>,
346 pub inferred_return_type: Option<Union>,
347 pub template_params: Arc<[TemplateParam]>,
348 pub assertions: Arc<[Assertion]>,
349 pub throws: Arc<[Arc<str>]>,
350 pub deprecated: Option<Arc<str>>,
351 pub visibility: Visibility,
352 pub is_static: bool,
353 pub is_abstract: bool,
354 pub is_final: bool,
355 pub is_constructor: bool,
356 pub is_pure: bool,
357 pub location: Option<Location>,
360}
361
362#[salsa::input]
373pub struct PropertyNode {
374 pub fqcn: Arc<str>,
375 pub name: Arc<str>,
376 pub active: bool,
377 pub ty: Option<Union>,
378 pub visibility: Visibility,
379 pub is_static: bool,
380 pub is_readonly: bool,
381 pub location: Option<Location>,
382}
383
384#[salsa::input]
392pub struct ClassConstantNode {
393 pub fqcn: Arc<str>,
394 pub name: Arc<str>,
395 pub active: bool,
396 pub ty: Union,
397 pub visibility: Option<Visibility>,
398 pub is_final: bool,
399 pub location: Option<Location>,
403}
404
405#[salsa::input]
412pub struct GlobalConstantNode {
413 pub fqn: Arc<str>,
414 pub active: bool,
415 pub ty: Union,
416}
417
418#[derive(Clone, Debug, Default)]
427pub struct Ancestors(pub Vec<Arc<str>>);
428
429impl PartialEq for Ancestors {
430 fn eq(&self, other: &Self) -> bool {
431 self.0.len() == other.0.len()
432 && self
433 .0
434 .iter()
435 .zip(&other.0)
436 .all(|(a, b)| a.as_ref() == b.as_ref())
437 }
438}
439
440unsafe impl salsa::Update for Ancestors {
441 unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
442 let old = unsafe { &mut *old_ptr };
443 if *old == new_val {
444 return false;
445 }
446 *old = new_val;
447 true
448 }
449}
450
451fn ancestors_initial(_db: &dyn MirDatabase, _id: salsa::Id, _node: ClassNode) -> Ancestors {
456 Ancestors(vec![])
457}
458
459fn ancestors_cycle(
460 _db: &dyn MirDatabase,
461 _cycle: &salsa::Cycle,
462 _last: &Ancestors,
463 _value: Ancestors,
464 _node: ClassNode,
465) -> Ancestors {
466 Ancestors(vec![])
469}
470
471#[salsa::tracked(cycle_fn = ancestors_cycle, cycle_initial = ancestors_initial)]
481pub fn class_ancestors(db: &dyn MirDatabase, node: ClassNode) -> Ancestors {
482 if !node.active(db) {
483 return Ancestors(vec![]);
484 }
485 if node.is_enum(db) || node.is_trait(db) {
496 return Ancestors(vec![]);
497 }
498
499 let mut all: Vec<Arc<str>> = Vec::new();
500 let mut seen: HashSet<String> = HashSet::new();
501
502 let add = |fqcn: &Arc<str>, all: &mut Vec<Arc<str>>, seen: &mut HashSet<String>| {
503 if seen.insert(fqcn.to_string()) {
504 all.push(fqcn.clone());
505 }
506 };
507
508 if node.is_interface(db) {
509 for e in node.extends(db).iter() {
510 add(e, &mut all, &mut seen);
511 if let Some(parent_node) = db.lookup_class_node(e) {
512 for a in class_ancestors(db, parent_node).0 {
513 add(&a, &mut all, &mut seen);
514 }
515 }
516 }
517 } else {
518 if let Some(ref p) = node.parent(db) {
519 add(p, &mut all, &mut seen);
520 if let Some(parent_node) = db.lookup_class_node(p) {
521 for a in class_ancestors(db, parent_node).0 {
522 add(&a, &mut all, &mut seen);
523 }
524 }
525 }
526 for iface in node.interfaces(db).iter() {
527 add(iface, &mut all, &mut seen);
528 if let Some(iface_node) = db.lookup_class_node(iface) {
529 for a in class_ancestors(db, iface_node).0 {
530 add(&a, &mut all, &mut seen);
531 }
532 }
533 }
534 for t in node.traits(db).iter() {
535 add(t, &mut all, &mut seen);
536 }
537 }
538
539 Ancestors(all)
540}
541
542pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
551 let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
552 return false;
553 };
554 class_ancestors(db, node)
555 .0
556 .iter()
557 .any(|ancestor| !type_exists_via_db(db, ancestor))
558}
559
560pub fn method_is_concretely_implemented(
571 db: &dyn MirDatabase,
572 fqcn: &str,
573 method_name: &str,
574) -> bool {
575 let lower = method_name.to_lowercase();
576 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
577 return false;
578 };
579 if self_node.is_interface(db) {
582 return false;
583 }
584 if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
586 if !m.is_abstract(db) {
587 return true;
588 }
589 }
590 let mut visited_traits: HashSet<String> = HashSet::new();
592 for t in self_node.traits(db).iter() {
593 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
594 return true;
595 }
596 }
597 for ancestor in class_ancestors(db, self_node).0.iter() {
600 let Some(anc_node) = db
601 .lookup_class_node(ancestor.as_ref())
602 .filter(|n| n.active(db))
603 else {
604 continue;
605 };
606 if anc_node.is_interface(db) {
607 continue;
608 }
609 if !anc_node.is_trait(db) {
611 if let Some(m) = db
612 .lookup_method_node(ancestor.as_ref(), &lower)
613 .filter(|m| m.active(db))
614 {
615 if !m.is_abstract(db) {
616 return true;
617 }
618 }
619 }
620 if anc_node.is_trait(db) {
623 if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
624 return true;
625 }
626 } else {
627 for t in anc_node.traits(db).iter() {
628 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
629 return true;
630 }
631 }
632 }
633 }
634 false
635}
636
637fn trait_provides_method(
641 db: &dyn MirDatabase,
642 trait_fqcn: &str,
643 method_lower: &str,
644 visited: &mut HashSet<String>,
645) -> bool {
646 if !visited.insert(trait_fqcn.to_string()) {
647 return false;
648 }
649 if let Some(m) = db
650 .lookup_method_node(trait_fqcn, method_lower)
651 .filter(|m| m.active(db))
652 {
653 if !m.is_abstract(db) {
654 return true;
655 }
656 }
657 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
658 return false;
659 };
660 if !node.is_trait(db) {
661 return false;
662 }
663 for t in node.traits(db).iter() {
664 if trait_provides_method(db, t.as_ref(), method_lower, visited) {
665 return true;
666 }
667 }
668 false
669}
670
671pub fn lookup_method_in_chain(
686 db: &dyn MirDatabase,
687 fqcn: &str,
688 method_name: &str,
689) -> Option<MethodNode> {
690 let mut visited_mixins: HashSet<String> = HashSet::new();
691 lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
692}
693
694fn lookup_method_in_chain_inner(
695 db: &dyn MirDatabase,
696 fqcn: &str,
697 lower: &str,
698 visited_mixins: &mut HashSet<String>,
699) -> Option<MethodNode> {
700 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
701
702 if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
704 return Some(node);
705 }
706 for m in self_node.mixins(db).iter() {
710 if visited_mixins.insert(m.to_string()) {
711 if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
712 {
713 return Some(node);
714 }
715 }
716 }
717 let mut visited_traits: HashSet<String> = HashSet::new();
720 for t in self_node.traits(db).iter() {
721 if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
722 return Some(node);
723 }
724 }
725 for ancestor in class_ancestors(db, self_node).0.iter() {
727 if let Some(node) = db
728 .lookup_method_node(ancestor.as_ref(), lower)
729 .filter(|n| n.active(db))
730 {
731 return Some(node);
732 }
733 if let Some(anc_node) = db
734 .lookup_class_node(ancestor.as_ref())
735 .filter(|n| n.active(db))
736 {
737 if anc_node.is_trait(db) {
738 if let Some(node) =
739 trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
740 {
741 return Some(node);
742 }
743 } else {
744 for t in anc_node.traits(db).iter() {
745 if let Some(node) =
746 trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
747 {
748 return Some(node);
749 }
750 }
751 for m in anc_node.mixins(db).iter() {
752 if visited_mixins.insert(m.to_string()) {
753 if let Some(node) =
754 lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
755 {
756 return Some(node);
757 }
758 }
759 }
760 }
761 }
762 }
763 None
764}
765
766fn trait_provides_method_node(
770 db: &dyn MirDatabase,
771 trait_fqcn: &str,
772 method_lower: &str,
773 visited: &mut HashSet<String>,
774) -> Option<MethodNode> {
775 if !visited.insert(trait_fqcn.to_string()) {
776 return None;
777 }
778 if let Some(node) = db
779 .lookup_method_node(trait_fqcn, method_lower)
780 .filter(|n| n.active(db))
781 {
782 return Some(node);
783 }
784 let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
785 if !node.is_trait(db) {
786 return None;
787 }
788 for t in node.traits(db).iter() {
789 if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
790 return Some(found);
791 }
792 }
793 None
794}
795
796pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
797 let lower = method_name.to_lowercase();
798 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
799 return false;
800 };
801 if db
803 .lookup_method_node(fqcn, &lower)
804 .is_some_and(|m| m.active(db))
805 {
806 return true;
807 }
808 let mut visited_traits: HashSet<String> = HashSet::new();
810 for t in self_node.traits(db).iter() {
811 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
812 return true;
813 }
814 }
815 for ancestor in class_ancestors(db, self_node).0.iter() {
817 if db
818 .lookup_method_node(ancestor.as_ref(), &lower)
819 .is_some_and(|m| m.active(db))
820 {
821 return true;
822 }
823 if let Some(anc_node) = db
824 .lookup_class_node(ancestor.as_ref())
825 .filter(|n| n.active(db))
826 {
827 if anc_node.is_trait(db) {
828 if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
829 return true;
830 }
831 } else {
832 for t in anc_node.traits(db).iter() {
833 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
834 return true;
835 }
836 }
837 }
838 }
839 }
840 false
841}
842
843fn trait_declares_method(
847 db: &dyn MirDatabase,
848 trait_fqcn: &str,
849 method_lower: &str,
850 visited: &mut HashSet<String>,
851) -> bool {
852 if !visited.insert(trait_fqcn.to_string()) {
853 return false;
854 }
855 if db
856 .lookup_method_node(trait_fqcn, method_lower)
857 .is_some_and(|m| m.active(db))
858 {
859 return true;
860 }
861 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
862 return false;
863 };
864 if !node.is_trait(db) {
865 return false;
866 }
867 for t in node.traits(db).iter() {
868 if trait_declares_method(db, t.as_ref(), method_lower, visited) {
869 return true;
870 }
871 }
872 false
873}
874
875pub fn lookup_property_in_chain(
885 db: &dyn MirDatabase,
886 fqcn: &str,
887 prop_name: &str,
888) -> Option<PropertyNode> {
889 let mut visited_mixins: HashSet<String> = HashSet::new();
890 lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
891}
892
893fn lookup_property_in_chain_inner(
894 db: &dyn MirDatabase,
895 fqcn: &str,
896 prop_name: &str,
897 visited_mixins: &mut HashSet<String>,
898) -> Option<PropertyNode> {
899 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
900
901 if let Some(node) = db
903 .lookup_property_node(fqcn, prop_name)
904 .filter(|n| n.active(db))
905 {
906 return Some(node);
907 }
908 for m in self_node.mixins(db).iter() {
911 if visited_mixins.insert(m.to_string()) {
912 if let Some(node) =
913 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
914 {
915 return Some(node);
916 }
917 }
918 }
919 for ancestor in class_ancestors(db, self_node).0.iter() {
923 if let Some(node) = db
924 .lookup_property_node(ancestor.as_ref(), prop_name)
925 .filter(|n| n.active(db))
926 {
927 return Some(node);
928 }
929 if let Some(anc_node) = db
930 .lookup_class_node(ancestor.as_ref())
931 .filter(|n| n.active(db))
932 {
933 for m in anc_node.mixins(db).iter() {
934 if visited_mixins.insert(m.to_string()) {
935 if let Some(node) =
936 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
937 {
938 return Some(node);
939 }
940 }
941 }
942 }
943 }
944 None
945}
946
947pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
957 if db
958 .lookup_class_constant_node(fqcn, const_name)
959 .is_some_and(|n| n.active(db))
960 {
961 return true;
962 }
963 let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
964 return false;
965 };
966 for ancestor in class_ancestors(db, class_node).0.iter() {
967 if db
968 .lookup_class_constant_node(ancestor.as_ref(), const_name)
969 .is_some_and(|n| n.active(db))
970 {
971 return true;
972 }
973 }
974 false
975}
976
977pub fn member_location_via_db(
986 db: &dyn MirDatabase,
987 fqcn: &str,
988 member_name: &str,
989) -> Option<Location> {
990 if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
991 if let Some(loc) = node.location(db) {
992 return Some(loc);
993 }
994 }
995 if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
996 if let Some(loc) = node.location(db) {
997 return Some(loc);
998 }
999 }
1000 if let Some(node) = db
1002 .lookup_class_constant_node(fqcn, member_name)
1003 .filter(|n| n.active(db))
1004 {
1005 if let Some(loc) = node.location(db) {
1006 return Some(loc);
1007 }
1008 }
1009 let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
1010 for ancestor in class_ancestors(db, class_node).0.iter() {
1011 if let Some(node) = db
1012 .lookup_class_constant_node(ancestor.as_ref(), member_name)
1013 .filter(|n| n.active(db))
1014 {
1015 if let Some(loc) = node.location(db) {
1016 return Some(loc);
1017 }
1018 }
1019 }
1020 None
1021}
1022
1023pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
1037 if child == ancestor {
1038 return true;
1039 }
1040 let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
1041 return false;
1042 };
1043 if node.is_enum(db) {
1044 if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
1048 return true;
1049 }
1050 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
1051 return true;
1052 }
1053 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
1054 return true;
1055 }
1056 return false;
1057 }
1058 class_ancestors(db, node)
1059 .0
1060 .iter()
1061 .any(|p| p.as_ref() == ancestor)
1062}
1063
1064#[salsa::tracked]
1069pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
1070 let path = file.path(db);
1071 let text = file.text(db);
1072
1073 let arena = bumpalo::Bump::new();
1074 let parsed = php_rs_parser::parse(&arena, &text);
1075
1076 let mut all_issues: Vec<Issue> = parsed
1077 .errors
1078 .iter()
1079 .map(|err| {
1080 Issue::new(
1081 mir_issues::IssueKind::ParseError {
1082 message: err.to_string(),
1083 },
1084 mir_issues::Location {
1085 file: path.clone(),
1086 line: 1,
1087 line_end: 1,
1088 col_start: 0,
1089 col_end: 0,
1090 },
1091 )
1092 })
1093 .collect();
1094
1095 let collector =
1096 crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
1097 let (slice, collector_issues) = collector.collect_slice(&parsed.program);
1098 all_issues.extend(collector_issues);
1099
1100 FileDefinitions {
1101 slice: Arc::new(slice),
1102 issues: Arc::new(all_issues),
1103 }
1104}
1105
1106#[salsa::db]
1118#[derive(Default, Clone)]
1119pub struct MirDb {
1120 storage: salsa::Storage<Self>,
1121 class_nodes: HashMap<Arc<str>, ClassNode>,
1124 function_nodes: HashMap<Arc<str>, FunctionNode>,
1126 method_nodes: HashMap<Arc<str>, HashMap<Arc<str>, MethodNode>>,
1128 property_nodes: HashMap<Arc<str>, HashMap<Arc<str>, PropertyNode>>,
1130 class_constant_nodes: HashMap<Arc<str>, HashMap<Arc<str>, ClassConstantNode>>,
1132 global_constant_nodes: HashMap<Arc<str>, GlobalConstantNode>,
1134}
1135
1136#[salsa::db]
1137impl salsa::Database for MirDb {}
1138
1139#[salsa::db]
1140impl MirDatabase for MirDb {
1141 fn php_version_str(&self) -> Arc<str> {
1142 Arc::from("8.2")
1143 }
1144
1145 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
1146 self.class_nodes.get(fqcn).copied()
1147 }
1148
1149 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
1150 self.function_nodes.get(fqn).copied()
1151 }
1152
1153 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
1154 self.method_nodes
1155 .get(fqcn)
1156 .and_then(|m| m.get(method_name_lower).copied())
1157 }
1158
1159 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
1160 self.property_nodes
1161 .get(fqcn)
1162 .and_then(|m| m.get(prop_name).copied())
1163 }
1164
1165 fn lookup_class_constant_node(
1166 &self,
1167 fqcn: &str,
1168 const_name: &str,
1169 ) -> Option<ClassConstantNode> {
1170 self.class_constant_nodes
1171 .get(fqcn)
1172 .and_then(|m| m.get(const_name).copied())
1173 }
1174
1175 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
1176 self.global_constant_nodes.get(fqn).copied()
1177 }
1178
1179 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
1180 self.method_nodes
1181 .get(fqcn)
1182 .map(|m| m.values().copied().collect())
1183 .unwrap_or_default()
1184 }
1185
1186 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
1187 self.property_nodes
1188 .get(fqcn)
1189 .map(|m| m.values().copied().collect())
1190 .unwrap_or_default()
1191 }
1192
1193 fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
1194 self.class_nodes
1195 .iter()
1196 .filter_map(|(fqcn, node)| {
1197 if node.active(self) {
1198 Some(fqcn.clone())
1199 } else {
1200 None
1201 }
1202 })
1203 .collect()
1204 }
1205
1206 fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
1207 self.function_nodes
1208 .iter()
1209 .filter_map(|(fqn, node)| {
1210 if node.active(self) {
1211 Some(fqn.clone())
1212 } else {
1213 None
1214 }
1215 })
1216 .collect()
1217 }
1218}
1219
1220#[derive(Default)]
1236#[allow(clippy::type_complexity)]
1237pub struct InferredReturnTypes {
1238 functions: std::sync::Mutex<Vec<(Arc<str>, Union)>>,
1240 methods: std::sync::Mutex<Vec<(Arc<str>, Arc<str>, Union)>>,
1244}
1245
1246impl InferredReturnTypes {
1247 pub fn new() -> Self {
1248 Self::default()
1249 }
1250
1251 pub fn push_function(&self, fqn: Arc<str>, inferred: Union) {
1252 if let Ok(mut g) = self.functions.lock() {
1253 g.push((fqn, inferred));
1254 }
1255 }
1256
1257 pub fn push_method(&self, fqcn: Arc<str>, name: Arc<str>, inferred: Union) {
1258 if let Ok(mut g) = self.methods.lock() {
1259 g.push((fqcn, name, inferred));
1260 }
1261 }
1262}
1263
1264#[derive(Debug, Clone, Default)]
1272pub struct ClassNodeFields {
1273 pub fqcn: Arc<str>,
1274 pub is_interface: bool,
1275 pub is_trait: bool,
1276 pub is_enum: bool,
1277 pub is_abstract: bool,
1278 pub parent: Option<Arc<str>>,
1279 pub interfaces: Arc<[Arc<str>]>,
1280 pub traits: Arc<[Arc<str>]>,
1281 pub extends: Arc<[Arc<str>]>,
1282 pub template_params: Arc<[TemplateParam]>,
1283 pub require_extends: Arc<[Arc<str>]>,
1284 pub require_implements: Arc<[Arc<str>]>,
1285 pub is_backed_enum: bool,
1286 pub mixins: Arc<[Arc<str>]>,
1287 pub deprecated: Option<Arc<str>>,
1288 pub enum_scalar_type: Option<Union>,
1289 pub is_final: bool,
1290 pub is_readonly: bool,
1291 pub location: Option<Location>,
1292 pub extends_type_args: Arc<[Union]>,
1293 pub implements_type_args: ImplementsTypeArgs,
1294}
1295
1296impl ClassNodeFields {
1297 pub fn for_class(fqcn: Arc<str>) -> Self {
1298 Self {
1299 fqcn,
1300 ..Self::default()
1301 }
1302 }
1303
1304 pub fn for_interface(fqcn: Arc<str>) -> Self {
1305 Self {
1306 fqcn,
1307 is_interface: true,
1308 ..Self::default()
1309 }
1310 }
1311
1312 pub fn for_trait(fqcn: Arc<str>) -> Self {
1313 Self {
1314 fqcn,
1315 is_trait: true,
1316 ..Self::default()
1317 }
1318 }
1319
1320 pub fn for_enum(fqcn: Arc<str>) -> Self {
1321 Self {
1322 fqcn,
1323 is_enum: true,
1324 ..Self::default()
1325 }
1326 }
1327}
1328
1329impl MirDb {
1330 #[allow(clippy::too_many_arguments)]
1335 pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
1336 use salsa::Setter as _;
1337 let ClassNodeFields {
1338 fqcn,
1339 is_interface,
1340 is_trait,
1341 is_enum,
1342 is_abstract,
1343 parent,
1344 interfaces,
1345 traits,
1346 extends,
1347 template_params,
1348 require_extends,
1349 require_implements,
1350 is_backed_enum,
1351 mixins,
1352 deprecated,
1353 enum_scalar_type,
1354 is_final,
1355 is_readonly,
1356 location,
1357 extends_type_args,
1358 implements_type_args,
1359 } = fields;
1360 if let Some(&node) = self.class_nodes.get(&fqcn) {
1361 if node.active(self)
1374 && node.is_interface(self) == is_interface
1375 && node.is_trait(self) == is_trait
1376 && node.is_enum(self) == is_enum
1377 && node.is_abstract(self) == is_abstract
1378 && node.is_backed_enum(self) == is_backed_enum
1379 && node.parent(self) == parent
1380 && *node.interfaces(self) == *interfaces
1381 && *node.traits(self) == *traits
1382 && *node.extends(self) == *extends
1383 && *node.template_params(self) == *template_params
1384 && *node.require_extends(self) == *require_extends
1385 && *node.require_implements(self) == *require_implements
1386 && *node.mixins(self) == *mixins
1387 && node.deprecated(self) == deprecated
1388 && node.enum_scalar_type(self) == enum_scalar_type
1389 && node.is_final(self) == is_final
1390 && node.is_readonly(self) == is_readonly
1391 && node.location(self) == location
1392 && *node.extends_type_args(self) == *extends_type_args
1393 && *node.implements_type_args(self) == *implements_type_args
1394 {
1395 return node;
1396 }
1397 node.set_active(self).to(true);
1398 node.set_is_interface(self).to(is_interface);
1399 node.set_is_trait(self).to(is_trait);
1400 node.set_is_enum(self).to(is_enum);
1401 node.set_is_abstract(self).to(is_abstract);
1402 node.set_parent(self).to(parent);
1403 node.set_interfaces(self).to(interfaces);
1404 node.set_traits(self).to(traits);
1405 node.set_extends(self).to(extends);
1406 node.set_template_params(self).to(template_params);
1407 node.set_require_extends(self).to(require_extends);
1408 node.set_require_implements(self).to(require_implements);
1409 node.set_is_backed_enum(self).to(is_backed_enum);
1410 node.set_mixins(self).to(mixins);
1411 node.set_deprecated(self).to(deprecated);
1412 node.set_enum_scalar_type(self).to(enum_scalar_type);
1413 node.set_is_final(self).to(is_final);
1414 node.set_is_readonly(self).to(is_readonly);
1415 node.set_location(self).to(location);
1416 node.set_extends_type_args(self).to(extends_type_args);
1417 node.set_implements_type_args(self).to(implements_type_args);
1418 node
1419 } else {
1420 let node = ClassNode::new(
1421 self,
1422 fqcn.clone(),
1423 true,
1424 is_interface,
1425 is_trait,
1426 is_enum,
1427 is_abstract,
1428 parent,
1429 interfaces,
1430 traits,
1431 extends,
1432 template_params,
1433 require_extends,
1434 require_implements,
1435 is_backed_enum,
1436 mixins,
1437 deprecated,
1438 enum_scalar_type,
1439 is_final,
1440 is_readonly,
1441 location,
1442 extends_type_args,
1443 implements_type_args,
1444 );
1445 self.class_nodes.insert(fqcn, node);
1446 node
1447 }
1448 }
1449
1450 pub fn deactivate_class_node(&mut self, fqcn: &str) {
1455 use salsa::Setter as _;
1456 if let Some(&node) = self.class_nodes.get(fqcn) {
1457 node.set_active(self).to(false);
1458 }
1459 }
1460
1461 pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
1463 use salsa::Setter as _;
1464 let fqn = &storage.fqn;
1465 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1466 if node.active(self)
1472 && node.short_name(self) == storage.short_name
1473 && node.is_pure(self) == storage.is_pure
1474 && node.deprecated(self) == storage.deprecated
1475 && node.return_type(self) == storage.return_type
1476 && node.location(self) == storage.location
1477 && *node.params(self) == *storage.params.as_slice()
1478 && *node.template_params(self) == *storage.template_params.as_slice()
1479 && *node.assertions(self) == *storage.assertions.as_slice()
1480 && *node.throws(self) == *storage.throws.as_slice()
1481 {
1482 return node;
1483 }
1484 node.set_active(self).to(true);
1485 node.set_short_name(self).to(storage.short_name.clone());
1486 node.set_params(self)
1487 .to(Arc::from(storage.params.as_slice()));
1488 node.set_return_type(self).to(storage.return_type.clone());
1489 node.set_template_params(self)
1490 .to(Arc::from(storage.template_params.as_slice()));
1491 node.set_assertions(self)
1492 .to(Arc::from(storage.assertions.as_slice()));
1493 node.set_throws(self)
1494 .to(Arc::from(storage.throws.as_slice()));
1495 node.set_deprecated(self).to(storage.deprecated.clone());
1496 node.set_is_pure(self).to(storage.is_pure);
1497 node.set_location(self).to(storage.location.clone());
1498 node
1499 } else {
1500 let node = FunctionNode::new(
1501 self,
1502 fqn.clone(),
1503 storage.short_name.clone(),
1504 true,
1505 Arc::from(storage.params.as_slice()),
1506 storage.return_type.clone(),
1507 storage.inferred_return_type.clone(),
1508 Arc::from(storage.template_params.as_slice()),
1509 Arc::from(storage.assertions.as_slice()),
1510 Arc::from(storage.throws.as_slice()),
1511 storage.deprecated.clone(),
1512 storage.is_pure,
1513 storage.location.clone(),
1514 );
1515 self.function_nodes.insert(fqn.clone(), node);
1516 node
1517 }
1518 }
1519
1520 pub fn commit_inferred_return_types(&mut self, buf: &InferredReturnTypes) {
1532 use salsa::Setter as _;
1533 let funcs = std::mem::take(&mut *buf.functions.lock().expect("inferred buffer poisoned"));
1534 for (fqn, inferred) in funcs {
1535 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1536 if !node.active(self) {
1537 continue;
1538 }
1539 let new = Some(inferred);
1540 if node.inferred_return_type(self) == new {
1541 continue;
1542 }
1543 node.set_inferred_return_type(self).to(new);
1544 }
1545 }
1546 let methods = std::mem::take(&mut *buf.methods.lock().expect("inferred buffer poisoned"));
1547 for (fqcn, name, inferred) in methods {
1548 let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
1549 name.clone()
1550 } else {
1551 Arc::from(name.to_lowercase().as_str())
1552 };
1553 let node = self
1554 .method_nodes
1555 .get(fqcn.as_ref())
1556 .and_then(|m| m.get(&name_lower))
1557 .copied();
1558 if let Some(node) = node {
1559 if !node.active(self) {
1560 continue;
1561 }
1562 let new = Some(inferred);
1563 if node.inferred_return_type(self) == new {
1564 continue;
1565 }
1566 node.set_inferred_return_type(self).to(new);
1567 }
1568 }
1569 }
1570
1571 pub fn deactivate_function_node(&mut self, fqn: &str) {
1573 use salsa::Setter as _;
1574 if let Some(&node) = self.function_nodes.get(fqn) {
1575 node.set_active(self).to(false);
1576 }
1577 }
1578
1579 pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
1581 use salsa::Setter as _;
1582 let fqcn = &storage.fqcn;
1583 let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
1584 let existing = self
1587 .method_nodes
1588 .get(fqcn.as_ref())
1589 .and_then(|m| m.get(&name_lower))
1590 .copied();
1591 if let Some(node) = existing {
1592 if node.active(self)
1596 && node.visibility(self) == storage.visibility
1597 && node.is_static(self) == storage.is_static
1598 && node.is_abstract(self) == storage.is_abstract
1599 && node.is_final(self) == storage.is_final
1600 && node.is_constructor(self) == storage.is_constructor
1601 && node.is_pure(self) == storage.is_pure
1602 && node.deprecated(self) == storage.deprecated
1603 && node.return_type(self) == storage.return_type
1604 && node.location(self) == storage.location
1605 && *node.params(self) == *storage.params.as_slice()
1606 && *node.template_params(self) == *storage.template_params.as_slice()
1607 && *node.assertions(self) == *storage.assertions.as_slice()
1608 && *node.throws(self) == *storage.throws.as_slice()
1609 {
1610 return node;
1611 }
1612 node.set_active(self).to(true);
1613 node.set_params(self)
1614 .to(Arc::from(storage.params.as_slice()));
1615 node.set_return_type(self).to(storage.return_type.clone());
1616 node.set_template_params(self)
1617 .to(Arc::from(storage.template_params.as_slice()));
1618 node.set_assertions(self)
1619 .to(Arc::from(storage.assertions.as_slice()));
1620 node.set_throws(self)
1621 .to(Arc::from(storage.throws.as_slice()));
1622 node.set_deprecated(self).to(storage.deprecated.clone());
1623 node.set_visibility(self).to(storage.visibility);
1624 node.set_is_static(self).to(storage.is_static);
1625 node.set_is_abstract(self).to(storage.is_abstract);
1626 node.set_is_final(self).to(storage.is_final);
1627 node.set_is_constructor(self).to(storage.is_constructor);
1628 node.set_is_pure(self).to(storage.is_pure);
1629 node.set_location(self).to(storage.location.clone());
1630 node
1631 } else {
1632 let node = MethodNode::new(
1634 self,
1635 fqcn.clone(),
1636 storage.name.clone(),
1637 true,
1638 Arc::from(storage.params.as_slice()),
1639 storage.return_type.clone(),
1640 storage.inferred_return_type.clone(),
1641 Arc::from(storage.template_params.as_slice()),
1642 Arc::from(storage.assertions.as_slice()),
1643 Arc::from(storage.throws.as_slice()),
1644 storage.deprecated.clone(),
1645 storage.visibility,
1646 storage.is_static,
1647 storage.is_abstract,
1648 storage.is_final,
1649 storage.is_constructor,
1650 storage.is_pure,
1651 storage.location.clone(),
1652 );
1653 self.method_nodes
1654 .entry(fqcn.clone())
1655 .or_default()
1656 .insert(name_lower, node);
1657 node
1658 }
1659 }
1660
1661 pub fn deactivate_class_methods(&mut self, fqcn: &str) {
1663 use salsa::Setter as _;
1664 let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
1665 Some(methods) => methods.values().copied().collect(),
1666 None => return,
1667 };
1668 for node in nodes {
1669 node.set_active(self).to(false);
1670 }
1671 }
1672
1673 pub fn prune_class_methods(
1679 &mut self,
1680 fqcn: &str,
1681 keep_lower: &std::collections::HashSet<Arc<str>>,
1682 ) {
1683 use salsa::Setter as _;
1684 let candidates: Vec<MethodNode> = self
1685 .method_nodes
1686 .get(fqcn)
1687 .map(|m| {
1688 m.iter()
1689 .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
1690 .map(|(_, n)| *n)
1691 .collect()
1692 })
1693 .unwrap_or_default();
1694 for node in candidates {
1695 if node.active(self) {
1696 node.set_active(self).to(false);
1697 }
1698 }
1699 }
1700
1701 pub fn prune_class_properties(
1703 &mut self,
1704 fqcn: &str,
1705 keep: &std::collections::HashSet<Arc<str>>,
1706 ) {
1707 use salsa::Setter as _;
1708 let candidates: Vec<PropertyNode> = self
1709 .property_nodes
1710 .get(fqcn)
1711 .map(|m| {
1712 m.iter()
1713 .filter(|(k, _)| !keep.contains(k.as_ref()))
1714 .map(|(_, n)| *n)
1715 .collect()
1716 })
1717 .unwrap_or_default();
1718 for node in candidates {
1719 if node.active(self) {
1720 node.set_active(self).to(false);
1721 }
1722 }
1723 }
1724
1725 pub fn prune_class_constants(
1727 &mut self,
1728 fqcn: &str,
1729 keep: &std::collections::HashSet<Arc<str>>,
1730 ) {
1731 use salsa::Setter as _;
1732 let candidates: Vec<ClassConstantNode> = self
1733 .class_constant_nodes
1734 .get(fqcn)
1735 .map(|m| {
1736 m.iter()
1737 .filter(|(k, _)| !keep.contains(k.as_ref()))
1738 .map(|(_, n)| *n)
1739 .collect()
1740 })
1741 .unwrap_or_default();
1742 for node in candidates {
1743 if node.active(self) {
1744 node.set_active(self).to(false);
1745 }
1746 }
1747 }
1748
1749 pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
1751 use salsa::Setter as _;
1752 let existing = self
1753 .property_nodes
1754 .get(fqcn.as_ref())
1755 .and_then(|m| m.get(storage.name.as_ref()))
1756 .copied();
1757 if let Some(node) = existing {
1758 if node.active(self)
1760 && node.visibility(self) == storage.visibility
1761 && node.is_static(self) == storage.is_static
1762 && node.is_readonly(self) == storage.is_readonly
1763 && node.ty(self) == storage.ty
1764 && node.location(self) == storage.location
1765 {
1766 return;
1767 }
1768 node.set_active(self).to(true);
1769 node.set_ty(self).to(storage.ty.clone());
1770 node.set_visibility(self).to(storage.visibility);
1771 node.set_is_static(self).to(storage.is_static);
1772 node.set_is_readonly(self).to(storage.is_readonly);
1773 node.set_location(self).to(storage.location.clone());
1774 } else {
1775 let node = PropertyNode::new(
1776 self,
1777 fqcn.clone(),
1778 storage.name.clone(),
1779 true,
1780 storage.ty.clone(),
1781 storage.visibility,
1782 storage.is_static,
1783 storage.is_readonly,
1784 storage.location.clone(),
1785 );
1786 self.property_nodes
1787 .entry(fqcn.clone())
1788 .or_default()
1789 .insert(storage.name.clone(), node);
1790 }
1791 }
1792
1793 pub fn deactivate_class_properties(&mut self, fqcn: &str) {
1795 use salsa::Setter as _;
1796 let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
1797 Some(props) => props.values().copied().collect(),
1798 None => return,
1799 };
1800 for node in nodes {
1801 node.set_active(self).to(false);
1802 }
1803 }
1804
1805 pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
1807 use salsa::Setter as _;
1808 let existing = self
1809 .class_constant_nodes
1810 .get(fqcn.as_ref())
1811 .and_then(|m| m.get(storage.name.as_ref()))
1812 .copied();
1813 if let Some(node) = existing {
1814 if node.active(self)
1816 && node.visibility(self) == storage.visibility
1817 && node.is_final(self) == storage.is_final
1818 && node.ty(self) == storage.ty
1819 && node.location(self) == storage.location
1820 {
1821 return;
1822 }
1823 node.set_active(self).to(true);
1824 node.set_ty(self).to(storage.ty.clone());
1825 node.set_visibility(self).to(storage.visibility);
1826 node.set_is_final(self).to(storage.is_final);
1827 node.set_location(self).to(storage.location.clone());
1828 } else {
1829 let node = ClassConstantNode::new(
1830 self,
1831 fqcn.clone(),
1832 storage.name.clone(),
1833 true,
1834 storage.ty.clone(),
1835 storage.visibility,
1836 storage.is_final,
1837 storage.location.clone(),
1838 );
1839 self.class_constant_nodes
1840 .entry(fqcn.clone())
1841 .or_default()
1842 .insert(storage.name.clone(), node);
1843 }
1844 }
1845
1846 pub fn ingest_codebase(&mut self, codebase: &Codebase) {
1853 use std::collections::HashSet;
1854 for entry in codebase.classes.iter() {
1855 let cls = entry.value();
1856 self.upsert_class_node(ClassNodeFields {
1857 is_abstract: cls.is_abstract,
1858 parent: cls.parent.clone(),
1859 interfaces: Arc::from(cls.interfaces.as_slice()),
1860 traits: Arc::from(cls.traits.as_slice()),
1861 template_params: Arc::from(cls.template_params.as_slice()),
1862 mixins: Arc::from(cls.mixins.as_slice()),
1863 deprecated: cls.deprecated.clone(),
1864 is_final: cls.is_final,
1865 is_readonly: cls.is_readonly,
1866 location: cls.location.clone(),
1867 extends_type_args: Arc::from(cls.extends_type_args.as_slice()),
1868 implements_type_args: Arc::from(
1869 cls.implements_type_args
1870 .iter()
1871 .map(|(iface, args)| (iface.clone(), Arc::from(args.as_slice())))
1872 .collect::<Vec<_>>(),
1873 ),
1874 ..ClassNodeFields::for_class(cls.fqcn.clone())
1875 });
1876 let method_keep: HashSet<Arc<str>> = cls
1877 .own_methods
1878 .values()
1879 .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1880 .collect();
1881 self.prune_class_methods(&cls.fqcn, &method_keep);
1882 for method in cls.own_methods.values() {
1883 self.upsert_method_node(method.as_ref());
1884 }
1885 let prop_keep: HashSet<Arc<str>> = cls
1886 .own_properties
1887 .values()
1888 .map(|p| p.name.clone())
1889 .collect();
1890 self.prune_class_properties(&cls.fqcn, &prop_keep);
1891 for prop in cls.own_properties.values() {
1892 self.upsert_property_node(&cls.fqcn, prop);
1893 }
1894 let const_keep: HashSet<Arc<str>> =
1895 cls.own_constants.values().map(|c| c.name.clone()).collect();
1896 self.prune_class_constants(&cls.fqcn, &const_keep);
1897 for constant in cls.own_constants.values() {
1898 self.upsert_class_constant_node(&cls.fqcn, constant);
1899 }
1900 }
1901 for entry in codebase.interfaces.iter() {
1902 let iface = entry.value();
1903 self.upsert_class_node(ClassNodeFields {
1904 extends: Arc::from(iface.extends.as_slice()),
1905 template_params: Arc::from(iface.template_params.as_slice()),
1906 location: iface.location.clone(),
1907 ..ClassNodeFields::for_interface(iface.fqcn.clone())
1908 });
1909 let method_keep: HashSet<Arc<str>> = iface
1910 .own_methods
1911 .values()
1912 .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1913 .collect();
1914 self.prune_class_methods(&iface.fqcn, &method_keep);
1915 for method in iface.own_methods.values() {
1916 self.upsert_method_node(method.as_ref());
1917 }
1918 let const_keep: HashSet<Arc<str>> = iface
1919 .own_constants
1920 .values()
1921 .map(|c| c.name.clone())
1922 .collect();
1923 self.prune_class_constants(&iface.fqcn, &const_keep);
1924 for constant in iface.own_constants.values() {
1925 self.upsert_class_constant_node(&iface.fqcn, constant);
1926 }
1927 }
1928 for entry in codebase.traits.iter() {
1929 let tr = entry.value();
1930 self.upsert_class_node(ClassNodeFields {
1931 traits: Arc::from(tr.traits.as_slice()),
1932 template_params: Arc::from(tr.template_params.as_slice()),
1933 require_extends: Arc::from(tr.require_extends.as_slice()),
1934 require_implements: Arc::from(tr.require_implements.as_slice()),
1935 location: tr.location.clone(),
1936 ..ClassNodeFields::for_trait(tr.fqcn.clone())
1937 });
1938 let method_keep: HashSet<Arc<str>> = tr
1939 .own_methods
1940 .values()
1941 .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1942 .collect();
1943 self.prune_class_methods(&tr.fqcn, &method_keep);
1944 for method in tr.own_methods.values() {
1945 self.upsert_method_node(method.as_ref());
1946 }
1947 let prop_keep: HashSet<Arc<str>> =
1948 tr.own_properties.values().map(|p| p.name.clone()).collect();
1949 self.prune_class_properties(&tr.fqcn, &prop_keep);
1950 for prop in tr.own_properties.values() {
1951 self.upsert_property_node(&tr.fqcn, prop);
1952 }
1953 let const_keep: HashSet<Arc<str>> =
1954 tr.own_constants.values().map(|c| c.name.clone()).collect();
1955 self.prune_class_constants(&tr.fqcn, &const_keep);
1956 for constant in tr.own_constants.values() {
1957 self.upsert_class_constant_node(&tr.fqcn, constant);
1958 }
1959 }
1960 for entry in codebase.enums.iter() {
1961 let en = entry.value();
1962 self.upsert_class_node(ClassNodeFields {
1963 interfaces: Arc::from(en.interfaces.as_slice()),
1964 is_backed_enum: en.scalar_type.is_some(),
1965 enum_scalar_type: en.scalar_type.clone(),
1966 location: en.location.clone(),
1967 ..ClassNodeFields::for_enum(en.fqcn.clone())
1968 });
1969 let mut method_keep: HashSet<Arc<str>> = en
1970 .own_methods
1971 .values()
1972 .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1973 .collect();
1974 method_keep.insert(Arc::from("cases"));
1975 if en.scalar_type.is_some() {
1976 method_keep.insert(Arc::from("from"));
1977 method_keep.insert(Arc::from("tryfrom"));
1978 }
1979 self.prune_class_methods(&en.fqcn, &method_keep);
1980 for method in en.own_methods.values() {
1981 self.upsert_method_node(method.as_ref());
1982 }
1983 let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
1989 fqcn: en.fqcn.clone(),
1990 name: Arc::from(name),
1991 params: vec![],
1992 return_type: Some(Union::mixed()),
1993 inferred_return_type: None,
1994 visibility: Visibility::Public,
1995 is_static: true,
1996 is_abstract: false,
1997 is_constructor: false,
1998 template_params: vec![],
1999 assertions: vec![],
2000 throws: vec![],
2001 is_final: false,
2002 is_internal: false,
2003 is_pure: false,
2004 deprecated: None,
2005 location: None,
2006 };
2007 let already = |name: &str| {
2008 en.own_methods
2009 .keys()
2010 .any(|k| k.as_ref().eq_ignore_ascii_case(name))
2011 };
2012 if !already("cases") {
2013 self.upsert_method_node(&synth_method("cases"));
2014 }
2015 if en.scalar_type.is_some() {
2016 if !already("from") {
2017 self.upsert_method_node(&synth_method("from"));
2018 }
2019 if !already("tryFrom") {
2020 self.upsert_method_node(&synth_method("tryFrom"));
2021 }
2022 }
2023 let mut const_keep: HashSet<Arc<str>> =
2024 en.own_constants.values().map(|c| c.name.clone()).collect();
2025 for case in en.cases.values() {
2026 const_keep.insert(case.name.clone());
2027 }
2028 self.prune_class_constants(&en.fqcn, &const_keep);
2029 for constant in en.own_constants.values() {
2030 self.upsert_class_constant_node(&en.fqcn, constant);
2031 }
2032 for case in en.cases.values() {
2033 let case_const = ConstantStorage {
2034 name: case.name.clone(),
2035 ty: mir_types::Union::mixed(),
2036 visibility: None,
2037 is_final: false,
2038 location: case.location.clone(),
2039 };
2040 self.upsert_class_constant_node(&en.fqcn, &case_const);
2041 }
2042 }
2043 for entry in codebase.functions.iter() {
2044 self.upsert_function_node(entry.value());
2045 }
2046 for entry in codebase.constants.iter() {
2047 self.upsert_global_constant_node(entry.key().clone(), entry.value().clone());
2048 }
2049 }
2050
2051 pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
2053 use salsa::Setter as _;
2054 if let Some(&node) = self.global_constant_nodes.get(&fqn) {
2055 if node.active(self) && node.ty(self) == ty {
2057 return node;
2058 }
2059 node.set_active(self).to(true);
2060 node.set_ty(self).to(ty);
2061 node
2062 } else {
2063 let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
2064 self.global_constant_nodes.insert(fqn, node);
2065 node
2066 }
2067 }
2068
2069 pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
2071 use salsa::Setter as _;
2072 if let Some(&node) = self.global_constant_nodes.get(fqn) {
2073 node.set_active(self).to(false);
2074 }
2075 }
2076
2077 pub fn deactivate_class_constants(&mut self, fqcn: &str) {
2079 use salsa::Setter as _;
2080 let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
2081 Some(consts) => consts.values().copied().collect(),
2082 None => return,
2083 };
2084 for node in nodes {
2085 node.set_active(self).to(false);
2086 }
2087 }
2088}
2089
2090#[salsa::accumulator]
2116#[derive(Clone, Debug)]
2117pub struct IssueAccumulator(pub Issue);
2118
2119#[derive(Clone, Debug, PartialEq, Eq)]
2128pub struct RefLoc {
2129 pub symbol_key: Arc<str>,
2130 pub file: Arc<str>,
2131 pub line: u32,
2132 pub col_start: u16,
2133 pub col_end: u16,
2134}
2135
2136#[salsa::accumulator]
2143#[derive(Clone, Debug)]
2144pub struct RefLocAccumulator(pub RefLoc);
2145
2146#[salsa::input]
2151pub struct AnalyzeFileInput {
2152 pub php_version: Arc<str>,
2155}
2156
2157#[salsa::tracked]
2172pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, _input: AnalyzeFileInput) {
2173 use salsa::Accumulator as _;
2174 let path = file.path(db);
2175 let text = file.text(db);
2176
2177 let arena = bumpalo::Bump::new();
2178 let parsed = php_rs_parser::parse(&arena, &text);
2179
2180 for err in &parsed.errors {
2181 let issue = Issue::new(
2182 mir_issues::IssueKind::ParseError {
2183 message: err.to_string(),
2184 },
2185 mir_issues::Location {
2186 file: path.clone(),
2187 line: 1,
2188 line_end: 1,
2189 col_start: 0,
2190 col_end: 0,
2191 },
2192 );
2193 IssueAccumulator(issue).accumulate(db);
2194 }
2195}
2196
2197#[cfg(test)]
2202mod tests {
2203 use super::*;
2204 use salsa::Setter as _;
2205
2206 fn upsert_class(
2207 db: &mut MirDb,
2208 fqcn: &str,
2209 parent: Option<Arc<str>>,
2210 extends: Arc<[Arc<str>]>,
2211 is_interface: bool,
2212 ) -> ClassNode {
2213 db.upsert_class_node(ClassNodeFields {
2214 is_interface,
2215 parent,
2216 extends,
2217 ..ClassNodeFields::for_class(Arc::from(fqcn))
2218 })
2219 }
2220
2221 #[test]
2222 fn mirdb_constructs() {
2223 let _db = MirDb::default();
2224 }
2225
2226 #[test]
2227 fn source_file_input_roundtrip() {
2228 let db = MirDb::default();
2229 let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
2230 assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
2231 assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
2232 }
2233
2234 #[test]
2235 fn collect_file_definitions_basic() {
2236 let db = MirDb::default();
2237 let src = Arc::from("<?php class Foo {}");
2238 let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
2239 let defs = collect_file_definitions(&db, file);
2240 assert!(defs.issues.is_empty());
2241 assert_eq!(defs.slice.classes.len(), 1);
2242 assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
2243 }
2244
2245 #[test]
2246 fn collect_file_definitions_memoized() {
2247 let db = MirDb::default();
2248 let file = SourceFile::new(
2249 &db,
2250 Arc::from("/tmp/memo.php"),
2251 Arc::from("<?php class Bar {}"),
2252 );
2253
2254 let defs1 = collect_file_definitions(&db, file);
2255 let defs2 = collect_file_definitions(&db, file);
2256 assert!(
2257 Arc::ptr_eq(&defs1.slice, &defs2.slice),
2258 "unchanged file must return the memoized result"
2259 );
2260 }
2261
2262 #[test]
2263 fn analyze_file_accumulates_parse_errors() {
2264 let db = MirDb::default();
2265 let file = SourceFile::new(
2267 &db,
2268 Arc::from("/tmp/parse_err.php"),
2269 Arc::from("<?php $x = \"unterminated"),
2270 );
2271 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2272 analyze_file(&db, file, input);
2273 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2274 assert!(
2275 !issues.is_empty(),
2276 "expected parse error to surface as accumulated IssueAccumulator"
2277 );
2278 assert!(matches!(
2279 issues[0].0.kind,
2280 mir_issues::IssueKind::ParseError { .. }
2281 ));
2282 }
2283
2284 #[test]
2285 fn analyze_file_clean_input_accumulates_nothing() {
2286 let db = MirDb::default();
2287 let file = SourceFile::new(
2288 &db,
2289 Arc::from("/tmp/clean.php"),
2290 Arc::from("<?php class Foo {}"),
2291 );
2292 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2293 analyze_file(&db, file, input);
2294 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2295 let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
2296 assert!(issues.is_empty());
2297 assert!(refs.is_empty());
2298 }
2299
2300 #[test]
2301 fn collect_file_definitions_recomputes_on_change() {
2302 let mut db = MirDb::default();
2303 let file = SourceFile::new(
2304 &db,
2305 Arc::from("/tmp/memo2.php"),
2306 Arc::from("<?php class Foo {}"),
2307 );
2308
2309 let defs1 = collect_file_definitions(&db, file);
2310 file.set_text(&mut db)
2311 .to(Arc::from("<?php class Foo {} class Bar {}"));
2312 let defs2 = collect_file_definitions(&db, file);
2313
2314 assert!(
2315 !Arc::ptr_eq(&defs1.slice, &defs2.slice),
2316 "changed file must produce a new result"
2317 );
2318 assert_eq!(defs2.slice.classes.len(), 2);
2319 }
2320
2321 #[test]
2322 fn class_ancestors_empty_for_root_class() {
2323 let mut db = MirDb::default();
2324 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2325 let ancestors = class_ancestors(&db, node);
2326 assert!(ancestors.0.is_empty(), "root class has no ancestors");
2327 }
2328
2329 #[test]
2330 fn class_ancestors_single_parent() {
2331 let mut db = MirDb::default();
2332 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2333 let child = upsert_class(
2334 &mut db,
2335 "Child",
2336 Some(Arc::from("Base")),
2337 Arc::from([]),
2338 false,
2339 );
2340 let ancestors = class_ancestors(&db, child);
2341 assert_eq!(ancestors.0.len(), 1);
2342 assert_eq!(ancestors.0[0].as_ref(), "Base");
2343 }
2344
2345 #[test]
2346 fn class_ancestors_transitive() {
2347 let mut db = MirDb::default();
2348 upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
2349 upsert_class(
2350 &mut db,
2351 "Parent",
2352 Some(Arc::from("GrandParent")),
2353 Arc::from([]),
2354 false,
2355 );
2356 let child = upsert_class(
2357 &mut db,
2358 "Child",
2359 Some(Arc::from("Parent")),
2360 Arc::from([]),
2361 false,
2362 );
2363 let ancestors = class_ancestors(&db, child);
2364 assert_eq!(ancestors.0.len(), 2);
2365 assert_eq!(ancestors.0[0].as_ref(), "Parent");
2366 assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
2367 }
2368
2369 #[test]
2370 fn class_ancestors_cycle_returns_empty() {
2371 let mut db = MirDb::default();
2372 let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
2374 let ancestors = class_ancestors(&db, node_a);
2375 assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
2377 }
2378
2379 #[test]
2380 fn class_ancestors_inactive_node_returns_empty() {
2381 let mut db = MirDb::default();
2382 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2383 db.deactivate_class_node("Foo");
2384 let ancestors = class_ancestors(&db, node);
2385 assert!(ancestors.0.is_empty(), "inactive node must yield empty");
2386 }
2387
2388 #[test]
2389 fn class_ancestors_recomputes_on_parent_change() {
2390 let mut db = MirDb::default();
2391 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2392 let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
2393
2394 let before = class_ancestors(&db, child);
2395 assert!(before.0.is_empty());
2396
2397 child.set_parent(&mut db).to(Some(Arc::from("Base")));
2399
2400 let after = class_ancestors(&db, child);
2401 assert_eq!(after.0.len(), 1);
2402 assert_eq!(after.0[0].as_ref(), "Base");
2403 }
2404
2405 #[test]
2406 fn interface_ancestors_via_extends() {
2407 let mut db = MirDb::default();
2408 upsert_class(&mut db, "Countable", None, Arc::from([]), true);
2409 let child_iface = upsert_class(
2410 &mut db,
2411 "Collection",
2412 None,
2413 Arc::from([Arc::from("Countable")]),
2414 true,
2415 );
2416 let ancestors = class_ancestors(&db, child_iface);
2417 assert_eq!(ancestors.0.len(), 1);
2418 assert_eq!(ancestors.0[0].as_ref(), "Countable");
2419 }
2420
2421 #[test]
2422 fn type_exists_via_db_tracks_active_state() {
2423 let mut db = MirDb::default();
2424 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2425 assert!(type_exists_via_db(&db, "Foo"));
2426 assert!(!type_exists_via_db(&db, "Bar"));
2427 db.deactivate_class_node("Foo");
2428 assert!(!type_exists_via_db(&db, "Foo"));
2429 }
2430
2431 #[test]
2432 fn clone_preserves_class_node_lookups() {
2433 let mut db = MirDb::default();
2436 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2437 let cloned = db.clone();
2438 assert!(
2439 type_exists_via_db(&cloned, "Foo"),
2440 "clone must observe nodes registered before clone()"
2441 );
2442 assert!(
2443 !type_exists_via_db(&cloned, "Bar"),
2444 "clone must not observe nodes that were never registered"
2445 );
2446 let foo_node = cloned.lookup_class_node("Foo").expect("registered");
2448 let ancestors = class_ancestors(&cloned, foo_node);
2449 assert!(ancestors.0.is_empty(), "Foo has no ancestors");
2450 }
2451
2452 fn upsert_class_with_traits(
2457 db: &mut MirDb,
2458 fqcn: &str,
2459 parent: Option<Arc<str>>,
2460 traits: &[&str],
2461 is_interface: bool,
2462 is_trait: bool,
2463 ) -> ClassNode {
2464 db.upsert_class_node(ClassNodeFields {
2465 is_interface,
2466 is_trait,
2467 parent,
2468 traits: Arc::from(
2469 traits
2470 .iter()
2471 .map(|t| Arc::<str>::from(*t))
2472 .collect::<Vec<_>>(),
2473 ),
2474 ..ClassNodeFields::for_class(Arc::from(fqcn))
2475 })
2476 }
2477
2478 fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
2479 let storage = MethodStorage {
2480 name: Arc::from(name),
2481 fqcn: Arc::from(fqcn),
2482 params: vec![],
2483 return_type: None,
2484 inferred_return_type: None,
2485 visibility: Visibility::Public,
2486 is_static: false,
2487 is_abstract,
2488 is_final: false,
2489 is_constructor: name == "__construct",
2490 template_params: vec![],
2491 assertions: vec![],
2492 throws: vec![],
2493 deprecated: None,
2494 is_internal: false,
2495 is_pure: false,
2496 location: None,
2497 };
2498 db.upsert_method_node(&storage)
2499 }
2500
2501 fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
2502 db.upsert_class_node(ClassNodeFields {
2503 interfaces: Arc::from(
2504 interfaces
2505 .iter()
2506 .map(|i| Arc::<str>::from(*i))
2507 .collect::<Vec<_>>(),
2508 ),
2509 is_backed_enum: is_backed,
2510 ..ClassNodeFields::for_enum(Arc::from(fqcn))
2511 })
2512 }
2513
2514 #[test]
2519 fn method_exists_via_db_finds_own_method() {
2520 let mut db = MirDb::default();
2521 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2522 upsert_method(&mut db, "Foo", "bar", false);
2523 assert!(method_exists_via_db(&db, "Foo", "bar"));
2524 assert!(!method_exists_via_db(&db, "Foo", "missing"));
2525 }
2526
2527 #[test]
2528 fn method_exists_via_db_walks_parent() {
2529 let mut db = MirDb::default();
2530 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2531 upsert_method(&mut db, "Base", "inherited", false);
2532 upsert_class(
2533 &mut db,
2534 "Child",
2535 Some(Arc::from("Base")),
2536 Arc::from([]),
2537 false,
2538 );
2539 assert!(method_exists_via_db(&db, "Child", "inherited"));
2540 }
2541
2542 #[test]
2543 fn method_exists_via_db_walks_traits_transitively() {
2544 let mut db = MirDb::default();
2545 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2546 upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
2547 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2548 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2549 assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
2550 }
2551
2552 #[test]
2553 fn method_exists_via_db_is_case_insensitive() {
2554 let mut db = MirDb::default();
2555 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2556 upsert_method(&mut db, "Foo", "doStuff", false);
2557 assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
2559 assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
2560 }
2561
2562 #[test]
2563 fn method_exists_via_db_unknown_class_returns_false() {
2564 let db = MirDb::default();
2565 assert!(!method_exists_via_db(&db, "Nope", "anything"));
2566 }
2567
2568 #[test]
2569 fn method_exists_via_db_inactive_class_returns_false() {
2570 let mut db = MirDb::default();
2571 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2572 upsert_method(&mut db, "Foo", "bar", false);
2573 db.deactivate_class_node("Foo");
2574 assert!(!method_exists_via_db(&db, "Foo", "bar"));
2575 }
2576
2577 #[test]
2578 fn method_exists_via_db_finds_abstract_methods() {
2579 let mut db = MirDb::default();
2582 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2583 upsert_method(&mut db, "Foo", "abstr", true);
2584 assert!(method_exists_via_db(&db, "Foo", "abstr"));
2585 }
2586
2587 #[test]
2592 fn method_is_concretely_implemented_skips_abstract() {
2593 let mut db = MirDb::default();
2594 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2595 upsert_method(&mut db, "Foo", "abstr", true);
2596 assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
2597 }
2598
2599 #[test]
2600 fn method_is_concretely_implemented_finds_concrete_in_trait() {
2601 let mut db = MirDb::default();
2602 upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
2603 upsert_method(&mut db, "MyTrait", "provided", false);
2604 upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
2605 assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
2606 }
2607
2608 #[test]
2609 fn method_is_concretely_implemented_skips_interface_definitions() {
2610 let mut db = MirDb::default();
2613 upsert_class(&mut db, "I", None, Arc::from([]), true);
2614 upsert_method(&mut db, "I", "m", false);
2615 upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
2616 assert!(!method_is_concretely_implemented(&db, "C", "m"));
2618 }
2619
2620 #[test]
2625 fn extends_or_implements_via_db_self_match() {
2626 let mut db = MirDb::default();
2627 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2628 assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
2629 }
2630
2631 #[test]
2632 fn extends_or_implements_via_db_transitive() {
2633 let mut db = MirDb::default();
2634 upsert_class(&mut db, "Animal", None, Arc::from([]), false);
2635 upsert_class(
2636 &mut db,
2637 "Mammal",
2638 Some(Arc::from("Animal")),
2639 Arc::from([]),
2640 false,
2641 );
2642 upsert_class(
2643 &mut db,
2644 "Dog",
2645 Some(Arc::from("Mammal")),
2646 Arc::from([]),
2647 false,
2648 );
2649 assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
2650 assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
2651 assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
2652 }
2653
2654 #[test]
2655 fn extends_or_implements_via_db_unknown_returns_false() {
2656 let db = MirDb::default();
2657 assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
2658 }
2659
2660 #[test]
2661 fn extends_or_implements_via_db_unit_enum_implicit() {
2662 let mut db = MirDb::default();
2663 upsert_enum(&mut db, "Status", &[], false);
2664 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2665 assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
2666 assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2668 }
2669
2670 #[test]
2671 fn extends_or_implements_via_db_backed_enum_implicit() {
2672 let mut db = MirDb::default();
2673 upsert_enum(&mut db, "Status", &[], true);
2674 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2675 assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2676 assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
2677 }
2678
2679 #[test]
2680 fn extends_or_implements_via_db_enum_declared_interface() {
2681 let mut db = MirDb::default();
2682 upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
2683 upsert_enum(&mut db, "Status", &["Stringable"], false);
2684 assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
2685 }
2686
2687 #[test]
2692 fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
2693 let mut db = MirDb::default();
2694 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2695 upsert_class(
2696 &mut db,
2697 "Child",
2698 Some(Arc::from("Base")),
2699 Arc::from([]),
2700 false,
2701 );
2702 assert!(!has_unknown_ancestor_via_db(&db, "Child"));
2703 }
2704
2705 #[test]
2706 fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
2707 let mut db = MirDb::default();
2708 upsert_class(
2710 &mut db,
2711 "Child",
2712 Some(Arc::from("Missing")),
2713 Arc::from([]),
2714 false,
2715 );
2716 assert!(has_unknown_ancestor_via_db(&db, "Child"));
2717 }
2718
2719 #[test]
2720 fn class_template_params_via_db_returns_registered_params() {
2721 use mir_types::Variance;
2722 let mut db = MirDb::default();
2723 let tp = TemplateParam {
2724 name: Arc::from("T"),
2725 bound: None,
2726 defining_entity: Arc::from("Box"),
2727 variance: Variance::Invariant,
2728 };
2729 db.upsert_class_node(ClassNodeFields {
2730 template_params: Arc::from([tp.clone()]),
2731 ..ClassNodeFields::for_class(Arc::from("Box"))
2732 });
2733 let got = class_template_params_via_db(&db, "Box").expect("registered");
2734 assert_eq!(got.len(), 1);
2735 assert_eq!(got[0].name.as_ref(), "T");
2736
2737 assert!(class_template_params_via_db(&db, "Missing").is_none());
2738 db.deactivate_class_node("Box");
2739 assert!(class_template_params_via_db(&db, "Box").is_none());
2740 }
2741
2742 fn upsert_class_with_mixins(
2747 db: &mut MirDb,
2748 fqcn: &str,
2749 parent: Option<Arc<str>>,
2750 mixins: &[&str],
2751 ) -> ClassNode {
2752 db.upsert_class_node(ClassNodeFields {
2753 parent,
2754 mixins: Arc::from(
2755 mixins
2756 .iter()
2757 .map(|m| Arc::<str>::from(*m))
2758 .collect::<Vec<_>>(),
2759 ),
2760 ..ClassNodeFields::for_class(Arc::from(fqcn))
2761 })
2762 }
2763
2764 #[test]
2765 fn lookup_method_in_chain_finds_own_then_ancestor() {
2766 let mut db = MirDb::default();
2767 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2768 upsert_method(&mut db, "Base", "shared", false);
2769 upsert_class(
2770 &mut db,
2771 "Child",
2772 Some(Arc::from("Base")),
2773 Arc::from([]),
2774 false,
2775 );
2776 upsert_method(&mut db, "Child", "shared", false);
2777 let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
2779 assert_eq!(found.fqcn(&db).as_ref(), "Child");
2780 upsert_method(&mut db, "Base", "only_in_base", false);
2782 let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
2783 assert_eq!(found.fqcn(&db).as_ref(), "Base");
2784 }
2785
2786 #[test]
2787 fn lookup_method_in_chain_walks_trait_of_traits() {
2788 let mut db = MirDb::default();
2789 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2790 upsert_method(&mut db, "InnerTrait", "deep", false);
2791 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2792 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2793 let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
2794 assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
2795 }
2796
2797 #[test]
2798 fn lookup_method_in_chain_walks_mixins() {
2799 let mut db = MirDb::default();
2800 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
2801 upsert_method(&mut db, "MixinTarget", "magic", false);
2802 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
2803 let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
2804 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
2805 }
2806
2807 #[test]
2808 fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
2809 let mut db = MirDb::default();
2810 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
2812 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
2813 assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
2814 }
2815
2816 #[test]
2817 fn lookup_method_in_chain_is_case_insensitive() {
2818 let mut db = MirDb::default();
2819 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2820 upsert_method(&mut db, "Foo", "doStuff", false);
2821 assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
2822 assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
2823 }
2824
2825 #[test]
2826 fn lookup_method_in_chain_unknown_returns_none() {
2827 let db = MirDb::default();
2828 assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
2829 }
2830
2831 fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
2836 let storage = PropertyStorage {
2837 name: Arc::from(name),
2838 ty: None,
2839 inferred_ty: None,
2840 visibility: Visibility::Public,
2841 is_static: false,
2842 is_readonly,
2843 default: None,
2844 location: None,
2845 };
2846 let owner = Arc::<str>::from(fqcn);
2847 db.upsert_property_node(&owner, &storage);
2848 db.lookup_property_node(fqcn, name).expect("registered")
2849 }
2850
2851 #[test]
2852 fn lookup_property_in_chain_own_then_ancestor() {
2853 let mut db = MirDb::default();
2854 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2855 upsert_property(&mut db, "Base", "x", false);
2856 upsert_class(
2857 &mut db,
2858 "Child",
2859 Some(Arc::from("Base")),
2860 Arc::from([]),
2861 false,
2862 );
2863 let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
2865 assert_eq!(found.fqcn(&db).as_ref(), "Base");
2866 upsert_property(&mut db, "Child", "x", true);
2868 let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
2869 assert_eq!(found.fqcn(&db).as_ref(), "Child");
2870 assert!(found.is_readonly(&db));
2871 }
2872
2873 #[test]
2874 fn lookup_property_in_chain_walks_mixins() {
2875 let mut db = MirDb::default();
2876 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
2877 upsert_property(&mut db, "MixinTarget", "exposed", false);
2878 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
2879 let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
2880 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
2881 }
2882
2883 #[test]
2884 fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
2885 let mut db = MirDb::default();
2886 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
2887 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
2888 assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
2889 }
2890
2891 #[test]
2892 fn lookup_property_in_chain_is_case_sensitive() {
2893 let mut db = MirDb::default();
2894 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2895 upsert_property(&mut db, "Foo", "myProp", false);
2896 assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
2897 assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
2899 }
2900
2901 #[test]
2902 fn lookup_property_in_chain_inactive_returns_none() {
2903 let mut db = MirDb::default();
2904 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2905 upsert_property(&mut db, "Foo", "x", false);
2906 db.deactivate_class_node("Foo");
2907 assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
2908 }
2909
2910 fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
2915 let storage = ConstantStorage {
2916 name: Arc::from(name),
2917 ty: mir_types::Union::mixed(),
2918 visibility: None,
2919 is_final: false,
2920 location: None,
2921 };
2922 let owner = Arc::<str>::from(fqcn);
2923 db.upsert_class_constant_node(&owner, &storage);
2924 }
2925
2926 #[test]
2927 fn class_constant_exists_in_chain_finds_own() {
2928 let mut db = MirDb::default();
2929 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2930 upsert_constant(&mut db, "Foo", "MAX");
2931 assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
2932 assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
2933 }
2934
2935 #[test]
2936 fn class_constant_exists_in_chain_walks_parent() {
2937 let mut db = MirDb::default();
2938 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2939 upsert_constant(&mut db, "Base", "VERSION");
2940 upsert_class(
2941 &mut db,
2942 "Child",
2943 Some(Arc::from("Base")),
2944 Arc::from([]),
2945 false,
2946 );
2947 assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
2948 }
2949
2950 #[test]
2951 fn class_constant_exists_in_chain_walks_interface() {
2952 let mut db = MirDb::default();
2953 upsert_class(&mut db, "I", None, Arc::from([]), true);
2954 upsert_constant(&mut db, "I", "TYPE");
2955 db.upsert_class_node(ClassNodeFields {
2958 interfaces: Arc::from([Arc::from("I")]),
2959 ..ClassNodeFields::for_class(Arc::from("Impl"))
2960 });
2961 assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
2962 }
2963
2964 #[test]
2965 fn class_constant_exists_in_chain_walks_direct_trait() {
2966 let mut db = MirDb::default();
2967 upsert_class_with_traits(&mut db, "T", None, &[], false, true);
2968 upsert_constant(&mut db, "T", "FROM_TRAIT");
2969 upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
2970 assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
2971 }
2972
2973 #[test]
2974 fn class_constant_exists_in_chain_unknown_class_returns_false() {
2975 let db = MirDb::default();
2976 assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
2977 }
2978
2979 #[test]
2980 fn class_constant_exists_in_chain_inactive_returns_false() {
2981 let mut db = MirDb::default();
2982 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2983 upsert_constant(&mut db, "Foo", "X");
2984 db.deactivate_class_node("Foo");
2985 db.deactivate_class_constants("Foo");
2986 assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
2987 }
2988
2989 #[test]
2995 fn parallel_reads_then_serial_write_does_not_deadlock() {
2996 use rayon::prelude::*;
2997 use std::sync::mpsc;
2998 use std::time::Duration;
2999
3000 let (tx, rx) = mpsc::channel::<()>();
3001 std::thread::spawn(move || {
3002 let mut db = MirDb::default();
3003 let storage = mir_codebase::storage::FunctionStorage {
3004 fqn: Arc::from("foo"),
3005 short_name: Arc::from("foo"),
3006 params: vec![],
3007 return_type: None,
3008 inferred_return_type: None,
3009 template_params: vec![],
3010 assertions: vec![],
3011 throws: vec![],
3012 deprecated: None,
3013 is_pure: false,
3014 location: None,
3015 };
3016 let node = db.upsert_function_node(&storage);
3017
3018 let db_for_sweep = db.clone();
3020 (0..256u32)
3021 .into_par_iter()
3022 .for_each_with(db_for_sweep, |db, _| {
3023 let _ = node.return_type(&*db as &dyn MirDatabase);
3024 });
3025
3026 node.set_return_type(&mut db).to(Some(Union::mixed()));
3030 assert_eq!(node.return_type(&db), Some(Union::mixed()));
3031 tx.send(()).unwrap();
3032 });
3033
3034 match rx.recv_timeout(Duration::from_secs(30)) {
3035 Ok(()) => {}
3036 Err(_) => {
3037 panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
3038 }
3039 }
3040 }
3041
3042 #[test]
3053 fn sibling_clone_blocks_setter_until_dropped() {
3054 use std::sync::mpsc;
3055 use std::time::Duration;
3056
3057 let mut db = MirDb::default();
3058 let storage = mir_codebase::storage::FunctionStorage {
3059 fqn: Arc::from("foo"),
3060 short_name: Arc::from("foo"),
3061 params: vec![],
3062 return_type: None,
3063 inferred_return_type: None,
3064 template_params: vec![],
3065 assertions: vec![],
3066 throws: vec![],
3067 deprecated: None,
3068 is_pure: false,
3069 location: None,
3070 };
3071 let node = db.upsert_function_node(&storage);
3072
3073 let sibling = db.clone();
3074
3075 let (tx, rx) = mpsc::channel::<()>();
3078 let writer = std::thread::spawn(move || {
3079 node.set_return_type(&mut db).to(Some(Union::mixed()));
3080 tx.send(()).unwrap();
3081 });
3082
3083 match rx.recv_timeout(Duration::from_millis(500)) {
3086 Err(mpsc::RecvTimeoutError::Timeout) => { }
3087 Ok(()) => panic!(
3088 "setter completed while sibling clone was alive — strong-count==1 \
3089 invariant of `cancel_others` is broken; commit_inferred_return_types \
3090 cannot rely on tight-scoping clones"
3091 ),
3092 Err(e) => panic!("unexpected channel error: {e:?}"),
3093 }
3094
3095 drop(sibling);
3097
3098 match rx.recv_timeout(Duration::from_secs(5)) {
3099 Ok(()) => {}
3100 Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
3101 }
3102 writer.join().expect("writer thread panicked");
3103 }
3104}