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 return format!("{}\\{}", ns, name);
325 }
326 name.to_string()
327}
328
329pub fn class_template_params_via_db(
334 db: &dyn MirDatabase,
335 fqcn: &str,
336) -> Option<Arc<[TemplateParam]>> {
337 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
338 Some(node.template_params(db))
339}
340
341pub fn inherited_template_bindings_via_db(
348 db: &dyn MirDatabase,
349 fqcn: &str,
350) -> std::collections::HashMap<Arc<str>, Union> {
351 let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
352 let mut visited: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
353 let mut current: Arc<str> = Arc::from(fqcn);
354 loop {
355 if !visited.insert(current.clone()) {
356 break;
357 }
358 let node = match db
359 .lookup_class_node(current.as_ref())
360 .filter(|n| n.active(db))
361 {
362 Some(n) => n,
363 None => break,
364 };
365 let parent = match node.parent(db) {
366 Some(p) => p,
367 None => break,
368 };
369 let extends_type_args = node.extends_type_args(db);
370 if !extends_type_args.is_empty() {
371 if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
372 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
373 bindings
374 .entry(tp.name.clone())
375 .or_insert_with(|| ty.clone());
376 }
377 }
378 }
379 current = parent;
380 }
381 bindings
382}
383
384#[salsa::input]
401pub struct FunctionNode {
402 pub fqn: Arc<str>,
403 pub short_name: Arc<str>,
404 pub active: bool,
405 pub params: Arc<[FnParam]>,
406 pub return_type: Option<Arc<Union>>,
407 pub inferred_return_type: Option<Arc<Union>>,
408 pub template_params: Arc<[TemplateParam]>,
409 pub assertions: Arc<[Assertion]>,
410 pub throws: Arc<[Arc<str>]>,
411 pub deprecated: Option<Arc<str>>,
412 pub is_pure: bool,
413 pub location: Option<Location>,
416}
417
418#[salsa::input]
435pub struct MethodNode {
436 pub fqcn: Arc<str>,
437 pub name: Arc<str>,
438 pub active: bool,
439 pub params: Arc<[FnParam]>,
440 pub return_type: Option<Arc<Union>>,
441 pub inferred_return_type: Option<Arc<Union>>,
442 pub template_params: Arc<[TemplateParam]>,
443 pub assertions: Arc<[Assertion]>,
444 pub throws: Arc<[Arc<str>]>,
445 pub deprecated: Option<Arc<str>>,
446 pub visibility: Visibility,
447 pub is_static: bool,
448 pub is_abstract: bool,
449 pub is_final: bool,
450 pub is_constructor: bool,
451 pub is_pure: bool,
452 pub location: Option<Location>,
455}
456
457#[salsa::input]
468pub struct PropertyNode {
469 pub fqcn: Arc<str>,
470 pub name: Arc<str>,
471 pub active: bool,
472 pub ty: Option<Union>,
473 pub visibility: Visibility,
474 pub is_static: bool,
475 pub is_readonly: bool,
476 pub location: Option<Location>,
477}
478
479#[salsa::input]
487pub struct ClassConstantNode {
488 pub fqcn: Arc<str>,
489 pub name: Arc<str>,
490 pub active: bool,
491 pub ty: Union,
492 pub visibility: Option<Visibility>,
493 pub is_final: bool,
494 pub location: Option<Location>,
498}
499
500#[salsa::input]
507pub struct GlobalConstantNode {
508 pub fqn: Arc<str>,
509 pub active: bool,
510 pub ty: Union,
511}
512
513#[derive(Clone, Debug, Default)]
522pub struct Ancestors(pub Vec<Arc<str>>);
523
524impl PartialEq for Ancestors {
525 fn eq(&self, other: &Self) -> bool {
526 self.0.len() == other.0.len()
527 && self
528 .0
529 .iter()
530 .zip(&other.0)
531 .all(|(a, b)| a.as_ref() == b.as_ref())
532 }
533}
534
535unsafe impl salsa::Update for Ancestors {
536 unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
537 let old = unsafe { &mut *old_ptr };
538 if *old == new_val {
539 return false;
540 }
541 *old = new_val;
542 true
543 }
544}
545
546fn ancestors_initial(_db: &dyn MirDatabase, _id: salsa::Id, _node: ClassNode) -> Ancestors {
551 Ancestors(vec![])
552}
553
554fn ancestors_cycle(
555 _db: &dyn MirDatabase,
556 _cycle: &salsa::Cycle,
557 _last: &Ancestors,
558 _value: Ancestors,
559 _node: ClassNode,
560) -> Ancestors {
561 Ancestors(vec![])
564}
565
566#[salsa::tracked(cycle_fn = ancestors_cycle, cycle_initial = ancestors_initial)]
576pub fn class_ancestors(db: &dyn MirDatabase, node: ClassNode) -> Ancestors {
577 if !node.active(db) {
578 return Ancestors(vec![]);
579 }
580 if node.is_enum(db) || node.is_trait(db) {
590 return Ancestors(vec![]);
591 }
592
593 let mut all: Vec<Arc<str>> = Vec::new();
594 let mut seen: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
595
596 let add =
597 |fqcn: &Arc<str>, all: &mut Vec<Arc<str>>, seen: &mut rustc_hash::FxHashSet<Arc<str>>| {
598 if seen.insert(fqcn.clone()) {
599 all.push(fqcn.clone());
600 }
601 };
602
603 if node.is_interface(db) {
604 for e in node.extends(db).iter() {
605 add(e, &mut all, &mut seen);
606 if let Some(parent_node) = db.lookup_class_node(e) {
607 for a in class_ancestors(db, parent_node).0 {
608 add(&a, &mut all, &mut seen);
609 }
610 }
611 }
612 } else {
613 if let Some(ref p) = node.parent(db) {
614 add(p, &mut all, &mut seen);
615 if let Some(parent_node) = db.lookup_class_node(p) {
616 for a in class_ancestors(db, parent_node).0 {
617 add(&a, &mut all, &mut seen);
618 }
619 }
620 }
621 for iface in node.interfaces(db).iter() {
622 add(iface, &mut all, &mut seen);
623 if let Some(iface_node) = db.lookup_class_node(iface) {
624 for a in class_ancestors(db, iface_node).0 {
625 add(&a, &mut all, &mut seen);
626 }
627 }
628 }
629 for t in node.traits(db).iter() {
630 add(t, &mut all, &mut seen);
631 }
632 }
633
634 Ancestors(all)
635}
636
637pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
645 let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
646 return false;
647 };
648 class_ancestors(db, node)
649 .0
650 .iter()
651 .any(|ancestor| !type_exists_via_db(db, ancestor))
652}
653
654pub fn method_is_concretely_implemented(
665 db: &dyn MirDatabase,
666 fqcn: &str,
667 method_name: &str,
668) -> bool {
669 let lower = method_name.to_lowercase();
670 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
671 return false;
672 };
673 if self_node.is_interface(db) {
676 return false;
677 }
678 if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
680 if !m.is_abstract(db) {
681 return true;
682 }
683 }
684 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
686 for t in self_node.traits(db).iter() {
687 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
688 return true;
689 }
690 }
691 for ancestor in class_ancestors(db, self_node).0.iter() {
694 let Some(anc_node) = db
695 .lookup_class_node(ancestor.as_ref())
696 .filter(|n| n.active(db))
697 else {
698 continue;
699 };
700 if anc_node.is_interface(db) {
701 continue;
702 }
703 if !anc_node.is_trait(db) {
705 if let Some(m) = db
706 .lookup_method_node(ancestor.as_ref(), &lower)
707 .filter(|m| m.active(db))
708 {
709 if !m.is_abstract(db) {
710 return true;
711 }
712 }
713 }
714 if anc_node.is_trait(db) {
717 if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
718 return true;
719 }
720 } else {
721 for t in anc_node.traits(db).iter() {
722 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
723 return true;
724 }
725 }
726 }
727 }
728 false
729}
730
731fn trait_provides_method(
735 db: &dyn MirDatabase,
736 trait_fqcn: &str,
737 method_lower: &str,
738 visited: &mut rustc_hash::FxHashSet<String>,
739) -> bool {
740 if !visited.insert(trait_fqcn.to_string()) {
741 return false;
742 }
743 if let Some(m) = db
744 .lookup_method_node(trait_fqcn, method_lower)
745 .filter(|m| m.active(db))
746 {
747 if !m.is_abstract(db) {
748 return true;
749 }
750 }
751 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
752 return false;
753 };
754 if !node.is_trait(db) {
755 return false;
756 }
757 for t in node.traits(db).iter() {
758 if trait_provides_method(db, t.as_ref(), method_lower, visited) {
759 return true;
760 }
761 }
762 false
763}
764
765pub fn lookup_method_in_chain(
780 db: &dyn MirDatabase,
781 fqcn: &str,
782 method_name: &str,
783) -> Option<MethodNode> {
784 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
785 lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
786}
787
788fn lookup_method_in_chain_inner(
789 db: &dyn MirDatabase,
790 fqcn: &str,
791 lower: &str,
792 visited_mixins: &mut rustc_hash::FxHashSet<String>,
793) -> Option<MethodNode> {
794 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
795
796 if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
798 return Some(node);
799 }
800 for m in self_node.mixins(db).iter() {
804 if visited_mixins.insert(m.to_string()) {
805 if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
806 {
807 return Some(node);
808 }
809 }
810 }
811 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
814 for t in self_node.traits(db).iter() {
815 if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
816 return Some(node);
817 }
818 }
819 for ancestor in class_ancestors(db, self_node).0.iter() {
821 if let Some(node) = db
822 .lookup_method_node(ancestor.as_ref(), lower)
823 .filter(|n| n.active(db))
824 {
825 return Some(node);
826 }
827 if let Some(anc_node) = db
828 .lookup_class_node(ancestor.as_ref())
829 .filter(|n| n.active(db))
830 {
831 if anc_node.is_trait(db) {
832 if let Some(node) =
833 trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
834 {
835 return Some(node);
836 }
837 } else {
838 for t in anc_node.traits(db).iter() {
839 if let Some(node) =
840 trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
841 {
842 return Some(node);
843 }
844 }
845 for m in anc_node.mixins(db).iter() {
846 if visited_mixins.insert(m.to_string()) {
847 if let Some(node) =
848 lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
849 {
850 return Some(node);
851 }
852 }
853 }
854 }
855 }
856 }
857 None
858}
859
860fn trait_provides_method_node(
864 db: &dyn MirDatabase,
865 trait_fqcn: &str,
866 method_lower: &str,
867 visited: &mut rustc_hash::FxHashSet<String>,
868) -> Option<MethodNode> {
869 if !visited.insert(trait_fqcn.to_string()) {
870 return None;
871 }
872 if let Some(node) = db
873 .lookup_method_node(trait_fqcn, method_lower)
874 .filter(|n| n.active(db))
875 {
876 return Some(node);
877 }
878 let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
879 if !node.is_trait(db) {
880 return None;
881 }
882 for t in node.traits(db).iter() {
883 if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
884 return Some(found);
885 }
886 }
887 None
888}
889
890pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
891 let lower = method_name.to_lowercase();
892 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
893 return false;
894 };
895 if db
897 .lookup_method_node(fqcn, &lower)
898 .is_some_and(|m| m.active(db))
899 {
900 return true;
901 }
902 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
904 for t in self_node.traits(db).iter() {
905 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
906 return true;
907 }
908 }
909 for ancestor in class_ancestors(db, self_node).0.iter() {
911 if db
912 .lookup_method_node(ancestor.as_ref(), &lower)
913 .is_some_and(|m| m.active(db))
914 {
915 return true;
916 }
917 if let Some(anc_node) = db
918 .lookup_class_node(ancestor.as_ref())
919 .filter(|n| n.active(db))
920 {
921 if anc_node.is_trait(db) {
922 if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
923 return true;
924 }
925 } else {
926 for t in anc_node.traits(db).iter() {
927 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
928 return true;
929 }
930 }
931 }
932 }
933 }
934 false
935}
936
937fn trait_declares_method(
941 db: &dyn MirDatabase,
942 trait_fqcn: &str,
943 method_lower: &str,
944 visited: &mut rustc_hash::FxHashSet<String>,
945) -> bool {
946 if !visited.insert(trait_fqcn.to_string()) {
947 return false;
948 }
949 if db
950 .lookup_method_node(trait_fqcn, method_lower)
951 .is_some_and(|m| m.active(db))
952 {
953 return true;
954 }
955 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
956 return false;
957 };
958 if !node.is_trait(db) {
959 return false;
960 }
961 for t in node.traits(db).iter() {
962 if trait_declares_method(db, t.as_ref(), method_lower, visited) {
963 return true;
964 }
965 }
966 false
967}
968
969pub fn lookup_property_in_chain(
979 db: &dyn MirDatabase,
980 fqcn: &str,
981 prop_name: &str,
982) -> Option<PropertyNode> {
983 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
984 lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
985}
986
987fn lookup_property_in_chain_inner(
988 db: &dyn MirDatabase,
989 fqcn: &str,
990 prop_name: &str,
991 visited_mixins: &mut rustc_hash::FxHashSet<String>,
992) -> Option<PropertyNode> {
993 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
994
995 if let Some(node) = db
997 .lookup_property_node(fqcn, prop_name)
998 .filter(|n| n.active(db))
999 {
1000 return Some(node);
1001 }
1002 for m in self_node.mixins(db).iter() {
1005 if visited_mixins.insert(m.to_string()) {
1006 if let Some(node) =
1007 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
1008 {
1009 return Some(node);
1010 }
1011 }
1012 }
1013 for ancestor in class_ancestors(db, self_node).0.iter() {
1017 if let Some(node) = db
1018 .lookup_property_node(ancestor.as_ref(), prop_name)
1019 .filter(|n| n.active(db))
1020 {
1021 return Some(node);
1022 }
1023 if let Some(anc_node) = db
1024 .lookup_class_node(ancestor.as_ref())
1025 .filter(|n| n.active(db))
1026 {
1027 for m in anc_node.mixins(db).iter() {
1028 if visited_mixins.insert(m.to_string()) {
1029 if let Some(node) =
1030 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
1031 {
1032 return Some(node);
1033 }
1034 }
1035 }
1036 }
1037 }
1038 None
1039}
1040
1041pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
1051 if db
1052 .lookup_class_constant_node(fqcn, const_name)
1053 .is_some_and(|n| n.active(db))
1054 {
1055 return true;
1056 }
1057 let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
1058 return false;
1059 };
1060 for ancestor in class_ancestors(db, class_node).0.iter() {
1061 if db
1062 .lookup_class_constant_node(ancestor.as_ref(), const_name)
1063 .is_some_and(|n| n.active(db))
1064 {
1065 return true;
1066 }
1067 }
1068 false
1069}
1070
1071pub fn member_location_via_db(
1080 db: &dyn MirDatabase,
1081 fqcn: &str,
1082 member_name: &str,
1083) -> Option<Location> {
1084 if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
1085 if let Some(loc) = node.location(db) {
1086 return Some(loc);
1087 }
1088 }
1089 if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
1090 if let Some(loc) = node.location(db) {
1091 return Some(loc);
1092 }
1093 }
1094 if let Some(node) = db
1096 .lookup_class_constant_node(fqcn, member_name)
1097 .filter(|n| n.active(db))
1098 {
1099 if let Some(loc) = node.location(db) {
1100 return Some(loc);
1101 }
1102 }
1103 let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
1104 for ancestor in class_ancestors(db, class_node).0.iter() {
1105 if let Some(node) = db
1106 .lookup_class_constant_node(ancestor.as_ref(), member_name)
1107 .filter(|n| n.active(db))
1108 {
1109 if let Some(loc) = node.location(db) {
1110 return Some(loc);
1111 }
1112 }
1113 }
1114 None
1115}
1116
1117pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
1130 if child == ancestor {
1131 return true;
1132 }
1133 let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
1134 return false;
1135 };
1136 if node.is_enum(db) {
1137 if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
1141 return true;
1142 }
1143 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
1144 return true;
1145 }
1146 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
1147 return true;
1148 }
1149 return false;
1150 }
1151 class_ancestors(db, node)
1152 .0
1153 .iter()
1154 .any(|p| p.as_ref() == ancestor)
1155}
1156
1157pub fn collect_file_definitions_uncached(
1166 db: &dyn MirDatabase,
1167 file: SourceFile,
1168) -> FileDefinitions {
1169 let path = file.path(db);
1170 let text = file.text(db);
1171
1172 let arena = bumpalo::Bump::new();
1173 let parsed = php_rs_parser::parse(&arena, &text);
1174
1175 let mut all_issues: Vec<Issue> = parsed
1176 .errors
1177 .iter()
1178 .map(|err| {
1179 Issue::new(
1180 mir_issues::IssueKind::ParseError {
1181 message: err.to_string(),
1182 },
1183 mir_issues::Location {
1184 file: path.clone(),
1185 line: 1,
1186 line_end: 1,
1187 col_start: 0,
1188 col_end: 0,
1189 },
1190 )
1191 })
1192 .collect();
1193
1194 let collector =
1195 crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
1196 let (slice, collector_issues) = collector.collect_slice(&parsed.program);
1197 all_issues.extend(collector_issues);
1198
1199 FileDefinitions {
1200 slice: Arc::new(slice),
1201 issues: Arc::new(all_issues),
1202 }
1203}
1204
1205#[salsa::tracked]
1206pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
1207 collect_file_definitions_uncached(db, file)
1208}
1209
1210type MemberRegistry<V> = Arc<FxHashMap<Arc<str>, FxHashMap<Arc<str>, V>>>;
1222type ReferenceLocations =
1223 Arc<std::sync::Mutex<FxHashMap<Arc<str>, Vec<(Arc<str>, u32, u16, u16)>>>>;
1224
1225#[salsa::db]
1226#[derive(Default, Clone)]
1227pub struct MirDb {
1228 storage: salsa::Storage<Self>,
1229 class_nodes: Arc<FxHashMap<Arc<str>, ClassNode>>,
1237 class_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
1241 function_nodes: Arc<FxHashMap<Arc<str>, FunctionNode>>,
1244 function_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
1248 method_nodes: MemberRegistry<MethodNode>,
1250 property_nodes: MemberRegistry<PropertyNode>,
1252 class_constant_nodes: MemberRegistry<ClassConstantNode>,
1254 global_constant_nodes: Arc<FxHashMap<Arc<str>, GlobalConstantNode>>,
1256 file_namespaces: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1258 file_imports: Arc<FxHashMap<Arc<str>, HashMap<String, String>>>,
1260 global_vars: Arc<FxHashMap<Arc<str>, Union>>,
1262 symbol_to_file: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1264 reference_locations: ReferenceLocations,
1266}
1267
1268#[salsa::db]
1269impl salsa::Database for MirDb {}
1270
1271#[salsa::db]
1272impl MirDatabase for MirDb {
1273 fn php_version_str(&self) -> Arc<str> {
1274 Arc::from("8.2")
1275 }
1276
1277 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
1278 if let Some(&node) = self.class_nodes.get(fqcn) {
1279 return Some(node);
1280 }
1281 let lower = fqcn.to_ascii_lowercase();
1282 let canonical = self.class_node_keys_lower.get(&lower)?;
1283 self.class_nodes.get(canonical.as_ref()).copied()
1284 }
1285
1286 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
1287 if let Some(&node) = self.function_nodes.get(fqn) {
1288 return Some(node);
1289 }
1290 let lower = fqn.to_ascii_lowercase();
1291 let canonical = self.function_node_keys_lower.get(&lower)?;
1292 self.function_nodes.get(canonical.as_ref()).copied()
1293 }
1294
1295 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
1296 self.method_nodes
1297 .get(fqcn)
1298 .and_then(|m| m.get(method_name_lower).copied())
1299 }
1300
1301 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
1302 self.property_nodes
1303 .get(fqcn)
1304 .and_then(|m| m.get(prop_name).copied())
1305 }
1306
1307 fn lookup_class_constant_node(
1308 &self,
1309 fqcn: &str,
1310 const_name: &str,
1311 ) -> Option<ClassConstantNode> {
1312 self.class_constant_nodes
1313 .get(fqcn)
1314 .and_then(|m| m.get(const_name).copied())
1315 }
1316
1317 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
1318 self.global_constant_nodes.get(fqn).copied()
1319 }
1320
1321 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
1322 self.method_nodes
1323 .get(fqcn)
1324 .map(|m| m.values().copied().collect())
1325 .unwrap_or_default()
1326 }
1327
1328 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
1329 self.property_nodes
1330 .get(fqcn)
1331 .map(|m| m.values().copied().collect())
1332 .unwrap_or_default()
1333 }
1334
1335 fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
1336 self.class_nodes
1337 .iter()
1338 .filter_map(|(fqcn, node)| {
1339 if node.active(self) {
1340 Some(fqcn.clone())
1341 } else {
1342 None
1343 }
1344 })
1345 .collect()
1346 }
1347
1348 fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
1349 self.function_nodes
1350 .iter()
1351 .filter_map(|(fqn, node)| {
1352 if node.active(self) {
1353 Some(fqn.clone())
1354 } else {
1355 None
1356 }
1357 })
1358 .collect()
1359 }
1360
1361 fn file_namespace(&self, file: &str) -> Option<Arc<str>> {
1362 self.file_namespaces.get(file).cloned()
1363 }
1364
1365 fn file_imports(&self, file: &str) -> HashMap<String, String> {
1366 self.file_imports.get(file).cloned().unwrap_or_default()
1367 }
1368
1369 fn global_var_type(&self, name: &str) -> Option<Union> {
1370 self.global_vars.get(name).cloned()
1371 }
1372
1373 fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)> {
1374 self.file_imports
1375 .iter()
1376 .map(|(file, imports)| (file.clone(), imports.clone()))
1377 .collect()
1378 }
1379
1380 fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>> {
1381 self.symbol_to_file.get(symbol).cloned()
1382 }
1383
1384 fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>> {
1385 self.symbol_to_file
1386 .iter()
1387 .filter_map(|(sym, defining_file)| {
1388 if defining_file.as_ref() == file {
1389 Some(sym.clone())
1390 } else {
1391 None
1392 }
1393 })
1394 .collect()
1395 }
1396
1397 fn record_reference_location(&self, loc: RefLoc) {
1398 let mut refs = self
1399 .reference_locations
1400 .lock()
1401 .expect("reference lock poisoned");
1402 let entry = refs.entry(loc.symbol_key).or_default();
1403 let tuple = (loc.file, loc.line, loc.col_start, loc.col_end);
1404 if !entry.iter().any(|existing| existing == &tuple) {
1405 entry.push(tuple);
1406 }
1407 }
1408
1409 fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
1410 for (symbol, line, col_start, col_end) in locs {
1411 self.record_reference_location(RefLoc {
1412 symbol_key: Arc::from(symbol.as_str()),
1413 file: file.clone(),
1414 line: *line,
1415 col_start: *col_start,
1416 col_end: *col_end,
1417 });
1418 }
1419 }
1420
1421 fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1422 let refs = self
1423 .reference_locations
1424 .lock()
1425 .expect("reference lock poisoned");
1426 let mut out = Vec::new();
1427 for (symbol, locs) in refs.iter() {
1428 for (loc_file, line, col_start, col_end) in locs {
1429 if loc_file.as_ref() == file {
1430 out.push((symbol.clone(), *line, *col_start, *col_end));
1431 }
1432 }
1433 }
1434 out
1435 }
1436
1437 fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1438 let refs = self
1439 .reference_locations
1440 .lock()
1441 .expect("reference lock poisoned");
1442 refs.get(symbol).cloned().unwrap_or_default()
1443 }
1444
1445 fn has_reference(&self, symbol: &str) -> bool {
1446 let refs = self
1447 .reference_locations
1448 .lock()
1449 .expect("reference lock poisoned");
1450 refs.get(symbol).is_some_and(|locs| !locs.is_empty())
1451 }
1452
1453 fn clear_file_references(&self, file: &str) {
1454 let mut refs = self
1455 .reference_locations
1456 .lock()
1457 .expect("reference lock poisoned");
1458 for locs in refs.values_mut() {
1459 locs.retain(|(loc_file, _, _, _)| loc_file.as_ref() != file);
1460 }
1461 }
1462}
1463
1464#[derive(Default)]
1480#[allow(clippy::type_complexity)]
1481pub struct InferredReturnTypes {
1482 functions: std::sync::Mutex<Vec<(Arc<str>, Union)>>,
1484 methods: std::sync::Mutex<Vec<(Arc<str>, Arc<str>, Union)>>,
1488}
1489
1490impl InferredReturnTypes {
1491 pub fn new() -> Self {
1492 Self::default()
1493 }
1494
1495 pub fn push_function(&self, fqn: Arc<str>, inferred: Union) {
1496 if let Ok(mut g) = self.functions.lock() {
1497 g.push((fqn, inferred));
1498 }
1499 }
1500
1501 pub fn push_method(&self, fqcn: Arc<str>, name: Arc<str>, inferred: Union) {
1502 if let Ok(mut g) = self.methods.lock() {
1503 g.push((fqcn, name, inferred));
1504 }
1505 }
1506}
1507
1508#[derive(Debug, Clone, Default)]
1516pub struct ClassNodeFields {
1517 pub fqcn: Arc<str>,
1518 pub is_interface: bool,
1519 pub is_trait: bool,
1520 pub is_enum: bool,
1521 pub is_abstract: bool,
1522 pub parent: Option<Arc<str>>,
1523 pub interfaces: Arc<[Arc<str>]>,
1524 pub traits: Arc<[Arc<str>]>,
1525 pub extends: Arc<[Arc<str>]>,
1526 pub template_params: Arc<[TemplateParam]>,
1527 pub require_extends: Arc<[Arc<str>]>,
1528 pub require_implements: Arc<[Arc<str>]>,
1529 pub is_backed_enum: bool,
1530 pub mixins: Arc<[Arc<str>]>,
1531 pub deprecated: Option<Arc<str>>,
1532 pub enum_scalar_type: Option<Union>,
1533 pub is_final: bool,
1534 pub is_readonly: bool,
1535 pub location: Option<Location>,
1536 pub extends_type_args: Arc<[Union]>,
1537 pub implements_type_args: ImplementsTypeArgs,
1538}
1539
1540impl ClassNodeFields {
1541 pub fn for_class(fqcn: Arc<str>) -> Self {
1542 Self {
1543 fqcn,
1544 ..Self::default()
1545 }
1546 }
1547
1548 pub fn for_interface(fqcn: Arc<str>) -> Self {
1549 Self {
1550 fqcn,
1551 is_interface: true,
1552 ..Self::default()
1553 }
1554 }
1555
1556 pub fn for_trait(fqcn: Arc<str>) -> Self {
1557 Self {
1558 fqcn,
1559 is_trait: true,
1560 ..Self::default()
1561 }
1562 }
1563
1564 pub fn for_enum(fqcn: Arc<str>) -> Self {
1565 Self {
1566 fqcn,
1567 is_enum: true,
1568 ..Self::default()
1569 }
1570 }
1571}
1572
1573impl MirDb {
1574 pub fn remove_file_definitions(&mut self, file: &str) {
1575 let symbols = self.symbols_defined_in_file(file);
1576 for symbol in &symbols {
1577 self.deactivate_class_node(symbol);
1578 self.deactivate_function_node(symbol);
1579 self.deactivate_class_methods(symbol);
1580 self.deactivate_class_properties(symbol);
1581 self.deactivate_class_constants(symbol);
1582 self.deactivate_global_constant_node(symbol);
1583 }
1584 let symbol_set: HashSet<Arc<str>> = symbols.into_iter().collect();
1585 Arc::make_mut(&mut self.symbol_to_file).retain(|sym, defining_file| {
1586 defining_file.as_ref() != file && !symbol_set.contains(sym)
1587 });
1588 Arc::make_mut(&mut self.file_namespaces).retain(|path, _| path.as_ref() != file);
1589 Arc::make_mut(&mut self.file_imports).retain(|path, _| path.as_ref() != file);
1590 Arc::make_mut(&mut self.global_vars).retain(|name, _| !symbol_set.contains(name));
1591 self.clear_file_references(file);
1592 }
1593
1594 pub fn type_count(&self) -> usize {
1595 self.class_nodes
1596 .values()
1597 .filter(|node| node.active(self))
1598 .count()
1599 }
1600
1601 pub fn function_count(&self) -> usize {
1602 self.function_nodes
1603 .values()
1604 .filter(|node| node.active(self))
1605 .count()
1606 }
1607
1608 pub fn constant_count(&self) -> usize {
1609 self.global_constant_nodes
1610 .values()
1611 .filter(|node| node.active(self))
1612 .count()
1613 }
1614
1615 pub fn ingest_stub_slice(&mut self, slice: &StubSlice) {
1621 use std::collections::HashSet;
1622
1623 let mut slice = slice.clone();
1626 mir_codebase::storage::deduplicate_params_in_slice(&mut slice);
1627
1628 if let Some(file) = &slice.file {
1629 if let Some(namespace) = &slice.namespace {
1630 Arc::make_mut(&mut self.file_namespaces).insert(file.clone(), namespace.clone());
1631 }
1632 if !slice.imports.is_empty() {
1633 Arc::make_mut(&mut self.file_imports).insert(file.clone(), slice.imports.clone());
1634 }
1635 for (name, _) in &slice.global_vars {
1636 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1637 Arc::make_mut(&mut self.symbol_to_file)
1638 .insert(Arc::from(global_name), file.clone());
1639 }
1640 }
1641 for (name, ty) in &slice.global_vars {
1642 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1643 Arc::make_mut(&mut self.global_vars).insert(Arc::from(global_name), ty.clone());
1644 }
1645
1646 for cls in &slice.classes {
1647 if let Some(file) = &slice.file {
1648 Arc::make_mut(&mut self.symbol_to_file).insert(cls.fqcn.clone(), file.clone());
1649 }
1650 self.upsert_class_node(ClassNodeFields {
1651 is_abstract: cls.is_abstract,
1652 parent: cls.parent.clone(),
1653 interfaces: Arc::from(cls.interfaces.as_ref()),
1654 traits: Arc::from(cls.traits.as_ref()),
1655 template_params: Arc::from(cls.template_params.as_ref()),
1656 mixins: Arc::from(cls.mixins.as_ref()),
1657 deprecated: cls.deprecated.clone(),
1658 is_final: cls.is_final,
1659 is_readonly: cls.is_readonly,
1660 location: cls.location.clone(),
1661 extends_type_args: Arc::from(cls.extends_type_args.as_ref()),
1662 implements_type_args: Arc::from(
1663 cls.implements_type_args
1664 .iter()
1665 .map(|(iface, args)| (iface.clone(), Arc::from(args.as_ref())))
1666 .collect::<Vec<_>>(),
1667 ),
1668 ..ClassNodeFields::for_class(cls.fqcn.clone())
1669 });
1670 if self.method_nodes.contains_key(cls.fqcn.as_ref()) {
1671 let method_keep: HashSet<&str> =
1672 cls.own_methods.keys().map(|m| m.as_ref()).collect();
1673 self.prune_class_methods(&cls.fqcn, &method_keep);
1674 }
1675 for method in cls.own_methods.values() {
1676 self.upsert_method_node(method.as_ref());
1681 }
1682 if self.property_nodes.contains_key(cls.fqcn.as_ref()) {
1683 let prop_keep: HashSet<&str> =
1684 cls.own_properties.keys().map(|p| p.as_ref()).collect();
1685 self.prune_class_properties(&cls.fqcn, &prop_keep);
1686 }
1687 for prop in cls.own_properties.values() {
1688 self.upsert_property_node(&cls.fqcn, prop);
1689 }
1690 if self.class_constant_nodes.contains_key(cls.fqcn.as_ref()) {
1691 let const_keep: HashSet<&str> =
1692 cls.own_constants.keys().map(|c| c.as_ref()).collect();
1693 self.prune_class_constants(&cls.fqcn, &const_keep);
1694 }
1695 for constant in cls.own_constants.values() {
1696 self.upsert_class_constant_node(&cls.fqcn, constant);
1697 }
1698 }
1699
1700 for iface in &slice.interfaces {
1701 if let Some(file) = &slice.file {
1702 Arc::make_mut(&mut self.symbol_to_file).insert(iface.fqcn.clone(), file.clone());
1703 }
1704 self.upsert_class_node(ClassNodeFields {
1705 extends: Arc::from(iface.extends.as_ref()),
1706 template_params: Arc::from(iface.template_params.as_ref()),
1707 location: iface.location.clone(),
1708 ..ClassNodeFields::for_interface(iface.fqcn.clone())
1709 });
1710 if self.method_nodes.contains_key(iface.fqcn.as_ref()) {
1711 let method_keep: HashSet<&str> =
1712 iface.own_methods.keys().map(|m| m.as_ref()).collect();
1713 self.prune_class_methods(&iface.fqcn, &method_keep);
1714 }
1715 for method in iface.own_methods.values() {
1716 self.upsert_method_node(method.as_ref());
1717 }
1718 if self.class_constant_nodes.contains_key(iface.fqcn.as_ref()) {
1719 let const_keep: HashSet<&str> =
1720 iface.own_constants.keys().map(|c| c.as_ref()).collect();
1721 self.prune_class_constants(&iface.fqcn, &const_keep);
1722 }
1723 for constant in iface.own_constants.values() {
1724 self.upsert_class_constant_node(&iface.fqcn, constant);
1725 }
1726 }
1727
1728 for tr in &slice.traits {
1729 if let Some(file) = &slice.file {
1730 Arc::make_mut(&mut self.symbol_to_file).insert(tr.fqcn.clone(), file.clone());
1731 }
1732 self.upsert_class_node(ClassNodeFields {
1733 traits: Arc::from(tr.traits.as_ref()),
1734 template_params: Arc::from(tr.template_params.as_ref()),
1735 require_extends: Arc::from(tr.require_extends.as_ref()),
1736 require_implements: Arc::from(tr.require_implements.as_ref()),
1737 location: tr.location.clone(),
1738 ..ClassNodeFields::for_trait(tr.fqcn.clone())
1739 });
1740 if self.method_nodes.contains_key(tr.fqcn.as_ref()) {
1741 let method_keep: HashSet<&str> =
1742 tr.own_methods.keys().map(|m| m.as_ref()).collect();
1743 self.prune_class_methods(&tr.fqcn, &method_keep);
1744 }
1745 for method in tr.own_methods.values() {
1746 self.upsert_method_node(method.as_ref());
1747 }
1748 if self.property_nodes.contains_key(tr.fqcn.as_ref()) {
1749 let prop_keep: HashSet<&str> =
1750 tr.own_properties.keys().map(|p| p.as_ref()).collect();
1751 self.prune_class_properties(&tr.fqcn, &prop_keep);
1752 }
1753 for prop in tr.own_properties.values() {
1754 self.upsert_property_node(&tr.fqcn, prop);
1755 }
1756 if self.class_constant_nodes.contains_key(tr.fqcn.as_ref()) {
1757 let const_keep: HashSet<&str> =
1758 tr.own_constants.keys().map(|c| c.as_ref()).collect();
1759 self.prune_class_constants(&tr.fqcn, &const_keep);
1760 }
1761 for constant in tr.own_constants.values() {
1762 self.upsert_class_constant_node(&tr.fqcn, constant);
1763 }
1764 }
1765
1766 for en in &slice.enums {
1767 if let Some(file) = &slice.file {
1768 Arc::make_mut(&mut self.symbol_to_file).insert(en.fqcn.clone(), file.clone());
1769 }
1770 self.upsert_class_node(ClassNodeFields {
1771 interfaces: Arc::from(en.interfaces.as_ref()),
1772 is_backed_enum: en.scalar_type.is_some(),
1773 enum_scalar_type: en.scalar_type.clone(),
1774 location: en.location.clone(),
1775 ..ClassNodeFields::for_enum(en.fqcn.clone())
1776 });
1777 if self.method_nodes.contains_key(en.fqcn.as_ref()) {
1778 let mut method_keep: HashSet<&str> =
1779 en.own_methods.keys().map(|m| m.as_ref()).collect();
1780 method_keep.insert("cases");
1781 if en.scalar_type.is_some() {
1782 method_keep.insert("from");
1783 method_keep.insert("tryfrom");
1784 }
1785 self.prune_class_methods(&en.fqcn, &method_keep);
1786 }
1787 for method in en.own_methods.values() {
1788 self.upsert_method_node(method.as_ref());
1789 }
1790 let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
1791 fqcn: en.fqcn.clone(),
1792 name: Arc::from(name),
1793 params: Arc::from([].as_ref()),
1794 return_type: Some(Arc::new(Union::mixed())),
1795 inferred_return_type: None,
1796 visibility: Visibility::Public,
1797 is_static: true,
1798 is_abstract: false,
1799 is_constructor: false,
1800 template_params: vec![],
1801 assertions: vec![],
1802 throws: vec![],
1803 is_final: false,
1804 is_internal: false,
1805 is_pure: false,
1806 deprecated: None,
1807 location: None,
1808 };
1809 let already = |name: &str| {
1810 en.own_methods
1811 .keys()
1812 .any(|k| k.as_ref().eq_ignore_ascii_case(name))
1813 };
1814 if !already("cases") {
1815 self.upsert_method_node(&synth_method("cases"));
1816 }
1817 if en.scalar_type.is_some() {
1818 if !already("from") {
1819 self.upsert_method_node(&synth_method("from"));
1820 }
1821 if !already("tryFrom") {
1822 self.upsert_method_node(&synth_method("tryFrom"));
1823 }
1824 }
1825 if self.class_constant_nodes.contains_key(en.fqcn.as_ref()) {
1826 let mut const_keep: HashSet<&str> =
1827 en.own_constants.keys().map(|c| c.as_ref()).collect();
1828 for case in en.cases.values() {
1829 const_keep.insert(case.name.as_ref());
1830 }
1831 self.prune_class_constants(&en.fqcn, &const_keep);
1832 }
1833 for constant in en.own_constants.values() {
1834 self.upsert_class_constant_node(&en.fqcn, constant);
1835 }
1836 for case in en.cases.values() {
1837 let case_const = ConstantStorage {
1838 name: case.name.clone(),
1839 ty: mir_types::Union::mixed(),
1840 visibility: None,
1841 is_final: false,
1842 location: case.location.clone(),
1843 };
1844 self.upsert_class_constant_node(&en.fqcn, &case_const);
1845 }
1846 }
1847
1848 for func in &slice.functions {
1849 if let Some(file) = &slice.file {
1850 Arc::make_mut(&mut self.symbol_to_file).insert(func.fqn.clone(), file.clone());
1851 }
1852 self.upsert_function_node(func);
1853 }
1854 for (fqn, ty) in &slice.constants {
1855 self.upsert_global_constant_node(fqn.clone(), ty.clone());
1856 }
1857 }
1858
1859 #[allow(clippy::too_many_arguments)]
1864 pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
1865 use salsa::Setter as _;
1866 let ClassNodeFields {
1867 fqcn,
1868 is_interface,
1869 is_trait,
1870 is_enum,
1871 is_abstract,
1872 parent,
1873 interfaces,
1874 traits,
1875 extends,
1876 template_params,
1877 require_extends,
1878 require_implements,
1879 is_backed_enum,
1880 mixins,
1881 deprecated,
1882 enum_scalar_type,
1883 is_final,
1884 is_readonly,
1885 location,
1886 extends_type_args,
1887 implements_type_args,
1888 } = fields;
1889 if let Some(&node) = self.class_nodes.get(&fqcn) {
1890 if node.active(self)
1903 && node.is_interface(self) == is_interface
1904 && node.is_trait(self) == is_trait
1905 && node.is_enum(self) == is_enum
1906 && node.is_abstract(self) == is_abstract
1907 && node.is_backed_enum(self) == is_backed_enum
1908 && node.parent(self) == parent
1909 && *node.interfaces(self) == *interfaces
1910 && *node.traits(self) == *traits
1911 && *node.extends(self) == *extends
1912 && *node.template_params(self) == *template_params
1913 && *node.require_extends(self) == *require_extends
1914 && *node.require_implements(self) == *require_implements
1915 && *node.mixins(self) == *mixins
1916 && node.deprecated(self) == deprecated
1917 && node.enum_scalar_type(self) == enum_scalar_type
1918 && node.is_final(self) == is_final
1919 && node.is_readonly(self) == is_readonly
1920 && node.location(self) == location
1921 && *node.extends_type_args(self) == *extends_type_args
1922 && *node.implements_type_args(self) == *implements_type_args
1923 {
1924 return node;
1925 }
1926 node.set_active(self).to(true);
1927 node.set_is_interface(self).to(is_interface);
1928 node.set_is_trait(self).to(is_trait);
1929 node.set_is_enum(self).to(is_enum);
1930 node.set_is_abstract(self).to(is_abstract);
1931 node.set_parent(self).to(parent);
1932 node.set_interfaces(self).to(interfaces);
1933 node.set_traits(self).to(traits);
1934 node.set_extends(self).to(extends);
1935 node.set_template_params(self).to(template_params);
1936 node.set_require_extends(self).to(require_extends);
1937 node.set_require_implements(self).to(require_implements);
1938 node.set_is_backed_enum(self).to(is_backed_enum);
1939 node.set_mixins(self).to(mixins);
1940 node.set_deprecated(self).to(deprecated);
1941 node.set_enum_scalar_type(self).to(enum_scalar_type);
1942 node.set_is_final(self).to(is_final);
1943 node.set_is_readonly(self).to(is_readonly);
1944 node.set_location(self).to(location);
1945 node.set_extends_type_args(self).to(extends_type_args);
1946 node.set_implements_type_args(self).to(implements_type_args);
1947 node
1948 } else {
1949 let node = ClassNode::new(
1950 self,
1951 fqcn.clone(),
1952 true,
1953 is_interface,
1954 is_trait,
1955 is_enum,
1956 is_abstract,
1957 parent,
1958 interfaces,
1959 traits,
1960 extends,
1961 template_params,
1962 require_extends,
1963 require_implements,
1964 is_backed_enum,
1965 mixins,
1966 deprecated,
1967 enum_scalar_type,
1968 is_final,
1969 is_readonly,
1970 location,
1971 extends_type_args,
1972 implements_type_args,
1973 );
1974 Arc::make_mut(&mut self.class_node_keys_lower)
1975 .insert(fqcn.to_ascii_lowercase(), fqcn.clone());
1976 Arc::make_mut(&mut self.class_nodes).insert(fqcn, node);
1977 node
1978 }
1979 }
1980
1981 pub fn deactivate_class_node(&mut self, fqcn: &str) {
1986 use salsa::Setter as _;
1987 if let Some(&node) = self.class_nodes.get(fqcn) {
1988 node.set_active(self).to(false);
1989 }
1990 }
1991
1992 pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
1994 use salsa::Setter as _;
1995 let fqn = &storage.fqn;
1996 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1997 if node.active(self)
2003 && node.short_name(self) == storage.short_name
2004 && node.is_pure(self) == storage.is_pure
2005 && node.deprecated(self) == storage.deprecated
2006 && node.return_type(self).as_deref() == storage.return_type.as_deref()
2007 && node.location(self) == storage.location
2008 && *node.params(self) == *storage.params.as_ref()
2009 && *node.template_params(self) == *storage.template_params
2010 && *node.assertions(self) == *storage.assertions
2011 && *node.throws(self) == *storage.throws
2012 {
2013 return node;
2014 }
2015 node.set_active(self).to(true);
2016 node.set_short_name(self).to(storage.short_name.clone());
2017 node.set_params(self).to(storage.params.clone());
2018 node.set_return_type(self).to(storage.return_type.clone());
2019 node.set_template_params(self)
2020 .to(Arc::from(storage.template_params.as_slice()));
2021 node.set_assertions(self)
2022 .to(Arc::from(storage.assertions.as_slice()));
2023 node.set_throws(self)
2024 .to(Arc::from(storage.throws.as_slice()));
2025 node.set_deprecated(self).to(storage.deprecated.clone());
2026 node.set_is_pure(self).to(storage.is_pure);
2027 node.set_location(self).to(storage.location.clone());
2028 node
2029 } else {
2030 let node = FunctionNode::new(
2031 self,
2032 fqn.clone(),
2033 storage.short_name.clone(),
2034 true,
2035 storage.params.clone(),
2036 storage.return_type.clone(),
2037 storage
2038 .inferred_return_type
2039 .as_ref()
2040 .map(|t| Arc::new(t.clone())),
2041 Arc::from(storage.template_params.as_slice()),
2042 Arc::from(storage.assertions.as_slice()),
2043 Arc::from(storage.throws.as_slice()),
2044 storage.deprecated.clone(),
2045 storage.is_pure,
2046 storage.location.clone(),
2047 );
2048 Arc::make_mut(&mut self.function_node_keys_lower)
2049 .insert(fqn.to_ascii_lowercase(), fqn.clone());
2050 Arc::make_mut(&mut self.function_nodes).insert(fqn.clone(), node);
2051 node
2052 }
2053 }
2054
2055 pub fn commit_inferred_return_types(&mut self, buf: &InferredReturnTypes) {
2067 use salsa::Setter as _;
2068 let funcs = std::mem::take(&mut *buf.functions.lock().expect("inferred buffer poisoned"));
2069 for (fqn, inferred) in funcs {
2070 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
2071 if !node.active(self) {
2072 continue;
2073 }
2074 let new = Some(Arc::new(inferred));
2075 if node.inferred_return_type(self) == new {
2076 continue;
2077 }
2078 node.set_inferred_return_type(self).to(new);
2079 }
2080 }
2081 let methods = std::mem::take(&mut *buf.methods.lock().expect("inferred buffer poisoned"));
2082 for (fqcn, name, inferred) in methods {
2083 let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
2084 name.clone()
2085 } else {
2086 Arc::from(name.to_lowercase().as_str())
2087 };
2088 let node = self
2089 .method_nodes
2090 .get(fqcn.as_ref())
2091 .and_then(|m| m.get(&name_lower))
2092 .copied();
2093 if let Some(node) = node {
2094 if !node.active(self) {
2095 continue;
2096 }
2097 let new = Some(Arc::new(inferred));
2098 if node.inferred_return_type(self) == new {
2099 continue;
2100 }
2101 node.set_inferred_return_type(self).to(new);
2102 }
2103 }
2104 }
2105
2106 pub fn deactivate_function_node(&mut self, fqn: &str) {
2108 use salsa::Setter as _;
2109 if let Some(&node) = self.function_nodes.get(fqn) {
2110 node.set_active(self).to(false);
2111 }
2112 }
2113
2114 pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
2116 use salsa::Setter as _;
2117 let fqcn = &storage.fqcn;
2118 let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
2119 let existing = self
2122 .method_nodes
2123 .get(fqcn.as_ref())
2124 .and_then(|m| m.get(&name_lower))
2125 .copied();
2126 if let Some(node) = existing {
2127 if node.active(self)
2131 && node.visibility(self) == storage.visibility
2132 && node.is_static(self) == storage.is_static
2133 && node.is_abstract(self) == storage.is_abstract
2134 && node.is_final(self) == storage.is_final
2135 && node.is_constructor(self) == storage.is_constructor
2136 && node.is_pure(self) == storage.is_pure
2137 && node.deprecated(self) == storage.deprecated
2138 && node.return_type(self).as_deref() == storage.return_type.as_deref()
2139 && node.location(self) == storage.location
2140 && *node.params(self) == *storage.params.as_ref()
2141 && *node.template_params(self) == *storage.template_params
2142 && *node.assertions(self) == *storage.assertions
2143 && *node.throws(self) == *storage.throws
2144 {
2145 return node;
2146 }
2147 node.set_active(self).to(true);
2148 node.set_params(self).to(storage.params.clone());
2149 node.set_return_type(self).to(storage.return_type.clone());
2150 node.set_template_params(self)
2151 .to(Arc::from(storage.template_params.as_slice()));
2152 node.set_assertions(self)
2153 .to(Arc::from(storage.assertions.as_slice()));
2154 node.set_throws(self)
2155 .to(Arc::from(storage.throws.as_slice()));
2156 node.set_deprecated(self).to(storage.deprecated.clone());
2157 node.set_visibility(self).to(storage.visibility);
2158 node.set_is_static(self).to(storage.is_static);
2159 node.set_is_abstract(self).to(storage.is_abstract);
2160 node.set_is_final(self).to(storage.is_final);
2161 node.set_is_constructor(self).to(storage.is_constructor);
2162 node.set_is_pure(self).to(storage.is_pure);
2163 node.set_location(self).to(storage.location.clone());
2164 node
2165 } else {
2166 let node = MethodNode::new(
2168 self,
2169 fqcn.clone(),
2170 storage.name.clone(),
2171 true,
2172 storage.params.clone(),
2173 storage.return_type.clone(),
2174 storage
2175 .inferred_return_type
2176 .as_ref()
2177 .map(|t| Arc::new(t.clone())),
2178 Arc::from(storage.template_params.as_slice()),
2179 Arc::from(storage.assertions.as_slice()),
2180 Arc::from(storage.throws.as_slice()),
2181 storage.deprecated.clone(),
2182 storage.visibility,
2183 storage.is_static,
2184 storage.is_abstract,
2185 storage.is_final,
2186 storage.is_constructor,
2187 storage.is_pure,
2188 storage.location.clone(),
2189 );
2190 Arc::make_mut(&mut self.method_nodes)
2191 .entry(fqcn.clone())
2192 .or_default()
2193 .insert(name_lower, node);
2194 node
2195 }
2196 }
2197
2198 pub fn deactivate_class_methods(&mut self, fqcn: &str) {
2200 use salsa::Setter as _;
2201 let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
2202 Some(methods) => methods.values().copied().collect(),
2203 None => return,
2204 };
2205 for node in nodes {
2206 node.set_active(self).to(false);
2207 }
2208 }
2209
2210 pub fn prune_class_methods<T>(&mut self, fqcn: &str, keep_lower: &std::collections::HashSet<T>)
2216 where
2217 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2218 {
2219 use salsa::Setter as _;
2220 let candidates: Vec<MethodNode> = self
2221 .method_nodes
2222 .get(fqcn)
2223 .map(|m| {
2224 m.iter()
2225 .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
2226 .map(|(_, n)| *n)
2227 .collect()
2228 })
2229 .unwrap_or_default();
2230 for node in candidates {
2231 if node.active(self) {
2232 node.set_active(self).to(false);
2233 }
2234 }
2235 }
2236
2237 pub fn prune_class_properties<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2239 where
2240 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2241 {
2242 use salsa::Setter as _;
2243 let candidates: Vec<PropertyNode> = self
2244 .property_nodes
2245 .get(fqcn)
2246 .map(|m| {
2247 m.iter()
2248 .filter(|(k, _)| !keep.contains(k.as_ref()))
2249 .map(|(_, n)| *n)
2250 .collect()
2251 })
2252 .unwrap_or_default();
2253 for node in candidates {
2254 if node.active(self) {
2255 node.set_active(self).to(false);
2256 }
2257 }
2258 }
2259
2260 pub fn prune_class_constants<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2262 where
2263 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2264 {
2265 use salsa::Setter as _;
2266 let candidates: Vec<ClassConstantNode> = self
2267 .class_constant_nodes
2268 .get(fqcn)
2269 .map(|m| {
2270 m.iter()
2271 .filter(|(k, _)| !keep.contains(k.as_ref()))
2272 .map(|(_, n)| *n)
2273 .collect()
2274 })
2275 .unwrap_or_default();
2276 for node in candidates {
2277 if node.active(self) {
2278 node.set_active(self).to(false);
2279 }
2280 }
2281 }
2282
2283 pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
2285 use salsa::Setter as _;
2286 let existing = self
2287 .property_nodes
2288 .get(fqcn.as_ref())
2289 .and_then(|m| m.get(storage.name.as_ref()))
2290 .copied();
2291 if let Some(node) = existing {
2292 if node.active(self)
2294 && node.visibility(self) == storage.visibility
2295 && node.is_static(self) == storage.is_static
2296 && node.is_readonly(self) == storage.is_readonly
2297 && node.ty(self) == storage.ty
2298 && node.location(self) == storage.location
2299 {
2300 return;
2301 }
2302 node.set_active(self).to(true);
2303 node.set_ty(self).to(storage.ty.clone());
2304 node.set_visibility(self).to(storage.visibility);
2305 node.set_is_static(self).to(storage.is_static);
2306 node.set_is_readonly(self).to(storage.is_readonly);
2307 node.set_location(self).to(storage.location.clone());
2308 } else {
2309 let node = PropertyNode::new(
2310 self,
2311 fqcn.clone(),
2312 storage.name.clone(),
2313 true,
2314 storage.ty.clone(),
2315 storage.visibility,
2316 storage.is_static,
2317 storage.is_readonly,
2318 storage.location.clone(),
2319 );
2320 Arc::make_mut(&mut self.property_nodes)
2321 .entry(fqcn.clone())
2322 .or_default()
2323 .insert(storage.name.clone(), node);
2324 }
2325 }
2326
2327 pub fn deactivate_class_properties(&mut self, fqcn: &str) {
2329 use salsa::Setter as _;
2330 let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
2331 Some(props) => props.values().copied().collect(),
2332 None => return,
2333 };
2334 for node in nodes {
2335 node.set_active(self).to(false);
2336 }
2337 }
2338
2339 pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
2341 use salsa::Setter as _;
2342 let existing = self
2343 .class_constant_nodes
2344 .get(fqcn.as_ref())
2345 .and_then(|m| m.get(storage.name.as_ref()))
2346 .copied();
2347 if let Some(node) = existing {
2348 if node.active(self)
2350 && node.visibility(self) == storage.visibility
2351 && node.is_final(self) == storage.is_final
2352 && node.ty(self) == storage.ty
2353 && node.location(self) == storage.location
2354 {
2355 return;
2356 }
2357 node.set_active(self).to(true);
2358 node.set_ty(self).to(storage.ty.clone());
2359 node.set_visibility(self).to(storage.visibility);
2360 node.set_is_final(self).to(storage.is_final);
2361 node.set_location(self).to(storage.location.clone());
2362 } else {
2363 let node = ClassConstantNode::new(
2364 self,
2365 fqcn.clone(),
2366 storage.name.clone(),
2367 true,
2368 storage.ty.clone(),
2369 storage.visibility,
2370 storage.is_final,
2371 storage.location.clone(),
2372 );
2373 Arc::make_mut(&mut self.class_constant_nodes)
2374 .entry(fqcn.clone())
2375 .or_default()
2376 .insert(storage.name.clone(), node);
2377 }
2378 }
2379
2380 pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
2382 use salsa::Setter as _;
2383 if let Some(&node) = self.global_constant_nodes.get(&fqn) {
2384 if node.active(self) && node.ty(self) == ty {
2386 return node;
2387 }
2388 node.set_active(self).to(true);
2389 node.set_ty(self).to(ty);
2390 node
2391 } else {
2392 let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
2393 Arc::make_mut(&mut self.global_constant_nodes).insert(fqn, node);
2394 node
2395 }
2396 }
2397
2398 pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
2400 use salsa::Setter as _;
2401 if let Some(&node) = self.global_constant_nodes.get(fqn) {
2402 node.set_active(self).to(false);
2403 }
2404 }
2405
2406 pub fn deactivate_class_constants(&mut self, fqcn: &str) {
2408 use salsa::Setter as _;
2409 let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
2410 Some(consts) => consts.values().copied().collect(),
2411 None => return,
2412 };
2413 for node in nodes {
2414 node.set_active(self).to(false);
2415 }
2416 }
2417}
2418
2419#[salsa::accumulator]
2445#[derive(Clone, Debug)]
2446pub struct IssueAccumulator(pub Issue);
2447
2448#[derive(Clone, Debug, PartialEq, Eq)]
2457pub struct RefLoc {
2458 pub symbol_key: Arc<str>,
2459 pub file: Arc<str>,
2460 pub line: u32,
2461 pub col_start: u16,
2462 pub col_end: u16,
2463}
2464
2465#[salsa::accumulator]
2472#[derive(Clone, Debug)]
2473pub struct RefLocAccumulator(pub RefLoc);
2474
2475#[salsa::input]
2480pub struct AnalyzeFileInput {
2481 pub php_version: Arc<str>,
2484}
2485
2486#[salsa::tracked]
2501pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, _input: AnalyzeFileInput) {
2502 use salsa::Accumulator as _;
2503 let path = file.path(db);
2504 let text = file.text(db);
2505
2506 let arena = bumpalo::Bump::new();
2507 let parsed = php_rs_parser::parse(&arena, &text);
2508
2509 for err in &parsed.errors {
2510 let issue = Issue::new(
2511 mir_issues::IssueKind::ParseError {
2512 message: err.to_string(),
2513 },
2514 mir_issues::Location {
2515 file: path.clone(),
2516 line: 1,
2517 line_end: 1,
2518 col_start: 0,
2519 col_end: 0,
2520 },
2521 );
2522 IssueAccumulator(issue).accumulate(db);
2523 }
2524}
2525
2526#[cfg(test)]
2531mod tests {
2532 use super::*;
2533 use salsa::Setter as _;
2534
2535 fn upsert_class(
2536 db: &mut MirDb,
2537 fqcn: &str,
2538 parent: Option<Arc<str>>,
2539 extends: Arc<[Arc<str>]>,
2540 is_interface: bool,
2541 ) -> ClassNode {
2542 db.upsert_class_node(ClassNodeFields {
2543 is_interface,
2544 parent,
2545 extends,
2546 ..ClassNodeFields::for_class(Arc::from(fqcn))
2547 })
2548 }
2549
2550 #[test]
2551 fn mirdb_constructs() {
2552 let _db = MirDb::default();
2553 }
2554
2555 #[test]
2556 fn source_file_input_roundtrip() {
2557 let db = MirDb::default();
2558 let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
2559 assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
2560 assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
2561 }
2562
2563 #[test]
2564 fn collect_file_definitions_basic() {
2565 let db = MirDb::default();
2566 let src = Arc::from("<?php class Foo {}");
2567 let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
2568 let defs = collect_file_definitions(&db, file);
2569 assert!(defs.issues.is_empty());
2570 assert_eq!(defs.slice.classes.len(), 1);
2571 assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
2572 }
2573
2574 #[test]
2575 fn collect_file_definitions_memoized() {
2576 let db = MirDb::default();
2577 let file = SourceFile::new(
2578 &db,
2579 Arc::from("/tmp/memo.php"),
2580 Arc::from("<?php class Bar {}"),
2581 );
2582
2583 let defs1 = collect_file_definitions(&db, file);
2584 let defs2 = collect_file_definitions(&db, file);
2585 assert!(
2586 Arc::ptr_eq(&defs1.slice, &defs2.slice),
2587 "unchanged file must return the memoized result"
2588 );
2589 }
2590
2591 #[test]
2592 fn analyze_file_accumulates_parse_errors() {
2593 let db = MirDb::default();
2594 let file = SourceFile::new(
2596 &db,
2597 Arc::from("/tmp/parse_err.php"),
2598 Arc::from("<?php $x = \"unterminated"),
2599 );
2600 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2601 analyze_file(&db, file, input);
2602 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2603 assert!(
2604 !issues.is_empty(),
2605 "expected parse error to surface as accumulated IssueAccumulator"
2606 );
2607 assert!(matches!(
2608 issues[0].0.kind,
2609 mir_issues::IssueKind::ParseError { .. }
2610 ));
2611 }
2612
2613 #[test]
2614 fn analyze_file_clean_input_accumulates_nothing() {
2615 let db = MirDb::default();
2616 let file = SourceFile::new(
2617 &db,
2618 Arc::from("/tmp/clean.php"),
2619 Arc::from("<?php class Foo {}"),
2620 );
2621 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2622 analyze_file(&db, file, input);
2623 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2624 let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
2625 assert!(issues.is_empty());
2626 assert!(refs.is_empty());
2627 }
2628
2629 #[test]
2630 fn collect_file_definitions_recomputes_on_change() {
2631 let mut db = MirDb::default();
2632 let file = SourceFile::new(
2633 &db,
2634 Arc::from("/tmp/memo2.php"),
2635 Arc::from("<?php class Foo {}"),
2636 );
2637
2638 let defs1 = collect_file_definitions(&db, file);
2639 file.set_text(&mut db)
2640 .to(Arc::from("<?php class Foo {} class Bar {}"));
2641 let defs2 = collect_file_definitions(&db, file);
2642
2643 assert!(
2644 !Arc::ptr_eq(&defs1.slice, &defs2.slice),
2645 "changed file must produce a new result"
2646 );
2647 assert_eq!(defs2.slice.classes.len(), 2);
2648 }
2649
2650 #[test]
2651 fn class_ancestors_empty_for_root_class() {
2652 let mut db = MirDb::default();
2653 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2654 let ancestors = class_ancestors(&db, node);
2655 assert!(ancestors.0.is_empty(), "root class has no ancestors");
2656 }
2657
2658 #[test]
2659 fn class_ancestors_single_parent() {
2660 let mut db = MirDb::default();
2661 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2662 let child = upsert_class(
2663 &mut db,
2664 "Child",
2665 Some(Arc::from("Base")),
2666 Arc::from([]),
2667 false,
2668 );
2669 let ancestors = class_ancestors(&db, child);
2670 assert_eq!(ancestors.0.len(), 1);
2671 assert_eq!(ancestors.0[0].as_ref(), "Base");
2672 }
2673
2674 #[test]
2675 fn class_ancestors_transitive() {
2676 let mut db = MirDb::default();
2677 upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
2678 upsert_class(
2679 &mut db,
2680 "Parent",
2681 Some(Arc::from("GrandParent")),
2682 Arc::from([]),
2683 false,
2684 );
2685 let child = upsert_class(
2686 &mut db,
2687 "Child",
2688 Some(Arc::from("Parent")),
2689 Arc::from([]),
2690 false,
2691 );
2692 let ancestors = class_ancestors(&db, child);
2693 assert_eq!(ancestors.0.len(), 2);
2694 assert_eq!(ancestors.0[0].as_ref(), "Parent");
2695 assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
2696 }
2697
2698 #[test]
2699 fn class_ancestors_cycle_returns_empty() {
2700 let mut db = MirDb::default();
2701 let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
2703 let ancestors = class_ancestors(&db, node_a);
2704 assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
2706 }
2707
2708 #[test]
2709 fn class_ancestors_inactive_node_returns_empty() {
2710 let mut db = MirDb::default();
2711 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2712 db.deactivate_class_node("Foo");
2713 let ancestors = class_ancestors(&db, node);
2714 assert!(ancestors.0.is_empty(), "inactive node must yield empty");
2715 }
2716
2717 #[test]
2718 fn class_ancestors_recomputes_on_parent_change() {
2719 let mut db = MirDb::default();
2720 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2721 let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
2722
2723 let before = class_ancestors(&db, child);
2724 assert!(before.0.is_empty());
2725
2726 child.set_parent(&mut db).to(Some(Arc::from("Base")));
2728
2729 let after = class_ancestors(&db, child);
2730 assert_eq!(after.0.len(), 1);
2731 assert_eq!(after.0[0].as_ref(), "Base");
2732 }
2733
2734 #[test]
2735 fn interface_ancestors_via_extends() {
2736 let mut db = MirDb::default();
2737 upsert_class(&mut db, "Countable", None, Arc::from([]), true);
2738 let child_iface = upsert_class(
2739 &mut db,
2740 "Collection",
2741 None,
2742 Arc::from([Arc::from("Countable")]),
2743 true,
2744 );
2745 let ancestors = class_ancestors(&db, child_iface);
2746 assert_eq!(ancestors.0.len(), 1);
2747 assert_eq!(ancestors.0[0].as_ref(), "Countable");
2748 }
2749
2750 #[test]
2751 fn type_exists_via_db_tracks_active_state() {
2752 let mut db = MirDb::default();
2753 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2754 assert!(type_exists_via_db(&db, "Foo"));
2755 assert!(!type_exists_via_db(&db, "Bar"));
2756 db.deactivate_class_node("Foo");
2757 assert!(!type_exists_via_db(&db, "Foo"));
2758 }
2759
2760 #[test]
2761 fn clone_preserves_class_node_lookups() {
2762 let mut db = MirDb::default();
2765 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2766 let cloned = db.clone();
2767 assert!(
2768 type_exists_via_db(&cloned, "Foo"),
2769 "clone must observe nodes registered before clone()"
2770 );
2771 assert!(
2772 !type_exists_via_db(&cloned, "Bar"),
2773 "clone must not observe nodes that were never registered"
2774 );
2775 let foo_node = cloned.lookup_class_node("Foo").expect("registered");
2777 let ancestors = class_ancestors(&cloned, foo_node);
2778 assert!(ancestors.0.is_empty(), "Foo has no ancestors");
2779 }
2780
2781 fn upsert_class_with_traits(
2786 db: &mut MirDb,
2787 fqcn: &str,
2788 parent: Option<Arc<str>>,
2789 traits: &[&str],
2790 is_interface: bool,
2791 is_trait: bool,
2792 ) -> ClassNode {
2793 db.upsert_class_node(ClassNodeFields {
2794 is_interface,
2795 is_trait,
2796 parent,
2797 traits: Arc::from(
2798 traits
2799 .iter()
2800 .map(|t| Arc::<str>::from(*t))
2801 .collect::<Vec<_>>(),
2802 ),
2803 ..ClassNodeFields::for_class(Arc::from(fqcn))
2804 })
2805 }
2806
2807 fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
2808 let storage = MethodStorage {
2809 name: Arc::from(name),
2810 fqcn: Arc::from(fqcn),
2811 params: Arc::from([].as_slice()),
2812 return_type: None,
2813 inferred_return_type: None,
2814 visibility: Visibility::Public,
2815 is_static: false,
2816 is_abstract,
2817 is_final: false,
2818 is_constructor: name == "__construct",
2819 template_params: vec![],
2820 assertions: vec![],
2821 throws: vec![],
2822 deprecated: None,
2823 is_internal: false,
2824 is_pure: false,
2825 location: None,
2826 };
2827 db.upsert_method_node(&storage)
2828 }
2829
2830 fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
2831 db.upsert_class_node(ClassNodeFields {
2832 interfaces: Arc::from(
2833 interfaces
2834 .iter()
2835 .map(|i| Arc::<str>::from(*i))
2836 .collect::<Vec<_>>(),
2837 ),
2838 is_backed_enum: is_backed,
2839 ..ClassNodeFields::for_enum(Arc::from(fqcn))
2840 })
2841 }
2842
2843 #[test]
2848 fn method_exists_via_db_finds_own_method() {
2849 let mut db = MirDb::default();
2850 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2851 upsert_method(&mut db, "Foo", "bar", false);
2852 assert!(method_exists_via_db(&db, "Foo", "bar"));
2853 assert!(!method_exists_via_db(&db, "Foo", "missing"));
2854 }
2855
2856 #[test]
2857 fn method_exists_via_db_walks_parent() {
2858 let mut db = MirDb::default();
2859 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2860 upsert_method(&mut db, "Base", "inherited", false);
2861 upsert_class(
2862 &mut db,
2863 "Child",
2864 Some(Arc::from("Base")),
2865 Arc::from([]),
2866 false,
2867 );
2868 assert!(method_exists_via_db(&db, "Child", "inherited"));
2869 }
2870
2871 #[test]
2872 fn method_exists_via_db_walks_traits_transitively() {
2873 let mut db = MirDb::default();
2874 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2875 upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
2876 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2877 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2878 assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
2879 }
2880
2881 #[test]
2882 fn method_exists_via_db_is_case_insensitive() {
2883 let mut db = MirDb::default();
2884 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2885 upsert_method(&mut db, "Foo", "doStuff", false);
2886 assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
2888 assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
2889 }
2890
2891 #[test]
2892 fn method_exists_via_db_unknown_class_returns_false() {
2893 let db = MirDb::default();
2894 assert!(!method_exists_via_db(&db, "Nope", "anything"));
2895 }
2896
2897 #[test]
2898 fn method_exists_via_db_inactive_class_returns_false() {
2899 let mut db = MirDb::default();
2900 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2901 upsert_method(&mut db, "Foo", "bar", false);
2902 db.deactivate_class_node("Foo");
2903 assert!(!method_exists_via_db(&db, "Foo", "bar"));
2904 }
2905
2906 #[test]
2907 fn method_exists_via_db_finds_abstract_methods() {
2908 let mut db = MirDb::default();
2911 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2912 upsert_method(&mut db, "Foo", "abstr", true);
2913 assert!(method_exists_via_db(&db, "Foo", "abstr"));
2914 }
2915
2916 #[test]
2921 fn method_is_concretely_implemented_skips_abstract() {
2922 let mut db = MirDb::default();
2923 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2924 upsert_method(&mut db, "Foo", "abstr", true);
2925 assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
2926 }
2927
2928 #[test]
2929 fn method_is_concretely_implemented_finds_concrete_in_trait() {
2930 let mut db = MirDb::default();
2931 upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
2932 upsert_method(&mut db, "MyTrait", "provided", false);
2933 upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
2934 assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
2935 }
2936
2937 #[test]
2938 fn method_is_concretely_implemented_skips_interface_definitions() {
2939 let mut db = MirDb::default();
2942 upsert_class(&mut db, "I", None, Arc::from([]), true);
2943 upsert_method(&mut db, "I", "m", false);
2944 upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
2945 assert!(!method_is_concretely_implemented(&db, "C", "m"));
2947 }
2948
2949 #[test]
2954 fn extends_or_implements_via_db_self_match() {
2955 let mut db = MirDb::default();
2956 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2957 assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
2958 }
2959
2960 #[test]
2961 fn extends_or_implements_via_db_transitive() {
2962 let mut db = MirDb::default();
2963 upsert_class(&mut db, "Animal", None, Arc::from([]), false);
2964 upsert_class(
2965 &mut db,
2966 "Mammal",
2967 Some(Arc::from("Animal")),
2968 Arc::from([]),
2969 false,
2970 );
2971 upsert_class(
2972 &mut db,
2973 "Dog",
2974 Some(Arc::from("Mammal")),
2975 Arc::from([]),
2976 false,
2977 );
2978 assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
2979 assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
2980 assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
2981 }
2982
2983 #[test]
2984 fn extends_or_implements_via_db_unknown_returns_false() {
2985 let db = MirDb::default();
2986 assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
2987 }
2988
2989 #[test]
2990 fn extends_or_implements_via_db_unit_enum_implicit() {
2991 let mut db = MirDb::default();
2992 upsert_enum(&mut db, "Status", &[], false);
2993 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2994 assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
2995 assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2997 }
2998
2999 #[test]
3000 fn extends_or_implements_via_db_backed_enum_implicit() {
3001 let mut db = MirDb::default();
3002 upsert_enum(&mut db, "Status", &[], true);
3003 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
3004 assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
3005 assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
3006 }
3007
3008 #[test]
3009 fn extends_or_implements_via_db_enum_declared_interface() {
3010 let mut db = MirDb::default();
3011 upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
3012 upsert_enum(&mut db, "Status", &["Stringable"], false);
3013 assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
3014 }
3015
3016 #[test]
3021 fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
3022 let mut db = MirDb::default();
3023 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3024 upsert_class(
3025 &mut db,
3026 "Child",
3027 Some(Arc::from("Base")),
3028 Arc::from([]),
3029 false,
3030 );
3031 assert!(!has_unknown_ancestor_via_db(&db, "Child"));
3032 }
3033
3034 #[test]
3035 fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
3036 let mut db = MirDb::default();
3037 upsert_class(
3039 &mut db,
3040 "Child",
3041 Some(Arc::from("Missing")),
3042 Arc::from([]),
3043 false,
3044 );
3045 assert!(has_unknown_ancestor_via_db(&db, "Child"));
3046 }
3047
3048 #[test]
3049 fn class_template_params_via_db_returns_registered_params() {
3050 use mir_types::Variance;
3051 let mut db = MirDb::default();
3052 let tp = TemplateParam {
3053 name: Arc::from("T"),
3054 bound: None,
3055 defining_entity: Arc::from("Box"),
3056 variance: Variance::Invariant,
3057 };
3058 db.upsert_class_node(ClassNodeFields {
3059 template_params: Arc::from([tp.clone()]),
3060 ..ClassNodeFields::for_class(Arc::from("Box"))
3061 });
3062 let got = class_template_params_via_db(&db, "Box").expect("registered");
3063 assert_eq!(got.len(), 1);
3064 assert_eq!(got[0].name.as_ref(), "T");
3065
3066 assert!(class_template_params_via_db(&db, "Missing").is_none());
3067 db.deactivate_class_node("Box");
3068 assert!(class_template_params_via_db(&db, "Box").is_none());
3069 }
3070
3071 fn upsert_class_with_mixins(
3076 db: &mut MirDb,
3077 fqcn: &str,
3078 parent: Option<Arc<str>>,
3079 mixins: &[&str],
3080 ) -> ClassNode {
3081 db.upsert_class_node(ClassNodeFields {
3082 parent,
3083 mixins: Arc::from(
3084 mixins
3085 .iter()
3086 .map(|m| Arc::<str>::from(*m))
3087 .collect::<Vec<_>>(),
3088 ),
3089 ..ClassNodeFields::for_class(Arc::from(fqcn))
3090 })
3091 }
3092
3093 #[test]
3094 fn lookup_method_in_chain_finds_own_then_ancestor() {
3095 let mut db = MirDb::default();
3096 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3097 upsert_method(&mut db, "Base", "shared", false);
3098 upsert_class(
3099 &mut db,
3100 "Child",
3101 Some(Arc::from("Base")),
3102 Arc::from([]),
3103 false,
3104 );
3105 upsert_method(&mut db, "Child", "shared", false);
3106 let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
3108 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3109 upsert_method(&mut db, "Base", "only_in_base", false);
3111 let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
3112 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3113 }
3114
3115 #[test]
3116 fn lookup_method_in_chain_walks_trait_of_traits() {
3117 let mut db = MirDb::default();
3118 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
3119 upsert_method(&mut db, "InnerTrait", "deep", false);
3120 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
3121 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
3122 let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
3123 assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
3124 }
3125
3126 #[test]
3127 fn lookup_method_in_chain_walks_mixins() {
3128 let mut db = MirDb::default();
3129 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3130 upsert_method(&mut db, "MixinTarget", "magic", false);
3131 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3132 let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
3133 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3134 }
3135
3136 #[test]
3137 fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
3138 let mut db = MirDb::default();
3139 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3141 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3142 assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
3143 }
3144
3145 #[test]
3146 fn lookup_method_in_chain_is_case_insensitive() {
3147 let mut db = MirDb::default();
3148 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3149 upsert_method(&mut db, "Foo", "doStuff", false);
3150 assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
3151 assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
3152 }
3153
3154 #[test]
3155 fn lookup_method_in_chain_unknown_returns_none() {
3156 let db = MirDb::default();
3157 assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
3158 }
3159
3160 fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
3165 let storage = PropertyStorage {
3166 name: Arc::from(name),
3167 ty: None,
3168 inferred_ty: None,
3169 visibility: Visibility::Public,
3170 is_static: false,
3171 is_readonly,
3172 default: None,
3173 location: None,
3174 };
3175 let owner = Arc::<str>::from(fqcn);
3176 db.upsert_property_node(&owner, &storage);
3177 db.lookup_property_node(fqcn, name).expect("registered")
3178 }
3179
3180 #[test]
3181 fn lookup_property_in_chain_own_then_ancestor() {
3182 let mut db = MirDb::default();
3183 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3184 upsert_property(&mut db, "Base", "x", false);
3185 upsert_class(
3186 &mut db,
3187 "Child",
3188 Some(Arc::from("Base")),
3189 Arc::from([]),
3190 false,
3191 );
3192 let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
3194 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3195 upsert_property(&mut db, "Child", "x", true);
3197 let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
3198 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3199 assert!(found.is_readonly(&db));
3200 }
3201
3202 #[test]
3203 fn lookup_property_in_chain_walks_mixins() {
3204 let mut db = MirDb::default();
3205 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3206 upsert_property(&mut db, "MixinTarget", "exposed", false);
3207 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3208 let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
3209 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3210 }
3211
3212 #[test]
3213 fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
3214 let mut db = MirDb::default();
3215 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3216 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3217 assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
3218 }
3219
3220 #[test]
3221 fn lookup_property_in_chain_is_case_sensitive() {
3222 let mut db = MirDb::default();
3223 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3224 upsert_property(&mut db, "Foo", "myProp", false);
3225 assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
3226 assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
3228 }
3229
3230 #[test]
3231 fn lookup_property_in_chain_inactive_returns_none() {
3232 let mut db = MirDb::default();
3233 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3234 upsert_property(&mut db, "Foo", "x", false);
3235 db.deactivate_class_node("Foo");
3236 assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
3237 }
3238
3239 fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
3244 let storage = ConstantStorage {
3245 name: Arc::from(name),
3246 ty: mir_types::Union::mixed(),
3247 visibility: None,
3248 is_final: false,
3249 location: None,
3250 };
3251 let owner = Arc::<str>::from(fqcn);
3252 db.upsert_class_constant_node(&owner, &storage);
3253 }
3254
3255 #[test]
3256 fn class_constant_exists_in_chain_finds_own() {
3257 let mut db = MirDb::default();
3258 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3259 upsert_constant(&mut db, "Foo", "MAX");
3260 assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
3261 assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
3262 }
3263
3264 #[test]
3265 fn class_constant_exists_in_chain_walks_parent() {
3266 let mut db = MirDb::default();
3267 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3268 upsert_constant(&mut db, "Base", "VERSION");
3269 upsert_class(
3270 &mut db,
3271 "Child",
3272 Some(Arc::from("Base")),
3273 Arc::from([]),
3274 false,
3275 );
3276 assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
3277 }
3278
3279 #[test]
3280 fn class_constant_exists_in_chain_walks_interface() {
3281 let mut db = MirDb::default();
3282 upsert_class(&mut db, "I", None, Arc::from([]), true);
3283 upsert_constant(&mut db, "I", "TYPE");
3284 db.upsert_class_node(ClassNodeFields {
3287 interfaces: Arc::from([Arc::from("I")]),
3288 ..ClassNodeFields::for_class(Arc::from("Impl"))
3289 });
3290 assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
3291 }
3292
3293 #[test]
3294 fn class_constant_exists_in_chain_walks_direct_trait() {
3295 let mut db = MirDb::default();
3296 upsert_class_with_traits(&mut db, "T", None, &[], false, true);
3297 upsert_constant(&mut db, "T", "FROM_TRAIT");
3298 upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
3299 assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
3300 }
3301
3302 #[test]
3303 fn class_constant_exists_in_chain_unknown_class_returns_false() {
3304 let db = MirDb::default();
3305 assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
3306 }
3307
3308 #[test]
3309 fn class_constant_exists_in_chain_inactive_returns_false() {
3310 let mut db = MirDb::default();
3311 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3312 upsert_constant(&mut db, "Foo", "X");
3313 db.deactivate_class_node("Foo");
3314 db.deactivate_class_constants("Foo");
3315 assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
3316 }
3317
3318 #[test]
3324 fn parallel_reads_then_serial_write_does_not_deadlock() {
3325 use rayon::prelude::*;
3326 use std::sync::mpsc;
3327 use std::time::Duration;
3328
3329 let (tx, rx) = mpsc::channel::<()>();
3330 std::thread::spawn(move || {
3331 let mut db = MirDb::default();
3332 let storage = mir_codebase::storage::FunctionStorage {
3333 fqn: Arc::from("foo"),
3334 short_name: Arc::from("foo"),
3335 params: Arc::from([].as_slice()),
3336 return_type: None,
3337 inferred_return_type: None,
3338 template_params: vec![],
3339 assertions: vec![],
3340 throws: vec![],
3341 deprecated: None,
3342 is_pure: false,
3343 location: None,
3344 };
3345 let node = db.upsert_function_node(&storage);
3346
3347 let db_for_sweep = db.clone();
3349 (0..256u32)
3350 .into_par_iter()
3351 .for_each_with(db_for_sweep, |db, _| {
3352 let _ = node.return_type(&*db as &dyn MirDatabase);
3353 });
3354
3355 node.set_return_type(&mut db)
3359 .to(Some(Arc::new(Union::mixed())));
3360 assert_eq!(node.return_type(&db), Some(Arc::new(Union::mixed())));
3361 tx.send(()).unwrap();
3362 });
3363
3364 match rx.recv_timeout(Duration::from_secs(30)) {
3365 Ok(()) => {}
3366 Err(_) => {
3367 panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
3368 }
3369 }
3370 }
3371
3372 #[test]
3383 fn sibling_clone_blocks_setter_until_dropped() {
3384 use std::sync::mpsc;
3385 use std::time::Duration;
3386
3387 let mut db = MirDb::default();
3388 let storage = mir_codebase::storage::FunctionStorage {
3389 fqn: Arc::from("foo"),
3390 short_name: Arc::from("foo"),
3391 params: Arc::from([].as_slice()),
3392 return_type: None,
3393 inferred_return_type: None,
3394 template_params: vec![],
3395 assertions: vec![],
3396 throws: vec![],
3397 deprecated: None,
3398 is_pure: false,
3399 location: None,
3400 };
3401 let node = db.upsert_function_node(&storage);
3402
3403 let sibling = db.clone();
3404
3405 let (tx, rx) = mpsc::channel::<()>();
3408 let writer = std::thread::spawn(move || {
3409 node.set_return_type(&mut db)
3410 .to(Some(Arc::new(Union::mixed())));
3411 tx.send(()).unwrap();
3412 });
3413
3414 match rx.recv_timeout(Duration::from_millis(500)) {
3417 Err(mpsc::RecvTimeoutError::Timeout) => { }
3418 Ok(()) => panic!(
3419 "setter completed while sibling clone was alive — strong-count==1 \
3420 invariant of `cancel_others` is broken; commit_inferred_return_types \
3421 cannot rely on tight-scoping clones"
3422 ),
3423 Err(e) => panic!("unexpected channel error: {e:?}"),
3424 }
3425
3426 drop(sibling);
3428
3429 match rx.recv_timeout(Duration::from_secs(5)) {
3430 Ok(()) => {}
3431 Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
3432 }
3433 writer.join().expect("writer thread panicked");
3434 }
3435}