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<Union>,
407 pub inferred_return_type: Option<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<Union>,
441 pub inferred_return_type: Option<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
1157#[salsa::tracked]
1162pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
1163 let path = file.path(db);
1164 let text = file.text(db);
1165
1166 let arena = bumpalo::Bump::new();
1167 let parsed = php_rs_parser::parse(&arena, &text);
1168
1169 let mut all_issues: Vec<Issue> = parsed
1170 .errors
1171 .iter()
1172 .map(|err| {
1173 Issue::new(
1174 mir_issues::IssueKind::ParseError {
1175 message: err.to_string(),
1176 },
1177 mir_issues::Location {
1178 file: path.clone(),
1179 line: 1,
1180 line_end: 1,
1181 col_start: 0,
1182 col_end: 0,
1183 },
1184 )
1185 })
1186 .collect();
1187
1188 let collector =
1189 crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
1190 let (slice, collector_issues) = collector.collect_slice(&parsed.program);
1191 all_issues.extend(collector_issues);
1192
1193 FileDefinitions {
1194 slice: Arc::new(slice),
1195 issues: Arc::new(all_issues),
1196 }
1197}
1198
1199type MemberRegistry<V> = Arc<FxHashMap<Arc<str>, FxHashMap<Arc<str>, V>>>;
1211type ReferenceLocations =
1212 Arc<std::sync::Mutex<FxHashMap<Arc<str>, Vec<(Arc<str>, u32, u16, u16)>>>>;
1213
1214#[salsa::db]
1215#[derive(Default, Clone)]
1216pub struct MirDb {
1217 storage: salsa::Storage<Self>,
1218 class_nodes: Arc<FxHashMap<Arc<str>, ClassNode>>,
1226 class_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
1230 function_nodes: Arc<FxHashMap<Arc<str>, FunctionNode>>,
1233 function_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
1237 method_nodes: MemberRegistry<MethodNode>,
1239 property_nodes: MemberRegistry<PropertyNode>,
1241 class_constant_nodes: MemberRegistry<ClassConstantNode>,
1243 global_constant_nodes: Arc<FxHashMap<Arc<str>, GlobalConstantNode>>,
1245 file_namespaces: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1247 file_imports: Arc<FxHashMap<Arc<str>, HashMap<String, String>>>,
1249 global_vars: Arc<FxHashMap<Arc<str>, Union>>,
1251 symbol_to_file: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1253 reference_locations: ReferenceLocations,
1255}
1256
1257#[salsa::db]
1258impl salsa::Database for MirDb {}
1259
1260#[salsa::db]
1261impl MirDatabase for MirDb {
1262 fn php_version_str(&self) -> Arc<str> {
1263 Arc::from("8.2")
1264 }
1265
1266 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
1267 if let Some(&node) = self.class_nodes.get(fqcn) {
1268 return Some(node);
1269 }
1270 let lower = fqcn.to_ascii_lowercase();
1271 let canonical = self.class_node_keys_lower.get(&lower)?;
1272 self.class_nodes.get(canonical.as_ref()).copied()
1273 }
1274
1275 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
1276 if let Some(&node) = self.function_nodes.get(fqn) {
1277 return Some(node);
1278 }
1279 let lower = fqn.to_ascii_lowercase();
1280 let canonical = self.function_node_keys_lower.get(&lower)?;
1281 self.function_nodes.get(canonical.as_ref()).copied()
1282 }
1283
1284 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
1285 self.method_nodes
1286 .get(fqcn)
1287 .and_then(|m| m.get(method_name_lower).copied())
1288 }
1289
1290 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
1291 self.property_nodes
1292 .get(fqcn)
1293 .and_then(|m| m.get(prop_name).copied())
1294 }
1295
1296 fn lookup_class_constant_node(
1297 &self,
1298 fqcn: &str,
1299 const_name: &str,
1300 ) -> Option<ClassConstantNode> {
1301 self.class_constant_nodes
1302 .get(fqcn)
1303 .and_then(|m| m.get(const_name).copied())
1304 }
1305
1306 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
1307 self.global_constant_nodes.get(fqn).copied()
1308 }
1309
1310 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
1311 self.method_nodes
1312 .get(fqcn)
1313 .map(|m| m.values().copied().collect())
1314 .unwrap_or_default()
1315 }
1316
1317 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
1318 self.property_nodes
1319 .get(fqcn)
1320 .map(|m| m.values().copied().collect())
1321 .unwrap_or_default()
1322 }
1323
1324 fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
1325 self.class_nodes
1326 .iter()
1327 .filter_map(|(fqcn, node)| {
1328 if node.active(self) {
1329 Some(fqcn.clone())
1330 } else {
1331 None
1332 }
1333 })
1334 .collect()
1335 }
1336
1337 fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
1338 self.function_nodes
1339 .iter()
1340 .filter_map(|(fqn, node)| {
1341 if node.active(self) {
1342 Some(fqn.clone())
1343 } else {
1344 None
1345 }
1346 })
1347 .collect()
1348 }
1349
1350 fn file_namespace(&self, file: &str) -> Option<Arc<str>> {
1351 self.file_namespaces.get(file).cloned()
1352 }
1353
1354 fn file_imports(&self, file: &str) -> HashMap<String, String> {
1355 self.file_imports.get(file).cloned().unwrap_or_default()
1356 }
1357
1358 fn global_var_type(&self, name: &str) -> Option<Union> {
1359 self.global_vars.get(name).cloned()
1360 }
1361
1362 fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)> {
1363 self.file_imports
1364 .iter()
1365 .map(|(file, imports)| (file.clone(), imports.clone()))
1366 .collect()
1367 }
1368
1369 fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>> {
1370 self.symbol_to_file.get(symbol).cloned()
1371 }
1372
1373 fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>> {
1374 self.symbol_to_file
1375 .iter()
1376 .filter_map(|(sym, defining_file)| {
1377 if defining_file.as_ref() == file {
1378 Some(sym.clone())
1379 } else {
1380 None
1381 }
1382 })
1383 .collect()
1384 }
1385
1386 fn record_reference_location(&self, loc: RefLoc) {
1387 let mut refs = self
1388 .reference_locations
1389 .lock()
1390 .expect("reference lock poisoned");
1391 let entry = refs.entry(loc.symbol_key).or_default();
1392 let tuple = (loc.file, loc.line, loc.col_start, loc.col_end);
1393 if !entry.iter().any(|existing| existing == &tuple) {
1394 entry.push(tuple);
1395 }
1396 }
1397
1398 fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
1399 for (symbol, line, col_start, col_end) in locs {
1400 self.record_reference_location(RefLoc {
1401 symbol_key: Arc::from(symbol.as_str()),
1402 file: file.clone(),
1403 line: *line,
1404 col_start: *col_start,
1405 col_end: *col_end,
1406 });
1407 }
1408 }
1409
1410 fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1411 let refs = self
1412 .reference_locations
1413 .lock()
1414 .expect("reference lock poisoned");
1415 let mut out = Vec::new();
1416 for (symbol, locs) in refs.iter() {
1417 for (loc_file, line, col_start, col_end) in locs {
1418 if loc_file.as_ref() == file {
1419 out.push((symbol.clone(), *line, *col_start, *col_end));
1420 }
1421 }
1422 }
1423 out
1424 }
1425
1426 fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1427 let refs = self
1428 .reference_locations
1429 .lock()
1430 .expect("reference lock poisoned");
1431 refs.get(symbol).cloned().unwrap_or_default()
1432 }
1433
1434 fn has_reference(&self, symbol: &str) -> bool {
1435 let refs = self
1436 .reference_locations
1437 .lock()
1438 .expect("reference lock poisoned");
1439 refs.get(symbol).is_some_and(|locs| !locs.is_empty())
1440 }
1441
1442 fn clear_file_references(&self, file: &str) {
1443 let mut refs = self
1444 .reference_locations
1445 .lock()
1446 .expect("reference lock poisoned");
1447 for locs in refs.values_mut() {
1448 locs.retain(|(loc_file, _, _, _)| loc_file.as_ref() != file);
1449 }
1450 }
1451}
1452
1453#[derive(Default)]
1469#[allow(clippy::type_complexity)]
1470pub struct InferredReturnTypes {
1471 functions: std::sync::Mutex<Vec<(Arc<str>, Union)>>,
1473 methods: std::sync::Mutex<Vec<(Arc<str>, Arc<str>, Union)>>,
1477}
1478
1479impl InferredReturnTypes {
1480 pub fn new() -> Self {
1481 Self::default()
1482 }
1483
1484 pub fn push_function(&self, fqn: Arc<str>, inferred: Union) {
1485 if let Ok(mut g) = self.functions.lock() {
1486 g.push((fqn, inferred));
1487 }
1488 }
1489
1490 pub fn push_method(&self, fqcn: Arc<str>, name: Arc<str>, inferred: Union) {
1491 if let Ok(mut g) = self.methods.lock() {
1492 g.push((fqcn, name, inferred));
1493 }
1494 }
1495}
1496
1497#[derive(Debug, Clone, Default)]
1505pub struct ClassNodeFields {
1506 pub fqcn: Arc<str>,
1507 pub is_interface: bool,
1508 pub is_trait: bool,
1509 pub is_enum: bool,
1510 pub is_abstract: bool,
1511 pub parent: Option<Arc<str>>,
1512 pub interfaces: Arc<[Arc<str>]>,
1513 pub traits: Arc<[Arc<str>]>,
1514 pub extends: Arc<[Arc<str>]>,
1515 pub template_params: Arc<[TemplateParam]>,
1516 pub require_extends: Arc<[Arc<str>]>,
1517 pub require_implements: Arc<[Arc<str>]>,
1518 pub is_backed_enum: bool,
1519 pub mixins: Arc<[Arc<str>]>,
1520 pub deprecated: Option<Arc<str>>,
1521 pub enum_scalar_type: Option<Union>,
1522 pub is_final: bool,
1523 pub is_readonly: bool,
1524 pub location: Option<Location>,
1525 pub extends_type_args: Arc<[Union]>,
1526 pub implements_type_args: ImplementsTypeArgs,
1527}
1528
1529impl ClassNodeFields {
1530 pub fn for_class(fqcn: Arc<str>) -> Self {
1531 Self {
1532 fqcn,
1533 ..Self::default()
1534 }
1535 }
1536
1537 pub fn for_interface(fqcn: Arc<str>) -> Self {
1538 Self {
1539 fqcn,
1540 is_interface: true,
1541 ..Self::default()
1542 }
1543 }
1544
1545 pub fn for_trait(fqcn: Arc<str>) -> Self {
1546 Self {
1547 fqcn,
1548 is_trait: true,
1549 ..Self::default()
1550 }
1551 }
1552
1553 pub fn for_enum(fqcn: Arc<str>) -> Self {
1554 Self {
1555 fqcn,
1556 is_enum: true,
1557 ..Self::default()
1558 }
1559 }
1560}
1561
1562impl MirDb {
1563 pub fn remove_file_definitions(&mut self, file: &str) {
1564 let symbols = self.symbols_defined_in_file(file);
1565 for symbol in &symbols {
1566 self.deactivate_class_node(symbol);
1567 self.deactivate_function_node(symbol);
1568 self.deactivate_class_methods(symbol);
1569 self.deactivate_class_properties(symbol);
1570 self.deactivate_class_constants(symbol);
1571 self.deactivate_global_constant_node(symbol);
1572 }
1573 let symbol_set: HashSet<Arc<str>> = symbols.into_iter().collect();
1574 Arc::make_mut(&mut self.symbol_to_file).retain(|sym, defining_file| {
1575 defining_file.as_ref() != file && !symbol_set.contains(sym)
1576 });
1577 Arc::make_mut(&mut self.file_namespaces).retain(|path, _| path.as_ref() != file);
1578 Arc::make_mut(&mut self.file_imports).retain(|path, _| path.as_ref() != file);
1579 Arc::make_mut(&mut self.global_vars).retain(|name, _| !symbol_set.contains(name));
1580 self.clear_file_references(file);
1581 }
1582
1583 pub fn type_count(&self) -> usize {
1584 self.class_nodes
1585 .values()
1586 .filter(|node| node.active(self))
1587 .count()
1588 }
1589
1590 pub fn function_count(&self) -> usize {
1591 self.function_nodes
1592 .values()
1593 .filter(|node| node.active(self))
1594 .count()
1595 }
1596
1597 pub fn constant_count(&self) -> usize {
1598 self.global_constant_nodes
1599 .values()
1600 .filter(|node| node.active(self))
1601 .count()
1602 }
1603
1604 pub fn ingest_stub_slice(&mut self, slice: &StubSlice) {
1610 use std::collections::HashSet;
1611
1612 if let Some(file) = &slice.file {
1613 if let Some(namespace) = &slice.namespace {
1614 Arc::make_mut(&mut self.file_namespaces).insert(file.clone(), namespace.clone());
1615 }
1616 if !slice.imports.is_empty() {
1617 Arc::make_mut(&mut self.file_imports).insert(file.clone(), slice.imports.clone());
1618 }
1619 for (name, _) in &slice.global_vars {
1620 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1621 Arc::make_mut(&mut self.symbol_to_file)
1622 .insert(Arc::from(global_name), file.clone());
1623 }
1624 }
1625 for (name, ty) in &slice.global_vars {
1626 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1627 Arc::make_mut(&mut self.global_vars).insert(Arc::from(global_name), ty.clone());
1628 }
1629
1630 for cls in &slice.classes {
1631 if let Some(file) = &slice.file {
1632 Arc::make_mut(&mut self.symbol_to_file).insert(cls.fqcn.clone(), file.clone());
1633 }
1634 self.upsert_class_node(ClassNodeFields {
1635 is_abstract: cls.is_abstract,
1636 parent: cls.parent.clone(),
1637 interfaces: Arc::from(cls.interfaces.as_slice()),
1638 traits: Arc::from(cls.traits.as_slice()),
1639 template_params: Arc::from(cls.template_params.as_slice()),
1640 mixins: Arc::from(cls.mixins.as_slice()),
1641 deprecated: cls.deprecated.clone(),
1642 is_final: cls.is_final,
1643 is_readonly: cls.is_readonly,
1644 location: cls.location.clone(),
1645 extends_type_args: Arc::from(cls.extends_type_args.as_slice()),
1646 implements_type_args: Arc::from(
1647 cls.implements_type_args
1648 .iter()
1649 .map(|(iface, args)| (iface.clone(), Arc::from(args.as_slice())))
1650 .collect::<Vec<_>>(),
1651 ),
1652 ..ClassNodeFields::for_class(cls.fqcn.clone())
1653 });
1654 if self.method_nodes.contains_key(cls.fqcn.as_ref()) {
1655 let method_keep: HashSet<&str> =
1656 cls.own_methods.keys().map(|m| m.as_ref()).collect();
1657 self.prune_class_methods(&cls.fqcn, &method_keep);
1658 }
1659 for method in cls.own_methods.values() {
1660 self.upsert_method_node(method.as_ref());
1661 }
1662 if self.property_nodes.contains_key(cls.fqcn.as_ref()) {
1663 let prop_keep: HashSet<&str> =
1664 cls.own_properties.keys().map(|p| p.as_ref()).collect();
1665 self.prune_class_properties(&cls.fqcn, &prop_keep);
1666 }
1667 for prop in cls.own_properties.values() {
1668 self.upsert_property_node(&cls.fqcn, prop);
1669 }
1670 if self.class_constant_nodes.contains_key(cls.fqcn.as_ref()) {
1671 let const_keep: HashSet<&str> =
1672 cls.own_constants.keys().map(|c| c.as_ref()).collect();
1673 self.prune_class_constants(&cls.fqcn, &const_keep);
1674 }
1675 for constant in cls.own_constants.values() {
1676 self.upsert_class_constant_node(&cls.fqcn, constant);
1677 }
1678 }
1679
1680 for iface in &slice.interfaces {
1681 if let Some(file) = &slice.file {
1682 Arc::make_mut(&mut self.symbol_to_file).insert(iface.fqcn.clone(), file.clone());
1683 }
1684 self.upsert_class_node(ClassNodeFields {
1685 extends: Arc::from(iface.extends.as_slice()),
1686 template_params: Arc::from(iface.template_params.as_slice()),
1687 location: iface.location.clone(),
1688 ..ClassNodeFields::for_interface(iface.fqcn.clone())
1689 });
1690 if self.method_nodes.contains_key(iface.fqcn.as_ref()) {
1691 let method_keep: HashSet<&str> =
1692 iface.own_methods.keys().map(|m| m.as_ref()).collect();
1693 self.prune_class_methods(&iface.fqcn, &method_keep);
1694 }
1695 for method in iface.own_methods.values() {
1696 self.upsert_method_node(method.as_ref());
1697 }
1698 if self.class_constant_nodes.contains_key(iface.fqcn.as_ref()) {
1699 let const_keep: HashSet<&str> =
1700 iface.own_constants.keys().map(|c| c.as_ref()).collect();
1701 self.prune_class_constants(&iface.fqcn, &const_keep);
1702 }
1703 for constant in iface.own_constants.values() {
1704 self.upsert_class_constant_node(&iface.fqcn, constant);
1705 }
1706 }
1707
1708 for tr in &slice.traits {
1709 if let Some(file) = &slice.file {
1710 Arc::make_mut(&mut self.symbol_to_file).insert(tr.fqcn.clone(), file.clone());
1711 }
1712 self.upsert_class_node(ClassNodeFields {
1713 traits: Arc::from(tr.traits.as_slice()),
1714 template_params: Arc::from(tr.template_params.as_slice()),
1715 require_extends: Arc::from(tr.require_extends.as_slice()),
1716 require_implements: Arc::from(tr.require_implements.as_slice()),
1717 location: tr.location.clone(),
1718 ..ClassNodeFields::for_trait(tr.fqcn.clone())
1719 });
1720 if self.method_nodes.contains_key(tr.fqcn.as_ref()) {
1721 let method_keep: HashSet<&str> =
1722 tr.own_methods.keys().map(|m| m.as_ref()).collect();
1723 self.prune_class_methods(&tr.fqcn, &method_keep);
1724 }
1725 for method in tr.own_methods.values() {
1726 self.upsert_method_node(method.as_ref());
1727 }
1728 if self.property_nodes.contains_key(tr.fqcn.as_ref()) {
1729 let prop_keep: HashSet<&str> =
1730 tr.own_properties.keys().map(|p| p.as_ref()).collect();
1731 self.prune_class_properties(&tr.fqcn, &prop_keep);
1732 }
1733 for prop in tr.own_properties.values() {
1734 self.upsert_property_node(&tr.fqcn, prop);
1735 }
1736 if self.class_constant_nodes.contains_key(tr.fqcn.as_ref()) {
1737 let const_keep: HashSet<&str> =
1738 tr.own_constants.keys().map(|c| c.as_ref()).collect();
1739 self.prune_class_constants(&tr.fqcn, &const_keep);
1740 }
1741 for constant in tr.own_constants.values() {
1742 self.upsert_class_constant_node(&tr.fqcn, constant);
1743 }
1744 }
1745
1746 for en in &slice.enums {
1747 if let Some(file) = &slice.file {
1748 Arc::make_mut(&mut self.symbol_to_file).insert(en.fqcn.clone(), file.clone());
1749 }
1750 self.upsert_class_node(ClassNodeFields {
1751 interfaces: Arc::from(en.interfaces.as_slice()),
1752 is_backed_enum: en.scalar_type.is_some(),
1753 enum_scalar_type: en.scalar_type.clone(),
1754 location: en.location.clone(),
1755 ..ClassNodeFields::for_enum(en.fqcn.clone())
1756 });
1757 if self.method_nodes.contains_key(en.fqcn.as_ref()) {
1758 let mut method_keep: HashSet<&str> =
1759 en.own_methods.keys().map(|m| m.as_ref()).collect();
1760 method_keep.insert("cases");
1761 if en.scalar_type.is_some() {
1762 method_keep.insert("from");
1763 method_keep.insert("tryfrom");
1764 }
1765 self.prune_class_methods(&en.fqcn, &method_keep);
1766 }
1767 for method in en.own_methods.values() {
1768 self.upsert_method_node(method.as_ref());
1769 }
1770 let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
1771 fqcn: en.fqcn.clone(),
1772 name: Arc::from(name),
1773 params: vec![],
1774 return_type: Some(Union::mixed()),
1775 inferred_return_type: None,
1776 visibility: Visibility::Public,
1777 is_static: true,
1778 is_abstract: false,
1779 is_constructor: false,
1780 template_params: vec![],
1781 assertions: vec![],
1782 throws: vec![],
1783 is_final: false,
1784 is_internal: false,
1785 is_pure: false,
1786 deprecated: None,
1787 location: None,
1788 };
1789 let already = |name: &str| {
1790 en.own_methods
1791 .keys()
1792 .any(|k| k.as_ref().eq_ignore_ascii_case(name))
1793 };
1794 if !already("cases") {
1795 self.upsert_method_node(&synth_method("cases"));
1796 }
1797 if en.scalar_type.is_some() {
1798 if !already("from") {
1799 self.upsert_method_node(&synth_method("from"));
1800 }
1801 if !already("tryFrom") {
1802 self.upsert_method_node(&synth_method("tryFrom"));
1803 }
1804 }
1805 if self.class_constant_nodes.contains_key(en.fqcn.as_ref()) {
1806 let mut const_keep: HashSet<&str> =
1807 en.own_constants.keys().map(|c| c.as_ref()).collect();
1808 for case in en.cases.values() {
1809 const_keep.insert(case.name.as_ref());
1810 }
1811 self.prune_class_constants(&en.fqcn, &const_keep);
1812 }
1813 for constant in en.own_constants.values() {
1814 self.upsert_class_constant_node(&en.fqcn, constant);
1815 }
1816 for case in en.cases.values() {
1817 let case_const = ConstantStorage {
1818 name: case.name.clone(),
1819 ty: mir_types::Union::mixed(),
1820 visibility: None,
1821 is_final: false,
1822 location: case.location.clone(),
1823 };
1824 self.upsert_class_constant_node(&en.fqcn, &case_const);
1825 }
1826 }
1827
1828 for func in &slice.functions {
1829 if let Some(file) = &slice.file {
1830 Arc::make_mut(&mut self.symbol_to_file).insert(func.fqn.clone(), file.clone());
1831 }
1832 self.upsert_function_node(func);
1833 }
1834 for (fqn, ty) in &slice.constants {
1835 self.upsert_global_constant_node(fqn.clone(), ty.clone());
1836 }
1837 }
1838
1839 #[allow(clippy::too_many_arguments)]
1844 pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
1845 use salsa::Setter as _;
1846 let ClassNodeFields {
1847 fqcn,
1848 is_interface,
1849 is_trait,
1850 is_enum,
1851 is_abstract,
1852 parent,
1853 interfaces,
1854 traits,
1855 extends,
1856 template_params,
1857 require_extends,
1858 require_implements,
1859 is_backed_enum,
1860 mixins,
1861 deprecated,
1862 enum_scalar_type,
1863 is_final,
1864 is_readonly,
1865 location,
1866 extends_type_args,
1867 implements_type_args,
1868 } = fields;
1869 if let Some(&node) = self.class_nodes.get(&fqcn) {
1870 if node.active(self)
1883 && node.is_interface(self) == is_interface
1884 && node.is_trait(self) == is_trait
1885 && node.is_enum(self) == is_enum
1886 && node.is_abstract(self) == is_abstract
1887 && node.is_backed_enum(self) == is_backed_enum
1888 && node.parent(self) == parent
1889 && *node.interfaces(self) == *interfaces
1890 && *node.traits(self) == *traits
1891 && *node.extends(self) == *extends
1892 && *node.template_params(self) == *template_params
1893 && *node.require_extends(self) == *require_extends
1894 && *node.require_implements(self) == *require_implements
1895 && *node.mixins(self) == *mixins
1896 && node.deprecated(self) == deprecated
1897 && node.enum_scalar_type(self) == enum_scalar_type
1898 && node.is_final(self) == is_final
1899 && node.is_readonly(self) == is_readonly
1900 && node.location(self) == location
1901 && *node.extends_type_args(self) == *extends_type_args
1902 && *node.implements_type_args(self) == *implements_type_args
1903 {
1904 return node;
1905 }
1906 node.set_active(self).to(true);
1907 node.set_is_interface(self).to(is_interface);
1908 node.set_is_trait(self).to(is_trait);
1909 node.set_is_enum(self).to(is_enum);
1910 node.set_is_abstract(self).to(is_abstract);
1911 node.set_parent(self).to(parent);
1912 node.set_interfaces(self).to(interfaces);
1913 node.set_traits(self).to(traits);
1914 node.set_extends(self).to(extends);
1915 node.set_template_params(self).to(template_params);
1916 node.set_require_extends(self).to(require_extends);
1917 node.set_require_implements(self).to(require_implements);
1918 node.set_is_backed_enum(self).to(is_backed_enum);
1919 node.set_mixins(self).to(mixins);
1920 node.set_deprecated(self).to(deprecated);
1921 node.set_enum_scalar_type(self).to(enum_scalar_type);
1922 node.set_is_final(self).to(is_final);
1923 node.set_is_readonly(self).to(is_readonly);
1924 node.set_location(self).to(location);
1925 node.set_extends_type_args(self).to(extends_type_args);
1926 node.set_implements_type_args(self).to(implements_type_args);
1927 node
1928 } else {
1929 let node = ClassNode::new(
1930 self,
1931 fqcn.clone(),
1932 true,
1933 is_interface,
1934 is_trait,
1935 is_enum,
1936 is_abstract,
1937 parent,
1938 interfaces,
1939 traits,
1940 extends,
1941 template_params,
1942 require_extends,
1943 require_implements,
1944 is_backed_enum,
1945 mixins,
1946 deprecated,
1947 enum_scalar_type,
1948 is_final,
1949 is_readonly,
1950 location,
1951 extends_type_args,
1952 implements_type_args,
1953 );
1954 Arc::make_mut(&mut self.class_node_keys_lower)
1955 .insert(fqcn.to_ascii_lowercase(), fqcn.clone());
1956 Arc::make_mut(&mut self.class_nodes).insert(fqcn, node);
1957 node
1958 }
1959 }
1960
1961 pub fn deactivate_class_node(&mut self, fqcn: &str) {
1966 use salsa::Setter as _;
1967 if let Some(&node) = self.class_nodes.get(fqcn) {
1968 node.set_active(self).to(false);
1969 }
1970 }
1971
1972 pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
1974 use salsa::Setter as _;
1975 let fqn = &storage.fqn;
1976 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1977 if node.active(self)
1983 && node.short_name(self) == storage.short_name
1984 && node.is_pure(self) == storage.is_pure
1985 && node.deprecated(self) == storage.deprecated
1986 && node.return_type(self) == storage.return_type
1987 && node.location(self) == storage.location
1988 && *node.params(self) == *storage.params.as_slice()
1989 && *node.template_params(self) == *storage.template_params.as_slice()
1990 && *node.assertions(self) == *storage.assertions.as_slice()
1991 && *node.throws(self) == *storage.throws.as_slice()
1992 {
1993 return node;
1994 }
1995 node.set_active(self).to(true);
1996 node.set_short_name(self).to(storage.short_name.clone());
1997 node.set_params(self)
1998 .to(Arc::from(storage.params.as_slice()));
1999 node.set_return_type(self).to(storage.return_type.clone());
2000 node.set_template_params(self)
2001 .to(Arc::from(storage.template_params.as_slice()));
2002 node.set_assertions(self)
2003 .to(Arc::from(storage.assertions.as_slice()));
2004 node.set_throws(self)
2005 .to(Arc::from(storage.throws.as_slice()));
2006 node.set_deprecated(self).to(storage.deprecated.clone());
2007 node.set_is_pure(self).to(storage.is_pure);
2008 node.set_location(self).to(storage.location.clone());
2009 node
2010 } else {
2011 let node = FunctionNode::new(
2012 self,
2013 fqn.clone(),
2014 storage.short_name.clone(),
2015 true,
2016 Arc::from(storage.params.as_slice()),
2017 storage.return_type.clone(),
2018 storage.inferred_return_type.clone(),
2019 Arc::from(storage.template_params.as_slice()),
2020 Arc::from(storage.assertions.as_slice()),
2021 Arc::from(storage.throws.as_slice()),
2022 storage.deprecated.clone(),
2023 storage.is_pure,
2024 storage.location.clone(),
2025 );
2026 Arc::make_mut(&mut self.function_node_keys_lower)
2027 .insert(fqn.to_ascii_lowercase(), fqn.clone());
2028 Arc::make_mut(&mut self.function_nodes).insert(fqn.clone(), node);
2029 node
2030 }
2031 }
2032
2033 pub fn commit_inferred_return_types(&mut self, buf: &InferredReturnTypes) {
2045 use salsa::Setter as _;
2046 let funcs = std::mem::take(&mut *buf.functions.lock().expect("inferred buffer poisoned"));
2047 for (fqn, inferred) in funcs {
2048 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
2049 if !node.active(self) {
2050 continue;
2051 }
2052 let new = Some(inferred);
2053 if node.inferred_return_type(self) == new {
2054 continue;
2055 }
2056 node.set_inferred_return_type(self).to(new);
2057 }
2058 }
2059 let methods = std::mem::take(&mut *buf.methods.lock().expect("inferred buffer poisoned"));
2060 for (fqcn, name, inferred) in methods {
2061 let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
2062 name.clone()
2063 } else {
2064 Arc::from(name.to_lowercase().as_str())
2065 };
2066 let node = self
2067 .method_nodes
2068 .get(fqcn.as_ref())
2069 .and_then(|m| m.get(&name_lower))
2070 .copied();
2071 if let Some(node) = node {
2072 if !node.active(self) {
2073 continue;
2074 }
2075 let new = Some(inferred);
2076 if node.inferred_return_type(self) == new {
2077 continue;
2078 }
2079 node.set_inferred_return_type(self).to(new);
2080 }
2081 }
2082 }
2083
2084 pub fn deactivate_function_node(&mut self, fqn: &str) {
2086 use salsa::Setter as _;
2087 if let Some(&node) = self.function_nodes.get(fqn) {
2088 node.set_active(self).to(false);
2089 }
2090 }
2091
2092 pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
2094 use salsa::Setter as _;
2095 let fqcn = &storage.fqcn;
2096 let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
2097 let existing = self
2100 .method_nodes
2101 .get(fqcn.as_ref())
2102 .and_then(|m| m.get(&name_lower))
2103 .copied();
2104 if let Some(node) = existing {
2105 if node.active(self)
2109 && node.visibility(self) == storage.visibility
2110 && node.is_static(self) == storage.is_static
2111 && node.is_abstract(self) == storage.is_abstract
2112 && node.is_final(self) == storage.is_final
2113 && node.is_constructor(self) == storage.is_constructor
2114 && node.is_pure(self) == storage.is_pure
2115 && node.deprecated(self) == storage.deprecated
2116 && node.return_type(self) == storage.return_type
2117 && node.location(self) == storage.location
2118 && *node.params(self) == *storage.params.as_slice()
2119 && *node.template_params(self) == *storage.template_params.as_slice()
2120 && *node.assertions(self) == *storage.assertions.as_slice()
2121 && *node.throws(self) == *storage.throws.as_slice()
2122 {
2123 return node;
2124 }
2125 node.set_active(self).to(true);
2126 node.set_params(self)
2127 .to(Arc::from(storage.params.as_slice()));
2128 node.set_return_type(self).to(storage.return_type.clone());
2129 node.set_template_params(self)
2130 .to(Arc::from(storage.template_params.as_slice()));
2131 node.set_assertions(self)
2132 .to(Arc::from(storage.assertions.as_slice()));
2133 node.set_throws(self)
2134 .to(Arc::from(storage.throws.as_slice()));
2135 node.set_deprecated(self).to(storage.deprecated.clone());
2136 node.set_visibility(self).to(storage.visibility);
2137 node.set_is_static(self).to(storage.is_static);
2138 node.set_is_abstract(self).to(storage.is_abstract);
2139 node.set_is_final(self).to(storage.is_final);
2140 node.set_is_constructor(self).to(storage.is_constructor);
2141 node.set_is_pure(self).to(storage.is_pure);
2142 node.set_location(self).to(storage.location.clone());
2143 node
2144 } else {
2145 let node = MethodNode::new(
2147 self,
2148 fqcn.clone(),
2149 storage.name.clone(),
2150 true,
2151 Arc::from(storage.params.as_slice()),
2152 storage.return_type.clone(),
2153 storage.inferred_return_type.clone(),
2154 Arc::from(storage.template_params.as_slice()),
2155 Arc::from(storage.assertions.as_slice()),
2156 Arc::from(storage.throws.as_slice()),
2157 storage.deprecated.clone(),
2158 storage.visibility,
2159 storage.is_static,
2160 storage.is_abstract,
2161 storage.is_final,
2162 storage.is_constructor,
2163 storage.is_pure,
2164 storage.location.clone(),
2165 );
2166 Arc::make_mut(&mut self.method_nodes)
2167 .entry(fqcn.clone())
2168 .or_default()
2169 .insert(name_lower, node);
2170 node
2171 }
2172 }
2173
2174 pub fn deactivate_class_methods(&mut self, fqcn: &str) {
2176 use salsa::Setter as _;
2177 let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
2178 Some(methods) => methods.values().copied().collect(),
2179 None => return,
2180 };
2181 for node in nodes {
2182 node.set_active(self).to(false);
2183 }
2184 }
2185
2186 pub fn prune_class_methods<T>(&mut self, fqcn: &str, keep_lower: &std::collections::HashSet<T>)
2192 where
2193 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2194 {
2195 use salsa::Setter as _;
2196 let candidates: Vec<MethodNode> = self
2197 .method_nodes
2198 .get(fqcn)
2199 .map(|m| {
2200 m.iter()
2201 .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
2202 .map(|(_, n)| *n)
2203 .collect()
2204 })
2205 .unwrap_or_default();
2206 for node in candidates {
2207 if node.active(self) {
2208 node.set_active(self).to(false);
2209 }
2210 }
2211 }
2212
2213 pub fn prune_class_properties<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2215 where
2216 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2217 {
2218 use salsa::Setter as _;
2219 let candidates: Vec<PropertyNode> = self
2220 .property_nodes
2221 .get(fqcn)
2222 .map(|m| {
2223 m.iter()
2224 .filter(|(k, _)| !keep.contains(k.as_ref()))
2225 .map(|(_, n)| *n)
2226 .collect()
2227 })
2228 .unwrap_or_default();
2229 for node in candidates {
2230 if node.active(self) {
2231 node.set_active(self).to(false);
2232 }
2233 }
2234 }
2235
2236 pub fn prune_class_constants<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2238 where
2239 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2240 {
2241 use salsa::Setter as _;
2242 let candidates: Vec<ClassConstantNode> = self
2243 .class_constant_nodes
2244 .get(fqcn)
2245 .map(|m| {
2246 m.iter()
2247 .filter(|(k, _)| !keep.contains(k.as_ref()))
2248 .map(|(_, n)| *n)
2249 .collect()
2250 })
2251 .unwrap_or_default();
2252 for node in candidates {
2253 if node.active(self) {
2254 node.set_active(self).to(false);
2255 }
2256 }
2257 }
2258
2259 pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
2261 use salsa::Setter as _;
2262 let existing = self
2263 .property_nodes
2264 .get(fqcn.as_ref())
2265 .and_then(|m| m.get(storage.name.as_ref()))
2266 .copied();
2267 if let Some(node) = existing {
2268 if node.active(self)
2270 && node.visibility(self) == storage.visibility
2271 && node.is_static(self) == storage.is_static
2272 && node.is_readonly(self) == storage.is_readonly
2273 && node.ty(self) == storage.ty
2274 && node.location(self) == storage.location
2275 {
2276 return;
2277 }
2278 node.set_active(self).to(true);
2279 node.set_ty(self).to(storage.ty.clone());
2280 node.set_visibility(self).to(storage.visibility);
2281 node.set_is_static(self).to(storage.is_static);
2282 node.set_is_readonly(self).to(storage.is_readonly);
2283 node.set_location(self).to(storage.location.clone());
2284 } else {
2285 let node = PropertyNode::new(
2286 self,
2287 fqcn.clone(),
2288 storage.name.clone(),
2289 true,
2290 storage.ty.clone(),
2291 storage.visibility,
2292 storage.is_static,
2293 storage.is_readonly,
2294 storage.location.clone(),
2295 );
2296 Arc::make_mut(&mut self.property_nodes)
2297 .entry(fqcn.clone())
2298 .or_default()
2299 .insert(storage.name.clone(), node);
2300 }
2301 }
2302
2303 pub fn deactivate_class_properties(&mut self, fqcn: &str) {
2305 use salsa::Setter as _;
2306 let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
2307 Some(props) => props.values().copied().collect(),
2308 None => return,
2309 };
2310 for node in nodes {
2311 node.set_active(self).to(false);
2312 }
2313 }
2314
2315 pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
2317 use salsa::Setter as _;
2318 let existing = self
2319 .class_constant_nodes
2320 .get(fqcn.as_ref())
2321 .and_then(|m| m.get(storage.name.as_ref()))
2322 .copied();
2323 if let Some(node) = existing {
2324 if node.active(self)
2326 && node.visibility(self) == storage.visibility
2327 && node.is_final(self) == storage.is_final
2328 && node.ty(self) == storage.ty
2329 && node.location(self) == storage.location
2330 {
2331 return;
2332 }
2333 node.set_active(self).to(true);
2334 node.set_ty(self).to(storage.ty.clone());
2335 node.set_visibility(self).to(storage.visibility);
2336 node.set_is_final(self).to(storage.is_final);
2337 node.set_location(self).to(storage.location.clone());
2338 } else {
2339 let node = ClassConstantNode::new(
2340 self,
2341 fqcn.clone(),
2342 storage.name.clone(),
2343 true,
2344 storage.ty.clone(),
2345 storage.visibility,
2346 storage.is_final,
2347 storage.location.clone(),
2348 );
2349 Arc::make_mut(&mut self.class_constant_nodes)
2350 .entry(fqcn.clone())
2351 .or_default()
2352 .insert(storage.name.clone(), node);
2353 }
2354 }
2355
2356 pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
2358 use salsa::Setter as _;
2359 if let Some(&node) = self.global_constant_nodes.get(&fqn) {
2360 if node.active(self) && node.ty(self) == ty {
2362 return node;
2363 }
2364 node.set_active(self).to(true);
2365 node.set_ty(self).to(ty);
2366 node
2367 } else {
2368 let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
2369 Arc::make_mut(&mut self.global_constant_nodes).insert(fqn, node);
2370 node
2371 }
2372 }
2373
2374 pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
2376 use salsa::Setter as _;
2377 if let Some(&node) = self.global_constant_nodes.get(fqn) {
2378 node.set_active(self).to(false);
2379 }
2380 }
2381
2382 pub fn deactivate_class_constants(&mut self, fqcn: &str) {
2384 use salsa::Setter as _;
2385 let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
2386 Some(consts) => consts.values().copied().collect(),
2387 None => return,
2388 };
2389 for node in nodes {
2390 node.set_active(self).to(false);
2391 }
2392 }
2393}
2394
2395#[salsa::accumulator]
2421#[derive(Clone, Debug)]
2422pub struct IssueAccumulator(pub Issue);
2423
2424#[derive(Clone, Debug, PartialEq, Eq)]
2433pub struct RefLoc {
2434 pub symbol_key: Arc<str>,
2435 pub file: Arc<str>,
2436 pub line: u32,
2437 pub col_start: u16,
2438 pub col_end: u16,
2439}
2440
2441#[salsa::accumulator]
2448#[derive(Clone, Debug)]
2449pub struct RefLocAccumulator(pub RefLoc);
2450
2451#[salsa::input]
2456pub struct AnalyzeFileInput {
2457 pub php_version: Arc<str>,
2460}
2461
2462#[salsa::tracked]
2477pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, _input: AnalyzeFileInput) {
2478 use salsa::Accumulator as _;
2479 let path = file.path(db);
2480 let text = file.text(db);
2481
2482 let arena = bumpalo::Bump::new();
2483 let parsed = php_rs_parser::parse(&arena, &text);
2484
2485 for err in &parsed.errors {
2486 let issue = Issue::new(
2487 mir_issues::IssueKind::ParseError {
2488 message: err.to_string(),
2489 },
2490 mir_issues::Location {
2491 file: path.clone(),
2492 line: 1,
2493 line_end: 1,
2494 col_start: 0,
2495 col_end: 0,
2496 },
2497 );
2498 IssueAccumulator(issue).accumulate(db);
2499 }
2500}
2501
2502#[cfg(test)]
2507mod tests {
2508 use super::*;
2509 use salsa::Setter as _;
2510
2511 fn upsert_class(
2512 db: &mut MirDb,
2513 fqcn: &str,
2514 parent: Option<Arc<str>>,
2515 extends: Arc<[Arc<str>]>,
2516 is_interface: bool,
2517 ) -> ClassNode {
2518 db.upsert_class_node(ClassNodeFields {
2519 is_interface,
2520 parent,
2521 extends,
2522 ..ClassNodeFields::for_class(Arc::from(fqcn))
2523 })
2524 }
2525
2526 #[test]
2527 fn mirdb_constructs() {
2528 let _db = MirDb::default();
2529 }
2530
2531 #[test]
2532 fn source_file_input_roundtrip() {
2533 let db = MirDb::default();
2534 let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
2535 assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
2536 assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
2537 }
2538
2539 #[test]
2540 fn collect_file_definitions_basic() {
2541 let db = MirDb::default();
2542 let src = Arc::from("<?php class Foo {}");
2543 let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
2544 let defs = collect_file_definitions(&db, file);
2545 assert!(defs.issues.is_empty());
2546 assert_eq!(defs.slice.classes.len(), 1);
2547 assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
2548 }
2549
2550 #[test]
2551 fn collect_file_definitions_memoized() {
2552 let db = MirDb::default();
2553 let file = SourceFile::new(
2554 &db,
2555 Arc::from("/tmp/memo.php"),
2556 Arc::from("<?php class Bar {}"),
2557 );
2558
2559 let defs1 = collect_file_definitions(&db, file);
2560 let defs2 = collect_file_definitions(&db, file);
2561 assert!(
2562 Arc::ptr_eq(&defs1.slice, &defs2.slice),
2563 "unchanged file must return the memoized result"
2564 );
2565 }
2566
2567 #[test]
2568 fn analyze_file_accumulates_parse_errors() {
2569 let db = MirDb::default();
2570 let file = SourceFile::new(
2572 &db,
2573 Arc::from("/tmp/parse_err.php"),
2574 Arc::from("<?php $x = \"unterminated"),
2575 );
2576 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2577 analyze_file(&db, file, input);
2578 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2579 assert!(
2580 !issues.is_empty(),
2581 "expected parse error to surface as accumulated IssueAccumulator"
2582 );
2583 assert!(matches!(
2584 issues[0].0.kind,
2585 mir_issues::IssueKind::ParseError { .. }
2586 ));
2587 }
2588
2589 #[test]
2590 fn analyze_file_clean_input_accumulates_nothing() {
2591 let db = MirDb::default();
2592 let file = SourceFile::new(
2593 &db,
2594 Arc::from("/tmp/clean.php"),
2595 Arc::from("<?php class Foo {}"),
2596 );
2597 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2598 analyze_file(&db, file, input);
2599 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2600 let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
2601 assert!(issues.is_empty());
2602 assert!(refs.is_empty());
2603 }
2604
2605 #[test]
2606 fn collect_file_definitions_recomputes_on_change() {
2607 let mut db = MirDb::default();
2608 let file = SourceFile::new(
2609 &db,
2610 Arc::from("/tmp/memo2.php"),
2611 Arc::from("<?php class Foo {}"),
2612 );
2613
2614 let defs1 = collect_file_definitions(&db, file);
2615 file.set_text(&mut db)
2616 .to(Arc::from("<?php class Foo {} class Bar {}"));
2617 let defs2 = collect_file_definitions(&db, file);
2618
2619 assert!(
2620 !Arc::ptr_eq(&defs1.slice, &defs2.slice),
2621 "changed file must produce a new result"
2622 );
2623 assert_eq!(defs2.slice.classes.len(), 2);
2624 }
2625
2626 #[test]
2627 fn class_ancestors_empty_for_root_class() {
2628 let mut db = MirDb::default();
2629 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2630 let ancestors = class_ancestors(&db, node);
2631 assert!(ancestors.0.is_empty(), "root class has no ancestors");
2632 }
2633
2634 #[test]
2635 fn class_ancestors_single_parent() {
2636 let mut db = MirDb::default();
2637 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2638 let child = upsert_class(
2639 &mut db,
2640 "Child",
2641 Some(Arc::from("Base")),
2642 Arc::from([]),
2643 false,
2644 );
2645 let ancestors = class_ancestors(&db, child);
2646 assert_eq!(ancestors.0.len(), 1);
2647 assert_eq!(ancestors.0[0].as_ref(), "Base");
2648 }
2649
2650 #[test]
2651 fn class_ancestors_transitive() {
2652 let mut db = MirDb::default();
2653 upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
2654 upsert_class(
2655 &mut db,
2656 "Parent",
2657 Some(Arc::from("GrandParent")),
2658 Arc::from([]),
2659 false,
2660 );
2661 let child = upsert_class(
2662 &mut db,
2663 "Child",
2664 Some(Arc::from("Parent")),
2665 Arc::from([]),
2666 false,
2667 );
2668 let ancestors = class_ancestors(&db, child);
2669 assert_eq!(ancestors.0.len(), 2);
2670 assert_eq!(ancestors.0[0].as_ref(), "Parent");
2671 assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
2672 }
2673
2674 #[test]
2675 fn class_ancestors_cycle_returns_empty() {
2676 let mut db = MirDb::default();
2677 let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
2679 let ancestors = class_ancestors(&db, node_a);
2680 assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
2682 }
2683
2684 #[test]
2685 fn class_ancestors_inactive_node_returns_empty() {
2686 let mut db = MirDb::default();
2687 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2688 db.deactivate_class_node("Foo");
2689 let ancestors = class_ancestors(&db, node);
2690 assert!(ancestors.0.is_empty(), "inactive node must yield empty");
2691 }
2692
2693 #[test]
2694 fn class_ancestors_recomputes_on_parent_change() {
2695 let mut db = MirDb::default();
2696 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2697 let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
2698
2699 let before = class_ancestors(&db, child);
2700 assert!(before.0.is_empty());
2701
2702 child.set_parent(&mut db).to(Some(Arc::from("Base")));
2704
2705 let after = class_ancestors(&db, child);
2706 assert_eq!(after.0.len(), 1);
2707 assert_eq!(after.0[0].as_ref(), "Base");
2708 }
2709
2710 #[test]
2711 fn interface_ancestors_via_extends() {
2712 let mut db = MirDb::default();
2713 upsert_class(&mut db, "Countable", None, Arc::from([]), true);
2714 let child_iface = upsert_class(
2715 &mut db,
2716 "Collection",
2717 None,
2718 Arc::from([Arc::from("Countable")]),
2719 true,
2720 );
2721 let ancestors = class_ancestors(&db, child_iface);
2722 assert_eq!(ancestors.0.len(), 1);
2723 assert_eq!(ancestors.0[0].as_ref(), "Countable");
2724 }
2725
2726 #[test]
2727 fn type_exists_via_db_tracks_active_state() {
2728 let mut db = MirDb::default();
2729 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2730 assert!(type_exists_via_db(&db, "Foo"));
2731 assert!(!type_exists_via_db(&db, "Bar"));
2732 db.deactivate_class_node("Foo");
2733 assert!(!type_exists_via_db(&db, "Foo"));
2734 }
2735
2736 #[test]
2737 fn clone_preserves_class_node_lookups() {
2738 let mut db = MirDb::default();
2741 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2742 let cloned = db.clone();
2743 assert!(
2744 type_exists_via_db(&cloned, "Foo"),
2745 "clone must observe nodes registered before clone()"
2746 );
2747 assert!(
2748 !type_exists_via_db(&cloned, "Bar"),
2749 "clone must not observe nodes that were never registered"
2750 );
2751 let foo_node = cloned.lookup_class_node("Foo").expect("registered");
2753 let ancestors = class_ancestors(&cloned, foo_node);
2754 assert!(ancestors.0.is_empty(), "Foo has no ancestors");
2755 }
2756
2757 fn upsert_class_with_traits(
2762 db: &mut MirDb,
2763 fqcn: &str,
2764 parent: Option<Arc<str>>,
2765 traits: &[&str],
2766 is_interface: bool,
2767 is_trait: bool,
2768 ) -> ClassNode {
2769 db.upsert_class_node(ClassNodeFields {
2770 is_interface,
2771 is_trait,
2772 parent,
2773 traits: Arc::from(
2774 traits
2775 .iter()
2776 .map(|t| Arc::<str>::from(*t))
2777 .collect::<Vec<_>>(),
2778 ),
2779 ..ClassNodeFields::for_class(Arc::from(fqcn))
2780 })
2781 }
2782
2783 fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
2784 let storage = MethodStorage {
2785 name: Arc::from(name),
2786 fqcn: Arc::from(fqcn),
2787 params: vec![],
2788 return_type: None,
2789 inferred_return_type: None,
2790 visibility: Visibility::Public,
2791 is_static: false,
2792 is_abstract,
2793 is_final: false,
2794 is_constructor: name == "__construct",
2795 template_params: vec![],
2796 assertions: vec![],
2797 throws: vec![],
2798 deprecated: None,
2799 is_internal: false,
2800 is_pure: false,
2801 location: None,
2802 };
2803 db.upsert_method_node(&storage)
2804 }
2805
2806 fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
2807 db.upsert_class_node(ClassNodeFields {
2808 interfaces: Arc::from(
2809 interfaces
2810 .iter()
2811 .map(|i| Arc::<str>::from(*i))
2812 .collect::<Vec<_>>(),
2813 ),
2814 is_backed_enum: is_backed,
2815 ..ClassNodeFields::for_enum(Arc::from(fqcn))
2816 })
2817 }
2818
2819 #[test]
2824 fn method_exists_via_db_finds_own_method() {
2825 let mut db = MirDb::default();
2826 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2827 upsert_method(&mut db, "Foo", "bar", false);
2828 assert!(method_exists_via_db(&db, "Foo", "bar"));
2829 assert!(!method_exists_via_db(&db, "Foo", "missing"));
2830 }
2831
2832 #[test]
2833 fn method_exists_via_db_walks_parent() {
2834 let mut db = MirDb::default();
2835 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2836 upsert_method(&mut db, "Base", "inherited", false);
2837 upsert_class(
2838 &mut db,
2839 "Child",
2840 Some(Arc::from("Base")),
2841 Arc::from([]),
2842 false,
2843 );
2844 assert!(method_exists_via_db(&db, "Child", "inherited"));
2845 }
2846
2847 #[test]
2848 fn method_exists_via_db_walks_traits_transitively() {
2849 let mut db = MirDb::default();
2850 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2851 upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
2852 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2853 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2854 assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
2855 }
2856
2857 #[test]
2858 fn method_exists_via_db_is_case_insensitive() {
2859 let mut db = MirDb::default();
2860 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2861 upsert_method(&mut db, "Foo", "doStuff", false);
2862 assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
2864 assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
2865 }
2866
2867 #[test]
2868 fn method_exists_via_db_unknown_class_returns_false() {
2869 let db = MirDb::default();
2870 assert!(!method_exists_via_db(&db, "Nope", "anything"));
2871 }
2872
2873 #[test]
2874 fn method_exists_via_db_inactive_class_returns_false() {
2875 let mut db = MirDb::default();
2876 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2877 upsert_method(&mut db, "Foo", "bar", false);
2878 db.deactivate_class_node("Foo");
2879 assert!(!method_exists_via_db(&db, "Foo", "bar"));
2880 }
2881
2882 #[test]
2883 fn method_exists_via_db_finds_abstract_methods() {
2884 let mut db = MirDb::default();
2887 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2888 upsert_method(&mut db, "Foo", "abstr", true);
2889 assert!(method_exists_via_db(&db, "Foo", "abstr"));
2890 }
2891
2892 #[test]
2897 fn method_is_concretely_implemented_skips_abstract() {
2898 let mut db = MirDb::default();
2899 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2900 upsert_method(&mut db, "Foo", "abstr", true);
2901 assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
2902 }
2903
2904 #[test]
2905 fn method_is_concretely_implemented_finds_concrete_in_trait() {
2906 let mut db = MirDb::default();
2907 upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
2908 upsert_method(&mut db, "MyTrait", "provided", false);
2909 upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
2910 assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
2911 }
2912
2913 #[test]
2914 fn method_is_concretely_implemented_skips_interface_definitions() {
2915 let mut db = MirDb::default();
2918 upsert_class(&mut db, "I", None, Arc::from([]), true);
2919 upsert_method(&mut db, "I", "m", false);
2920 upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
2921 assert!(!method_is_concretely_implemented(&db, "C", "m"));
2923 }
2924
2925 #[test]
2930 fn extends_or_implements_via_db_self_match() {
2931 let mut db = MirDb::default();
2932 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2933 assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
2934 }
2935
2936 #[test]
2937 fn extends_or_implements_via_db_transitive() {
2938 let mut db = MirDb::default();
2939 upsert_class(&mut db, "Animal", None, Arc::from([]), false);
2940 upsert_class(
2941 &mut db,
2942 "Mammal",
2943 Some(Arc::from("Animal")),
2944 Arc::from([]),
2945 false,
2946 );
2947 upsert_class(
2948 &mut db,
2949 "Dog",
2950 Some(Arc::from("Mammal")),
2951 Arc::from([]),
2952 false,
2953 );
2954 assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
2955 assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
2956 assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
2957 }
2958
2959 #[test]
2960 fn extends_or_implements_via_db_unknown_returns_false() {
2961 let db = MirDb::default();
2962 assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
2963 }
2964
2965 #[test]
2966 fn extends_or_implements_via_db_unit_enum_implicit() {
2967 let mut db = MirDb::default();
2968 upsert_enum(&mut db, "Status", &[], false);
2969 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2970 assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
2971 assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2973 }
2974
2975 #[test]
2976 fn extends_or_implements_via_db_backed_enum_implicit() {
2977 let mut db = MirDb::default();
2978 upsert_enum(&mut db, "Status", &[], true);
2979 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2980 assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2981 assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
2982 }
2983
2984 #[test]
2985 fn extends_or_implements_via_db_enum_declared_interface() {
2986 let mut db = MirDb::default();
2987 upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
2988 upsert_enum(&mut db, "Status", &["Stringable"], false);
2989 assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
2990 }
2991
2992 #[test]
2997 fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
2998 let mut db = MirDb::default();
2999 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3000 upsert_class(
3001 &mut db,
3002 "Child",
3003 Some(Arc::from("Base")),
3004 Arc::from([]),
3005 false,
3006 );
3007 assert!(!has_unknown_ancestor_via_db(&db, "Child"));
3008 }
3009
3010 #[test]
3011 fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
3012 let mut db = MirDb::default();
3013 upsert_class(
3015 &mut db,
3016 "Child",
3017 Some(Arc::from("Missing")),
3018 Arc::from([]),
3019 false,
3020 );
3021 assert!(has_unknown_ancestor_via_db(&db, "Child"));
3022 }
3023
3024 #[test]
3025 fn class_template_params_via_db_returns_registered_params() {
3026 use mir_types::Variance;
3027 let mut db = MirDb::default();
3028 let tp = TemplateParam {
3029 name: Arc::from("T"),
3030 bound: None,
3031 defining_entity: Arc::from("Box"),
3032 variance: Variance::Invariant,
3033 };
3034 db.upsert_class_node(ClassNodeFields {
3035 template_params: Arc::from([tp.clone()]),
3036 ..ClassNodeFields::for_class(Arc::from("Box"))
3037 });
3038 let got = class_template_params_via_db(&db, "Box").expect("registered");
3039 assert_eq!(got.len(), 1);
3040 assert_eq!(got[0].name.as_ref(), "T");
3041
3042 assert!(class_template_params_via_db(&db, "Missing").is_none());
3043 db.deactivate_class_node("Box");
3044 assert!(class_template_params_via_db(&db, "Box").is_none());
3045 }
3046
3047 fn upsert_class_with_mixins(
3052 db: &mut MirDb,
3053 fqcn: &str,
3054 parent: Option<Arc<str>>,
3055 mixins: &[&str],
3056 ) -> ClassNode {
3057 db.upsert_class_node(ClassNodeFields {
3058 parent,
3059 mixins: Arc::from(
3060 mixins
3061 .iter()
3062 .map(|m| Arc::<str>::from(*m))
3063 .collect::<Vec<_>>(),
3064 ),
3065 ..ClassNodeFields::for_class(Arc::from(fqcn))
3066 })
3067 }
3068
3069 #[test]
3070 fn lookup_method_in_chain_finds_own_then_ancestor() {
3071 let mut db = MirDb::default();
3072 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3073 upsert_method(&mut db, "Base", "shared", false);
3074 upsert_class(
3075 &mut db,
3076 "Child",
3077 Some(Arc::from("Base")),
3078 Arc::from([]),
3079 false,
3080 );
3081 upsert_method(&mut db, "Child", "shared", false);
3082 let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
3084 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3085 upsert_method(&mut db, "Base", "only_in_base", false);
3087 let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
3088 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3089 }
3090
3091 #[test]
3092 fn lookup_method_in_chain_walks_trait_of_traits() {
3093 let mut db = MirDb::default();
3094 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
3095 upsert_method(&mut db, "InnerTrait", "deep", false);
3096 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
3097 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
3098 let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
3099 assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
3100 }
3101
3102 #[test]
3103 fn lookup_method_in_chain_walks_mixins() {
3104 let mut db = MirDb::default();
3105 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3106 upsert_method(&mut db, "MixinTarget", "magic", false);
3107 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3108 let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
3109 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3110 }
3111
3112 #[test]
3113 fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
3114 let mut db = MirDb::default();
3115 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3117 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3118 assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
3119 }
3120
3121 #[test]
3122 fn lookup_method_in_chain_is_case_insensitive() {
3123 let mut db = MirDb::default();
3124 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3125 upsert_method(&mut db, "Foo", "doStuff", false);
3126 assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
3127 assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
3128 }
3129
3130 #[test]
3131 fn lookup_method_in_chain_unknown_returns_none() {
3132 let db = MirDb::default();
3133 assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
3134 }
3135
3136 fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
3141 let storage = PropertyStorage {
3142 name: Arc::from(name),
3143 ty: None,
3144 inferred_ty: None,
3145 visibility: Visibility::Public,
3146 is_static: false,
3147 is_readonly,
3148 default: None,
3149 location: None,
3150 };
3151 let owner = Arc::<str>::from(fqcn);
3152 db.upsert_property_node(&owner, &storage);
3153 db.lookup_property_node(fqcn, name).expect("registered")
3154 }
3155
3156 #[test]
3157 fn lookup_property_in_chain_own_then_ancestor() {
3158 let mut db = MirDb::default();
3159 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3160 upsert_property(&mut db, "Base", "x", false);
3161 upsert_class(
3162 &mut db,
3163 "Child",
3164 Some(Arc::from("Base")),
3165 Arc::from([]),
3166 false,
3167 );
3168 let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
3170 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3171 upsert_property(&mut db, "Child", "x", true);
3173 let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
3174 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3175 assert!(found.is_readonly(&db));
3176 }
3177
3178 #[test]
3179 fn lookup_property_in_chain_walks_mixins() {
3180 let mut db = MirDb::default();
3181 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3182 upsert_property(&mut db, "MixinTarget", "exposed", false);
3183 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3184 let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
3185 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3186 }
3187
3188 #[test]
3189 fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
3190 let mut db = MirDb::default();
3191 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3192 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3193 assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
3194 }
3195
3196 #[test]
3197 fn lookup_property_in_chain_is_case_sensitive() {
3198 let mut db = MirDb::default();
3199 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3200 upsert_property(&mut db, "Foo", "myProp", false);
3201 assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
3202 assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
3204 }
3205
3206 #[test]
3207 fn lookup_property_in_chain_inactive_returns_none() {
3208 let mut db = MirDb::default();
3209 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3210 upsert_property(&mut db, "Foo", "x", false);
3211 db.deactivate_class_node("Foo");
3212 assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
3213 }
3214
3215 fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
3220 let storage = ConstantStorage {
3221 name: Arc::from(name),
3222 ty: mir_types::Union::mixed(),
3223 visibility: None,
3224 is_final: false,
3225 location: None,
3226 };
3227 let owner = Arc::<str>::from(fqcn);
3228 db.upsert_class_constant_node(&owner, &storage);
3229 }
3230
3231 #[test]
3232 fn class_constant_exists_in_chain_finds_own() {
3233 let mut db = MirDb::default();
3234 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3235 upsert_constant(&mut db, "Foo", "MAX");
3236 assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
3237 assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
3238 }
3239
3240 #[test]
3241 fn class_constant_exists_in_chain_walks_parent() {
3242 let mut db = MirDb::default();
3243 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3244 upsert_constant(&mut db, "Base", "VERSION");
3245 upsert_class(
3246 &mut db,
3247 "Child",
3248 Some(Arc::from("Base")),
3249 Arc::from([]),
3250 false,
3251 );
3252 assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
3253 }
3254
3255 #[test]
3256 fn class_constant_exists_in_chain_walks_interface() {
3257 let mut db = MirDb::default();
3258 upsert_class(&mut db, "I", None, Arc::from([]), true);
3259 upsert_constant(&mut db, "I", "TYPE");
3260 db.upsert_class_node(ClassNodeFields {
3263 interfaces: Arc::from([Arc::from("I")]),
3264 ..ClassNodeFields::for_class(Arc::from("Impl"))
3265 });
3266 assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
3267 }
3268
3269 #[test]
3270 fn class_constant_exists_in_chain_walks_direct_trait() {
3271 let mut db = MirDb::default();
3272 upsert_class_with_traits(&mut db, "T", None, &[], false, true);
3273 upsert_constant(&mut db, "T", "FROM_TRAIT");
3274 upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
3275 assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
3276 }
3277
3278 #[test]
3279 fn class_constant_exists_in_chain_unknown_class_returns_false() {
3280 let db = MirDb::default();
3281 assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
3282 }
3283
3284 #[test]
3285 fn class_constant_exists_in_chain_inactive_returns_false() {
3286 let mut db = MirDb::default();
3287 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3288 upsert_constant(&mut db, "Foo", "X");
3289 db.deactivate_class_node("Foo");
3290 db.deactivate_class_constants("Foo");
3291 assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
3292 }
3293
3294 #[test]
3300 fn parallel_reads_then_serial_write_does_not_deadlock() {
3301 use rayon::prelude::*;
3302 use std::sync::mpsc;
3303 use std::time::Duration;
3304
3305 let (tx, rx) = mpsc::channel::<()>();
3306 std::thread::spawn(move || {
3307 let mut db = MirDb::default();
3308 let storage = mir_codebase::storage::FunctionStorage {
3309 fqn: Arc::from("foo"),
3310 short_name: Arc::from("foo"),
3311 params: vec![],
3312 return_type: None,
3313 inferred_return_type: None,
3314 template_params: vec![],
3315 assertions: vec![],
3316 throws: vec![],
3317 deprecated: None,
3318 is_pure: false,
3319 location: None,
3320 };
3321 let node = db.upsert_function_node(&storage);
3322
3323 let db_for_sweep = db.clone();
3325 (0..256u32)
3326 .into_par_iter()
3327 .for_each_with(db_for_sweep, |db, _| {
3328 let _ = node.return_type(&*db as &dyn MirDatabase);
3329 });
3330
3331 node.set_return_type(&mut db).to(Some(Union::mixed()));
3335 assert_eq!(node.return_type(&db), Some(Union::mixed()));
3336 tx.send(()).unwrap();
3337 });
3338
3339 match rx.recv_timeout(Duration::from_secs(30)) {
3340 Ok(()) => {}
3341 Err(_) => {
3342 panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
3343 }
3344 }
3345 }
3346
3347 #[test]
3358 fn sibling_clone_blocks_setter_until_dropped() {
3359 use std::sync::mpsc;
3360 use std::time::Duration;
3361
3362 let mut db = MirDb::default();
3363 let storage = mir_codebase::storage::FunctionStorage {
3364 fqn: Arc::from("foo"),
3365 short_name: Arc::from("foo"),
3366 params: vec![],
3367 return_type: None,
3368 inferred_return_type: None,
3369 template_params: vec![],
3370 assertions: vec![],
3371 throws: vec![],
3372 deprecated: None,
3373 is_pure: false,
3374 location: None,
3375 };
3376 let node = db.upsert_function_node(&storage);
3377
3378 let sibling = db.clone();
3379
3380 let (tx, rx) = mpsc::channel::<()>();
3383 let writer = std::thread::spawn(move || {
3384 node.set_return_type(&mut db).to(Some(Union::mixed()));
3385 tx.send(()).unwrap();
3386 });
3387
3388 match rx.recv_timeout(Duration::from_millis(500)) {
3391 Err(mpsc::RecvTimeoutError::Timeout) => { }
3392 Ok(()) => panic!(
3393 "setter completed while sibling clone was alive — strong-count==1 \
3394 invariant of `cancel_others` is broken; commit_inferred_return_types \
3395 cannot rely on tight-scoping clones"
3396 ),
3397 Err(e) => panic!("unexpected channel error: {e:?}"),
3398 }
3399
3400 drop(sibling);
3402
3403 match rx.recv_timeout(Duration::from_secs(5)) {
3404 Ok(()) => {}
3405 Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
3406 }
3407 writer.join().expect("writer thread panicked");
3408 }
3409}