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
14use crate::pass2::Pass2Driver;
15use crate::PhpVersion;
16
17#[salsa::db]
21pub trait MirDatabase: salsa::Database {
22 fn php_version_str(&self) -> Arc<str>;
24
25 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode>;
32
33 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode>;
35
36 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode>;
42
43 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode>;
45
46 fn lookup_class_constant_node(&self, fqcn: &str, const_name: &str)
48 -> Option<ClassConstantNode>;
49
50 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode>;
52
53 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode>;
56
57 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode>;
60
61 fn active_class_node_fqcns(&self) -> Vec<Arc<str>>;
65
66 fn active_function_node_fqns(&self) -> Vec<Arc<str>>;
69
70 fn file_namespace(&self, file: &str) -> Option<Arc<str>>;
72
73 fn file_imports(&self, file: &str) -> HashMap<String, String>;
75
76 fn global_var_type(&self, name: &str) -> Option<Union>;
78
79 fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)>;
81
82 fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>>;
84
85 fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>>;
87
88 fn record_reference_location(&self, loc: RefLoc);
90
91 fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]);
93
94 fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)>;
96
97 fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)>;
99
100 fn has_reference(&self, symbol: &str) -> bool;
102
103 fn clear_file_references(&self, file: &str);
105}
106
107#[salsa::input]
113pub struct SourceFile {
114 pub path: Arc<str>,
115 pub text: Arc<str>,
116}
117
118#[derive(Clone, Debug)]
122pub struct FileDefinitions {
123 pub slice: Arc<StubSlice>,
124 pub issues: Arc<Vec<Issue>>,
125}
126
127impl PartialEq for FileDefinitions {
128 fn eq(&self, other: &Self) -> bool {
129 Arc::ptr_eq(&self.slice, &other.slice) && Arc::ptr_eq(&self.issues, &other.issues)
130 }
131}
132
133unsafe impl salsa::Update for FileDefinitions {
134 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
135 unsafe { *old_pointer = new_value };
136 true
137 }
138}
139
140pub type ImplementsTypeArgs = Arc<[(Arc<str>, Arc<[Union]>)]>;
145
146#[salsa::input]
155pub struct ClassNode {
156 pub fqcn: Arc<str>,
157 pub active: bool,
160 pub is_interface: bool,
161 pub is_trait: bool,
166 pub is_enum: bool,
168 pub is_abstract: bool,
171 pub parent: Option<Arc<str>>,
173 pub interfaces: Arc<[Arc<str>]>,
175 pub traits: Arc<[Arc<str>]>,
178 pub extends: Arc<[Arc<str>]>,
180 pub template_params: Arc<[TemplateParam]>,
183 pub require_extends: Arc<[Arc<str>]>,
187 pub require_implements: Arc<[Arc<str>]>,
191 pub is_backed_enum: bool,
196 pub mixins: Arc<[Arc<str>]>,
201 pub deprecated: Option<Arc<str>>,
206 pub enum_scalar_type: Option<Union>,
211 pub is_final: bool,
215 pub is_readonly: bool,
218 pub location: Option<Location>,
223 pub extends_type_args: Arc<[Union]>,
226 pub implements_type_args: ImplementsTypeArgs,
229}
230
231#[derive(Debug, Clone, Copy)]
238pub struct ClassKind {
239 pub is_interface: bool,
240 pub is_trait: bool,
241 pub is_enum: bool,
242 pub is_abstract: bool,
243}
244
245pub fn class_kind_via_db(db: &dyn MirDatabase, fqcn: &str) -> Option<ClassKind> {
251 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
252 Some(ClassKind {
253 is_interface: node.is_interface(db),
254 is_trait: node.is_trait(db),
255 is_enum: node.is_enum(db),
256 is_abstract: node.is_abstract(db),
257 })
258}
259
260pub fn type_exists_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
266 db.lookup_class_node(fqcn).is_some_and(|n| n.active(db))
267}
268
269pub fn function_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
270 db.lookup_function_node(fqn).is_some_and(|n| n.active(db))
271}
272
273pub fn constant_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
274 db.lookup_global_constant_node(fqn)
275 .is_some_and(|n| n.active(db))
276}
277
278pub fn resolve_name_via_db(db: &dyn MirDatabase, file: &str, name: &str) -> String {
279 if name.starts_with('\\') {
280 return name.trim_start_matches('\\').to_string();
281 }
282
283 let lower = name.to_ascii_lowercase();
284 if matches!(lower.as_str(), "self" | "static" | "parent") {
285 return name.to_string();
286 }
287
288 if name.contains('\\') {
289 if let Some(imports) = (!name.starts_with('\\')).then(|| db.file_imports(file)) {
290 if let Some((first, rest)) = name.split_once('\\') {
291 if let Some(base) = imports.get(first) {
292 return format!("{base}\\{rest}");
293 }
294 }
295 }
296 if type_exists_via_db(db, name) {
297 return name.to_string();
298 }
299 if let Some(ns) = db.file_namespace(file) {
300 let qualified = format!("{}\\{}", ns, name);
301 if type_exists_via_db(db, &qualified) {
302 return qualified;
303 }
304 }
305 return name.to_string();
306 }
307
308 let imports = db.file_imports(file);
309 if let Some(fqcn) = imports.get(name) {
310 return fqcn.clone();
311 }
312 if let Some((_, fqcn)) = imports
313 .iter()
314 .find(|(alias, _)| alias.eq_ignore_ascii_case(name))
315 {
316 return fqcn.clone();
317 }
318 if let Some(ns) = db.file_namespace(file) {
319 return format!("{}\\{}", ns, name);
320 }
321 name.to_string()
322}
323
324pub fn class_template_params_via_db(
329 db: &dyn MirDatabase,
330 fqcn: &str,
331) -> Option<Arc<[TemplateParam]>> {
332 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
333 Some(node.template_params(db))
334}
335
336pub fn inherited_template_bindings_via_db(
343 db: &dyn MirDatabase,
344 fqcn: &str,
345) -> std::collections::HashMap<Arc<str>, Union> {
346 let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
347 let mut visited: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
348 let mut current: Arc<str> = Arc::from(fqcn);
349 loop {
350 if !visited.insert(current.clone()) {
351 break;
352 }
353 let node = match db
354 .lookup_class_node(current.as_ref())
355 .filter(|n| n.active(db))
356 {
357 Some(n) => n,
358 None => break,
359 };
360 let parent = match node.parent(db) {
361 Some(p) => p,
362 None => break,
363 };
364 let extends_type_args = node.extends_type_args(db);
365 if !extends_type_args.is_empty() {
366 if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
367 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
368 bindings
369 .entry(tp.name.clone())
370 .or_insert_with(|| ty.clone());
371 }
372 }
373 }
374 current = parent;
375 }
376 bindings
377}
378
379#[salsa::input]
394pub struct FunctionNode {
395 pub fqn: Arc<str>,
396 pub short_name: Arc<str>,
397 pub active: bool,
398 pub params: Arc<[FnParam]>,
399 pub return_type: Option<Arc<Union>>,
400 pub inferred_return_type: Option<Arc<Union>>,
401 pub template_params: Arc<[TemplateParam]>,
402 pub assertions: Arc<[Assertion]>,
403 pub throws: Arc<[Arc<str>]>,
404 pub deprecated: Option<Arc<str>>,
405 pub is_pure: bool,
406 pub location: Option<Location>,
409}
410
411#[salsa::input]
426pub struct MethodNode {
427 pub fqcn: Arc<str>,
428 pub name: Arc<str>,
429 pub active: bool,
430 pub params: Arc<[FnParam]>,
431 pub return_type: Option<Arc<Union>>,
432 pub inferred_return_type: Option<Arc<Union>>,
433 pub template_params: Arc<[TemplateParam]>,
434 pub assertions: Arc<[Assertion]>,
435 pub throws: Arc<[Arc<str>]>,
436 pub deprecated: Option<Arc<str>>,
437 pub is_internal: bool,
438 pub visibility: Visibility,
439 pub is_static: bool,
440 pub is_abstract: bool,
441 pub is_final: bool,
442 pub is_constructor: bool,
443 pub is_pure: bool,
444 pub location: Option<Location>,
447}
448
449#[salsa::input]
458pub struct PropertyNode {
459 pub fqcn: Arc<str>,
460 pub name: Arc<str>,
461 pub active: bool,
462 pub ty: Option<Union>,
463 pub visibility: Visibility,
464 pub is_static: bool,
465 pub is_readonly: bool,
466 pub location: Option<Location>,
467}
468
469#[salsa::input]
475pub struct ClassConstantNode {
476 pub fqcn: Arc<str>,
477 pub name: Arc<str>,
478 pub active: bool,
479 pub ty: Union,
480 pub visibility: Option<Visibility>,
481 pub is_final: bool,
482 pub location: Option<Location>,
486}
487
488#[salsa::input]
493pub struct GlobalConstantNode {
494 pub fqn: Arc<str>,
495 pub active: bool,
496 pub ty: Union,
497}
498
499#[derive(Clone, Debug, Default)]
506pub struct Ancestors(pub Vec<Arc<str>>);
507
508impl PartialEq for Ancestors {
509 fn eq(&self, other: &Self) -> bool {
510 self.0.len() == other.0.len()
511 && self
512 .0
513 .iter()
514 .zip(&other.0)
515 .all(|(a, b)| a.as_ref() == b.as_ref())
516 }
517}
518
519unsafe impl salsa::Update for Ancestors {
520 unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
521 let old = unsafe { &mut *old_ptr };
522 if *old == new_val {
523 return false;
524 }
525 *old = new_val;
526 true
527 }
528}
529
530fn ancestors_initial(_db: &dyn MirDatabase, _id: salsa::Id, _node: ClassNode) -> Ancestors {
533 Ancestors(vec![])
534}
535
536fn ancestors_cycle(
537 _db: &dyn MirDatabase,
538 _cycle: &salsa::Cycle,
539 _last: &Ancestors,
540 _value: Ancestors,
541 _node: ClassNode,
542) -> Ancestors {
543 Ancestors(vec![])
546}
547
548#[salsa::tracked(cycle_fn = ancestors_cycle, cycle_initial = ancestors_initial)]
558pub fn class_ancestors(db: &dyn MirDatabase, node: ClassNode) -> Ancestors {
559 if !node.active(db) {
560 return Ancestors(vec![]);
561 }
562 if node.is_enum(db) || node.is_trait(db) {
572 return Ancestors(vec![]);
573 }
574
575 let mut all: Vec<Arc<str>> = Vec::new();
576 let mut seen: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
577
578 let add =
579 |fqcn: &Arc<str>, all: &mut Vec<Arc<str>>, seen: &mut rustc_hash::FxHashSet<Arc<str>>| {
580 if seen.insert(fqcn.clone()) {
581 all.push(fqcn.clone());
582 }
583 };
584
585 if node.is_interface(db) {
586 for e in node.extends(db).iter() {
587 add(e, &mut all, &mut seen);
588 if let Some(parent_node) = db.lookup_class_node(e) {
589 for a in class_ancestors(db, parent_node).0 {
590 add(&a, &mut all, &mut seen);
591 }
592 }
593 }
594 } else {
595 if let Some(ref p) = node.parent(db) {
596 add(p, &mut all, &mut seen);
597 if let Some(parent_node) = db.lookup_class_node(p) {
598 for a in class_ancestors(db, parent_node).0 {
599 add(&a, &mut all, &mut seen);
600 }
601 }
602 }
603 for iface in node.interfaces(db).iter() {
604 add(iface, &mut all, &mut seen);
605 if let Some(iface_node) = db.lookup_class_node(iface) {
606 for a in class_ancestors(db, iface_node).0 {
607 add(&a, &mut all, &mut seen);
608 }
609 }
610 }
611 for t in node.traits(db).iter() {
612 add(t, &mut all, &mut seen);
613 }
614 }
615
616 Ancestors(all)
617}
618
619pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
627 let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
628 return false;
629 };
630 class_ancestors(db, node)
631 .0
632 .iter()
633 .any(|ancestor| !type_exists_via_db(db, ancestor))
634}
635
636pub fn method_is_concretely_implemented(
647 db: &dyn MirDatabase,
648 fqcn: &str,
649 method_name: &str,
650) -> bool {
651 let lower = method_name.to_lowercase();
652 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
653 return false;
654 };
655 if self_node.is_interface(db) {
658 return false;
659 }
660 if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
662 if !m.is_abstract(db) {
663 return true;
664 }
665 }
666 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
668 for t in self_node.traits(db).iter() {
669 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
670 return true;
671 }
672 }
673 for ancestor in class_ancestors(db, self_node).0.iter() {
676 let Some(anc_node) = db
677 .lookup_class_node(ancestor.as_ref())
678 .filter(|n| n.active(db))
679 else {
680 continue;
681 };
682 if anc_node.is_interface(db) {
683 continue;
684 }
685 if !anc_node.is_trait(db) {
687 if let Some(m) = db
688 .lookup_method_node(ancestor.as_ref(), &lower)
689 .filter(|m| m.active(db))
690 {
691 if !m.is_abstract(db) {
692 return true;
693 }
694 }
695 }
696 if anc_node.is_trait(db) {
699 if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
700 return true;
701 }
702 } else {
703 for t in anc_node.traits(db).iter() {
704 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
705 return true;
706 }
707 }
708 }
709 }
710 false
711}
712
713fn trait_provides_method(
717 db: &dyn MirDatabase,
718 trait_fqcn: &str,
719 method_lower: &str,
720 visited: &mut rustc_hash::FxHashSet<String>,
721) -> bool {
722 if !visited.insert(trait_fqcn.to_string()) {
723 return false;
724 }
725 if let Some(m) = db
726 .lookup_method_node(trait_fqcn, method_lower)
727 .filter(|m| m.active(db))
728 {
729 if !m.is_abstract(db) {
730 return true;
731 }
732 }
733 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
734 return false;
735 };
736 if !node.is_trait(db) {
737 return false;
738 }
739 for t in node.traits(db).iter() {
740 if trait_provides_method(db, t.as_ref(), method_lower, visited) {
741 return true;
742 }
743 }
744 false
745}
746
747pub fn lookup_method_in_chain(
762 db: &dyn MirDatabase,
763 fqcn: &str,
764 method_name: &str,
765) -> Option<MethodNode> {
766 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
767 lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
768}
769
770fn lookup_method_in_chain_inner(
771 db: &dyn MirDatabase,
772 fqcn: &str,
773 lower: &str,
774 visited_mixins: &mut rustc_hash::FxHashSet<String>,
775) -> Option<MethodNode> {
776 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
777
778 if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
780 return Some(node);
781 }
782 for m in self_node.mixins(db).iter() {
786 if visited_mixins.insert(m.to_string()) {
787 if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
788 {
789 return Some(node);
790 }
791 }
792 }
793 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
796 for t in self_node.traits(db).iter() {
797 if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
798 return Some(node);
799 }
800 }
801 for ancestor in class_ancestors(db, self_node).0.iter() {
803 if let Some(node) = db
804 .lookup_method_node(ancestor.as_ref(), lower)
805 .filter(|n| n.active(db))
806 {
807 return Some(node);
808 }
809 if let Some(anc_node) = db
810 .lookup_class_node(ancestor.as_ref())
811 .filter(|n| n.active(db))
812 {
813 if anc_node.is_trait(db) {
814 if let Some(node) =
815 trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
816 {
817 return Some(node);
818 }
819 } else {
820 for t in anc_node.traits(db).iter() {
821 if let Some(node) =
822 trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
823 {
824 return Some(node);
825 }
826 }
827 for m in anc_node.mixins(db).iter() {
828 if visited_mixins.insert(m.to_string()) {
829 if let Some(node) =
830 lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
831 {
832 return Some(node);
833 }
834 }
835 }
836 }
837 }
838 }
839 None
840}
841
842fn trait_provides_method_node(
846 db: &dyn MirDatabase,
847 trait_fqcn: &str,
848 method_lower: &str,
849 visited: &mut rustc_hash::FxHashSet<String>,
850) -> Option<MethodNode> {
851 if !visited.insert(trait_fqcn.to_string()) {
852 return None;
853 }
854 if let Some(node) = db
855 .lookup_method_node(trait_fqcn, method_lower)
856 .filter(|n| n.active(db))
857 {
858 return Some(node);
859 }
860 let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
861 if !node.is_trait(db) {
862 return None;
863 }
864 for t in node.traits(db).iter() {
865 if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
866 return Some(found);
867 }
868 }
869 None
870}
871
872pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
873 let lower = method_name.to_lowercase();
874 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
875 return false;
876 };
877 if db
879 .lookup_method_node(fqcn, &lower)
880 .is_some_and(|m| m.active(db))
881 {
882 return true;
883 }
884 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
886 for t in self_node.traits(db).iter() {
887 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
888 return true;
889 }
890 }
891 for ancestor in class_ancestors(db, self_node).0.iter() {
893 if db
894 .lookup_method_node(ancestor.as_ref(), &lower)
895 .is_some_and(|m| m.active(db))
896 {
897 return true;
898 }
899 if let Some(anc_node) = db
900 .lookup_class_node(ancestor.as_ref())
901 .filter(|n| n.active(db))
902 {
903 if anc_node.is_trait(db) {
904 if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
905 return true;
906 }
907 } else {
908 for t in anc_node.traits(db).iter() {
909 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
910 return true;
911 }
912 }
913 }
914 }
915 }
916 false
917}
918
919fn trait_declares_method(
923 db: &dyn MirDatabase,
924 trait_fqcn: &str,
925 method_lower: &str,
926 visited: &mut rustc_hash::FxHashSet<String>,
927) -> bool {
928 if !visited.insert(trait_fqcn.to_string()) {
929 return false;
930 }
931 if db
932 .lookup_method_node(trait_fqcn, method_lower)
933 .is_some_and(|m| m.active(db))
934 {
935 return true;
936 }
937 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
938 return false;
939 };
940 if !node.is_trait(db) {
941 return false;
942 }
943 for t in node.traits(db).iter() {
944 if trait_declares_method(db, t.as_ref(), method_lower, visited) {
945 return true;
946 }
947 }
948 false
949}
950
951pub fn lookup_property_in_chain(
961 db: &dyn MirDatabase,
962 fqcn: &str,
963 prop_name: &str,
964) -> Option<PropertyNode> {
965 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
966 lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
967}
968
969fn lookup_property_in_chain_inner(
970 db: &dyn MirDatabase,
971 fqcn: &str,
972 prop_name: &str,
973 visited_mixins: &mut rustc_hash::FxHashSet<String>,
974) -> Option<PropertyNode> {
975 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
976
977 if let Some(node) = db
979 .lookup_property_node(fqcn, prop_name)
980 .filter(|n| n.active(db))
981 {
982 return Some(node);
983 }
984 for m in self_node.mixins(db).iter() {
987 if visited_mixins.insert(m.to_string()) {
988 if let Some(node) =
989 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
990 {
991 return Some(node);
992 }
993 }
994 }
995 for ancestor in class_ancestors(db, self_node).0.iter() {
999 if let Some(node) = db
1000 .lookup_property_node(ancestor.as_ref(), prop_name)
1001 .filter(|n| n.active(db))
1002 {
1003 return Some(node);
1004 }
1005 if let Some(anc_node) = db
1006 .lookup_class_node(ancestor.as_ref())
1007 .filter(|n| n.active(db))
1008 {
1009 for m in anc_node.mixins(db).iter() {
1010 if visited_mixins.insert(m.to_string()) {
1011 if let Some(node) =
1012 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
1013 {
1014 return Some(node);
1015 }
1016 }
1017 }
1018 }
1019 }
1020 None
1021}
1022
1023pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
1033 if db
1034 .lookup_class_constant_node(fqcn, const_name)
1035 .is_some_and(|n| n.active(db))
1036 {
1037 return true;
1038 }
1039 let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
1040 return false;
1041 };
1042 for ancestor in class_ancestors(db, class_node).0.iter() {
1043 if db
1044 .lookup_class_constant_node(ancestor.as_ref(), const_name)
1045 .is_some_and(|n| n.active(db))
1046 {
1047 return true;
1048 }
1049 }
1050 false
1051}
1052
1053pub fn member_location_via_db(
1062 db: &dyn MirDatabase,
1063 fqcn: &str,
1064 member_name: &str,
1065) -> Option<Location> {
1066 if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
1067 if let Some(loc) = node.location(db) {
1068 return Some(loc);
1069 }
1070 }
1071 if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
1072 if let Some(loc) = node.location(db) {
1073 return Some(loc);
1074 }
1075 }
1076 if let Some(node) = db
1078 .lookup_class_constant_node(fqcn, member_name)
1079 .filter(|n| n.active(db))
1080 {
1081 if let Some(loc) = node.location(db) {
1082 return Some(loc);
1083 }
1084 }
1085 let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
1086 for ancestor in class_ancestors(db, class_node).0.iter() {
1087 if let Some(node) = db
1088 .lookup_class_constant_node(ancestor.as_ref(), member_name)
1089 .filter(|n| n.active(db))
1090 {
1091 if let Some(loc) = node.location(db) {
1092 return Some(loc);
1093 }
1094 }
1095 }
1096 None
1097}
1098
1099pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
1112 if child == ancestor {
1113 return true;
1114 }
1115 let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
1116 return false;
1117 };
1118 if node.is_enum(db) {
1119 if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
1123 return true;
1124 }
1125 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
1126 return true;
1127 }
1128 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
1129 return true;
1130 }
1131 return false;
1132 }
1133 class_ancestors(db, node)
1134 .0
1135 .iter()
1136 .any(|p| p.as_ref() == ancestor)
1137}
1138
1139pub fn collect_file_definitions_uncached(
1146 db: &dyn MirDatabase,
1147 file: SourceFile,
1148) -> FileDefinitions {
1149 let path = file.path(db);
1150 let text = file.text(db);
1151
1152 let arena = bumpalo::Bump::new();
1153 let parsed = php_rs_parser::parse(&arena, &text);
1154
1155 let mut all_issues: Vec<Issue> = parsed
1156 .errors
1157 .iter()
1158 .map(|err| {
1159 Issue::new(
1160 mir_issues::IssueKind::ParseError {
1161 message: err.to_string(),
1162 },
1163 mir_issues::Location {
1164 file: path.clone(),
1165 line: 1,
1166 line_end: 1,
1167 col_start: 0,
1168 col_end: 0,
1169 },
1170 )
1171 })
1172 .collect();
1173
1174 let collector =
1175 crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
1176 let (slice, collector_issues) = collector.collect_slice(&parsed.program);
1177 all_issues.extend(collector_issues);
1178
1179 FileDefinitions {
1180 slice: Arc::new(slice),
1181 issues: Arc::new(all_issues),
1182 }
1183}
1184
1185#[salsa::tracked]
1186pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
1187 collect_file_definitions_uncached(db, file)
1188}
1189
1190type MemberRegistry<V> = Arc<FxHashMap<Arc<str>, FxHashMap<Arc<str>, V>>>;
1200type ReferenceLocations =
1201 Arc<std::sync::Mutex<FxHashMap<Arc<str>, Vec<(Arc<str>, u32, u16, u16)>>>>;
1202
1203#[salsa::db]
1204#[derive(Default, Clone)]
1205pub struct MirDb {
1206 storage: salsa::Storage<Self>,
1207 class_nodes: Arc<FxHashMap<Arc<str>, ClassNode>>,
1215 class_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
1219 function_nodes: Arc<FxHashMap<Arc<str>, FunctionNode>>,
1222 function_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
1226 method_nodes: MemberRegistry<MethodNode>,
1228 property_nodes: MemberRegistry<PropertyNode>,
1230 class_constant_nodes: MemberRegistry<ClassConstantNode>,
1232 global_constant_nodes: Arc<FxHashMap<Arc<str>, GlobalConstantNode>>,
1234 file_namespaces: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1236 file_imports: Arc<FxHashMap<Arc<str>, HashMap<String, String>>>,
1238 global_vars: Arc<FxHashMap<Arc<str>, Union>>,
1240 symbol_to_file: Arc<FxHashMap<Arc<str>, Arc<str>>>,
1242 reference_locations: ReferenceLocations,
1244}
1245
1246#[salsa::db]
1247impl salsa::Database for MirDb {}
1248
1249#[salsa::db]
1250impl MirDatabase for MirDb {
1251 fn php_version_str(&self) -> Arc<str> {
1252 Arc::from("8.2")
1253 }
1254
1255 fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
1256 if let Some(&node) = self.class_nodes.get(fqcn) {
1257 return Some(node);
1258 }
1259 let lower = fqcn.to_ascii_lowercase();
1260 let canonical = self.class_node_keys_lower.get(&lower)?;
1261 self.class_nodes.get(canonical.as_ref()).copied()
1262 }
1263
1264 fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
1265 if let Some(&node) = self.function_nodes.get(fqn) {
1266 return Some(node);
1267 }
1268 let lower = fqn.to_ascii_lowercase();
1269 let canonical = self.function_node_keys_lower.get(&lower)?;
1270 self.function_nodes.get(canonical.as_ref()).copied()
1271 }
1272
1273 fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
1274 self.method_nodes
1275 .get(fqcn)
1276 .and_then(|m| m.get(method_name_lower).copied())
1277 }
1278
1279 fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
1280 self.property_nodes
1281 .get(fqcn)
1282 .and_then(|m| m.get(prop_name).copied())
1283 }
1284
1285 fn lookup_class_constant_node(
1286 &self,
1287 fqcn: &str,
1288 const_name: &str,
1289 ) -> Option<ClassConstantNode> {
1290 self.class_constant_nodes
1291 .get(fqcn)
1292 .and_then(|m| m.get(const_name).copied())
1293 }
1294
1295 fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
1296 self.global_constant_nodes.get(fqn).copied()
1297 }
1298
1299 fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
1300 self.method_nodes
1301 .get(fqcn)
1302 .map(|m| m.values().copied().collect())
1303 .unwrap_or_default()
1304 }
1305
1306 fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
1307 self.property_nodes
1308 .get(fqcn)
1309 .map(|m| m.values().copied().collect())
1310 .unwrap_or_default()
1311 }
1312
1313 fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
1314 self.class_nodes
1315 .iter()
1316 .filter_map(|(fqcn, node)| {
1317 if node.active(self) {
1318 Some(fqcn.clone())
1319 } else {
1320 None
1321 }
1322 })
1323 .collect()
1324 }
1325
1326 fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
1327 self.function_nodes
1328 .iter()
1329 .filter_map(|(fqn, node)| {
1330 if node.active(self) {
1331 Some(fqn.clone())
1332 } else {
1333 None
1334 }
1335 })
1336 .collect()
1337 }
1338
1339 fn file_namespace(&self, file: &str) -> Option<Arc<str>> {
1340 self.file_namespaces.get(file).cloned()
1341 }
1342
1343 fn file_imports(&self, file: &str) -> HashMap<String, String> {
1344 self.file_imports.get(file).cloned().unwrap_or_default()
1345 }
1346
1347 fn global_var_type(&self, name: &str) -> Option<Union> {
1348 self.global_vars.get(name).cloned()
1349 }
1350
1351 fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)> {
1352 self.file_imports
1353 .iter()
1354 .map(|(file, imports)| (file.clone(), imports.clone()))
1355 .collect()
1356 }
1357
1358 fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>> {
1359 self.symbol_to_file.get(symbol).cloned()
1360 }
1361
1362 fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>> {
1363 self.symbol_to_file
1364 .iter()
1365 .filter_map(|(sym, defining_file)| {
1366 if defining_file.as_ref() == file {
1367 Some(sym.clone())
1368 } else {
1369 None
1370 }
1371 })
1372 .collect()
1373 }
1374
1375 fn record_reference_location(&self, loc: RefLoc) {
1376 let mut refs = self
1377 .reference_locations
1378 .lock()
1379 .expect("reference lock poisoned");
1380 let entry = refs.entry(loc.symbol_key).or_default();
1381 let tuple = (loc.file, loc.line, loc.col_start, loc.col_end);
1382 if !entry.iter().any(|existing| existing == &tuple) {
1383 entry.push(tuple);
1384 }
1385 }
1386
1387 fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
1388 for (symbol, line, col_start, col_end) in locs {
1389 self.record_reference_location(RefLoc {
1390 symbol_key: Arc::from(symbol.as_str()),
1391 file: file.clone(),
1392 line: *line,
1393 col_start: *col_start,
1394 col_end: *col_end,
1395 });
1396 }
1397 }
1398
1399 fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1400 let refs = self
1401 .reference_locations
1402 .lock()
1403 .expect("reference lock poisoned");
1404 let mut out = Vec::new();
1405 for (symbol, locs) in refs.iter() {
1406 for (loc_file, line, col_start, col_end) in locs {
1407 if loc_file.as_ref() == file {
1408 out.push((symbol.clone(), *line, *col_start, *col_end));
1409 }
1410 }
1411 }
1412 out
1413 }
1414
1415 fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
1416 let refs = self
1417 .reference_locations
1418 .lock()
1419 .expect("reference lock poisoned");
1420 refs.get(symbol).cloned().unwrap_or_default()
1421 }
1422
1423 fn has_reference(&self, symbol: &str) -> bool {
1424 let refs = self
1425 .reference_locations
1426 .lock()
1427 .expect("reference lock poisoned");
1428 refs.get(symbol).is_some_and(|locs| !locs.is_empty())
1429 }
1430
1431 fn clear_file_references(&self, file: &str) {
1432 let mut refs = self
1433 .reference_locations
1434 .lock()
1435 .expect("reference lock poisoned");
1436 for locs in refs.values_mut() {
1437 locs.retain(|(loc_file, _, _, _)| loc_file.as_ref() != file);
1438 }
1439 }
1440}
1441
1442#[derive(Debug, Clone, Default)]
1450pub struct ClassNodeFields {
1451 pub fqcn: Arc<str>,
1452 pub is_interface: bool,
1453 pub is_trait: bool,
1454 pub is_enum: bool,
1455 pub is_abstract: bool,
1456 pub parent: Option<Arc<str>>,
1457 pub interfaces: Arc<[Arc<str>]>,
1458 pub traits: Arc<[Arc<str>]>,
1459 pub extends: Arc<[Arc<str>]>,
1460 pub template_params: Arc<[TemplateParam]>,
1461 pub require_extends: Arc<[Arc<str>]>,
1462 pub require_implements: Arc<[Arc<str>]>,
1463 pub is_backed_enum: bool,
1464 pub mixins: Arc<[Arc<str>]>,
1465 pub deprecated: Option<Arc<str>>,
1466 pub enum_scalar_type: Option<Union>,
1467 pub is_final: bool,
1468 pub is_readonly: bool,
1469 pub location: Option<Location>,
1470 pub extends_type_args: Arc<[Union]>,
1471 pub implements_type_args: ImplementsTypeArgs,
1472}
1473
1474impl ClassNodeFields {
1475 pub fn for_class(fqcn: Arc<str>) -> Self {
1476 Self {
1477 fqcn,
1478 ..Self::default()
1479 }
1480 }
1481
1482 pub fn for_interface(fqcn: Arc<str>) -> Self {
1483 Self {
1484 fqcn,
1485 is_interface: true,
1486 ..Self::default()
1487 }
1488 }
1489
1490 pub fn for_trait(fqcn: Arc<str>) -> Self {
1491 Self {
1492 fqcn,
1493 is_trait: true,
1494 ..Self::default()
1495 }
1496 }
1497
1498 pub fn for_enum(fqcn: Arc<str>) -> Self {
1499 Self {
1500 fqcn,
1501 is_enum: true,
1502 ..Self::default()
1503 }
1504 }
1505}
1506
1507impl MirDb {
1508 pub fn remove_file_definitions(&mut self, file: &str) {
1509 let symbols = self.symbols_defined_in_file(file);
1510 for symbol in &symbols {
1511 self.deactivate_class_node(symbol);
1512 self.deactivate_function_node(symbol);
1513 self.deactivate_class_methods(symbol);
1514 self.deactivate_class_properties(symbol);
1515 self.deactivate_class_constants(symbol);
1516 self.deactivate_global_constant_node(symbol);
1517 }
1518 let symbol_set: HashSet<Arc<str>> = symbols.into_iter().collect();
1519 Arc::make_mut(&mut self.symbol_to_file).retain(|sym, defining_file| {
1520 defining_file.as_ref() != file && !symbol_set.contains(sym)
1521 });
1522 Arc::make_mut(&mut self.file_namespaces).retain(|path, _| path.as_ref() != file);
1523 Arc::make_mut(&mut self.file_imports).retain(|path, _| path.as_ref() != file);
1524 Arc::make_mut(&mut self.global_vars).retain(|name, _| !symbol_set.contains(name));
1525 self.clear_file_references(file);
1526 }
1527
1528 pub fn type_count(&self) -> usize {
1529 self.class_nodes
1530 .values()
1531 .filter(|node| node.active(self))
1532 .count()
1533 }
1534
1535 pub fn function_count(&self) -> usize {
1536 self.function_nodes
1537 .values()
1538 .filter(|node| node.active(self))
1539 .count()
1540 }
1541
1542 pub fn constant_count(&self) -> usize {
1543 self.global_constant_nodes
1544 .values()
1545 .filter(|node| node.active(self))
1546 .count()
1547 }
1548
1549 pub fn ingest_stub_slice(&mut self, slice: &StubSlice) {
1555 use std::collections::HashSet;
1556
1557 let mut slice = slice.clone();
1560 mir_codebase::storage::deduplicate_params_in_slice(&mut slice);
1561
1562 if let Some(file) = &slice.file {
1563 if let Some(namespace) = &slice.namespace {
1564 Arc::make_mut(&mut self.file_namespaces).insert(file.clone(), namespace.clone());
1565 }
1566 if !slice.imports.is_empty() {
1567 Arc::make_mut(&mut self.file_imports).insert(file.clone(), slice.imports.clone());
1568 }
1569 for (name, _) in &slice.global_vars {
1570 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1571 Arc::make_mut(&mut self.symbol_to_file)
1572 .insert(Arc::from(global_name), file.clone());
1573 }
1574 }
1575 for (name, ty) in &slice.global_vars {
1576 let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
1577 Arc::make_mut(&mut self.global_vars).insert(Arc::from(global_name), ty.clone());
1578 }
1579
1580 for cls in &slice.classes {
1581 if let Some(file) = &slice.file {
1582 Arc::make_mut(&mut self.symbol_to_file).insert(cls.fqcn.clone(), file.clone());
1583 }
1584 self.upsert_class_node(ClassNodeFields {
1585 is_abstract: cls.is_abstract,
1586 parent: cls.parent.clone(),
1587 interfaces: Arc::from(cls.interfaces.as_ref()),
1588 traits: Arc::from(cls.traits.as_ref()),
1589 template_params: Arc::from(cls.template_params.as_ref()),
1590 mixins: Arc::from(cls.mixins.as_ref()),
1591 deprecated: cls.deprecated.clone(),
1592 is_final: cls.is_final,
1593 is_readonly: cls.is_readonly,
1594 location: cls.location.clone(),
1595 extends_type_args: Arc::from(cls.extends_type_args.as_ref()),
1596 implements_type_args: Arc::from(
1597 cls.implements_type_args
1598 .iter()
1599 .map(|(iface, args)| (iface.clone(), Arc::from(args.as_ref())))
1600 .collect::<Vec<_>>(),
1601 ),
1602 ..ClassNodeFields::for_class(cls.fqcn.clone())
1603 });
1604 if self.method_nodes.contains_key(cls.fqcn.as_ref()) {
1605 let method_keep: HashSet<&str> =
1606 cls.own_methods.keys().map(|m| m.as_ref()).collect();
1607 self.prune_class_methods(&cls.fqcn, &method_keep);
1608 }
1609 for method in cls.own_methods.values() {
1610 self.upsert_method_node(method.as_ref());
1615 }
1616 if self.property_nodes.contains_key(cls.fqcn.as_ref()) {
1617 let prop_keep: HashSet<&str> =
1618 cls.own_properties.keys().map(|p| p.as_ref()).collect();
1619 self.prune_class_properties(&cls.fqcn, &prop_keep);
1620 }
1621 for prop in cls.own_properties.values() {
1622 self.upsert_property_node(&cls.fqcn, prop);
1623 }
1624 if self.class_constant_nodes.contains_key(cls.fqcn.as_ref()) {
1625 let const_keep: HashSet<&str> =
1626 cls.own_constants.keys().map(|c| c.as_ref()).collect();
1627 self.prune_class_constants(&cls.fqcn, &const_keep);
1628 }
1629 for constant in cls.own_constants.values() {
1630 self.upsert_class_constant_node(&cls.fqcn, constant);
1631 }
1632 }
1633
1634 for iface in &slice.interfaces {
1635 if let Some(file) = &slice.file {
1636 Arc::make_mut(&mut self.symbol_to_file).insert(iface.fqcn.clone(), file.clone());
1637 }
1638 self.upsert_class_node(ClassNodeFields {
1639 extends: Arc::from(iface.extends.as_ref()),
1640 template_params: Arc::from(iface.template_params.as_ref()),
1641 location: iface.location.clone(),
1642 ..ClassNodeFields::for_interface(iface.fqcn.clone())
1643 });
1644 if self.method_nodes.contains_key(iface.fqcn.as_ref()) {
1645 let method_keep: HashSet<&str> =
1646 iface.own_methods.keys().map(|m| m.as_ref()).collect();
1647 self.prune_class_methods(&iface.fqcn, &method_keep);
1648 }
1649 for method in iface.own_methods.values() {
1650 self.upsert_method_node(method.as_ref());
1651 }
1652 if self.class_constant_nodes.contains_key(iface.fqcn.as_ref()) {
1653 let const_keep: HashSet<&str> =
1654 iface.own_constants.keys().map(|c| c.as_ref()).collect();
1655 self.prune_class_constants(&iface.fqcn, &const_keep);
1656 }
1657 for constant in iface.own_constants.values() {
1658 self.upsert_class_constant_node(&iface.fqcn, constant);
1659 }
1660 }
1661
1662 for tr in &slice.traits {
1663 if let Some(file) = &slice.file {
1664 Arc::make_mut(&mut self.symbol_to_file).insert(tr.fqcn.clone(), file.clone());
1665 }
1666 self.upsert_class_node(ClassNodeFields {
1667 traits: Arc::from(tr.traits.as_ref()),
1668 template_params: Arc::from(tr.template_params.as_ref()),
1669 require_extends: Arc::from(tr.require_extends.as_ref()),
1670 require_implements: Arc::from(tr.require_implements.as_ref()),
1671 location: tr.location.clone(),
1672 ..ClassNodeFields::for_trait(tr.fqcn.clone())
1673 });
1674 if self.method_nodes.contains_key(tr.fqcn.as_ref()) {
1675 let method_keep: HashSet<&str> =
1676 tr.own_methods.keys().map(|m| m.as_ref()).collect();
1677 self.prune_class_methods(&tr.fqcn, &method_keep);
1678 }
1679 for method in tr.own_methods.values() {
1680 self.upsert_method_node(method.as_ref());
1681 }
1682 if self.property_nodes.contains_key(tr.fqcn.as_ref()) {
1683 let prop_keep: HashSet<&str> =
1684 tr.own_properties.keys().map(|p| p.as_ref()).collect();
1685 self.prune_class_properties(&tr.fqcn, &prop_keep);
1686 }
1687 for prop in tr.own_properties.values() {
1688 self.upsert_property_node(&tr.fqcn, prop);
1689 }
1690 if self.class_constant_nodes.contains_key(tr.fqcn.as_ref()) {
1691 let const_keep: HashSet<&str> =
1692 tr.own_constants.keys().map(|c| c.as_ref()).collect();
1693 self.prune_class_constants(&tr.fqcn, &const_keep);
1694 }
1695 for constant in tr.own_constants.values() {
1696 self.upsert_class_constant_node(&tr.fqcn, constant);
1697 }
1698 }
1699
1700 for en in &slice.enums {
1701 if let Some(file) = &slice.file {
1702 Arc::make_mut(&mut self.symbol_to_file).insert(en.fqcn.clone(), file.clone());
1703 }
1704 self.upsert_class_node(ClassNodeFields {
1705 interfaces: Arc::from(en.interfaces.as_ref()),
1706 is_backed_enum: en.scalar_type.is_some(),
1707 enum_scalar_type: en.scalar_type.clone(),
1708 location: en.location.clone(),
1709 ..ClassNodeFields::for_enum(en.fqcn.clone())
1710 });
1711 if self.method_nodes.contains_key(en.fqcn.as_ref()) {
1712 let mut method_keep: HashSet<&str> =
1713 en.own_methods.keys().map(|m| m.as_ref()).collect();
1714 method_keep.insert("cases");
1715 if en.scalar_type.is_some() {
1716 method_keep.insert("from");
1717 method_keep.insert("tryfrom");
1718 }
1719 self.prune_class_methods(&en.fqcn, &method_keep);
1720 }
1721 for method in en.own_methods.values() {
1722 self.upsert_method_node(method.as_ref());
1723 }
1724 let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
1725 fqcn: en.fqcn.clone(),
1726 name: Arc::from(name),
1727 params: Arc::from([].as_ref()),
1728 return_type: Some(Arc::new(Union::mixed())),
1729 inferred_return_type: None,
1730 visibility: Visibility::Public,
1731 is_static: true,
1732 is_abstract: false,
1733 is_constructor: false,
1734 template_params: vec![],
1735 assertions: vec![],
1736 throws: vec![],
1737 is_final: false,
1738 is_internal: false,
1739 is_pure: false,
1740 deprecated: None,
1741 location: None,
1742 };
1743 let already = |name: &str| {
1744 en.own_methods
1745 .keys()
1746 .any(|k| k.as_ref().eq_ignore_ascii_case(name))
1747 };
1748 if !already("cases") {
1749 self.upsert_method_node(&synth_method("cases"));
1750 }
1751 if en.scalar_type.is_some() {
1752 if !already("from") {
1753 self.upsert_method_node(&synth_method("from"));
1754 }
1755 if !already("tryFrom") {
1756 self.upsert_method_node(&synth_method("tryFrom"));
1757 }
1758 }
1759 if self.class_constant_nodes.contains_key(en.fqcn.as_ref()) {
1760 let mut const_keep: HashSet<&str> =
1761 en.own_constants.keys().map(|c| c.as_ref()).collect();
1762 for case in en.cases.values() {
1763 const_keep.insert(case.name.as_ref());
1764 }
1765 self.prune_class_constants(&en.fqcn, &const_keep);
1766 }
1767 for constant in en.own_constants.values() {
1768 self.upsert_class_constant_node(&en.fqcn, constant);
1769 }
1770 for case in en.cases.values() {
1771 let case_const = ConstantStorage {
1772 name: case.name.clone(),
1773 ty: mir_types::Union::mixed(),
1774 visibility: None,
1775 is_final: false,
1776 location: case.location.clone(),
1777 };
1778 self.upsert_class_constant_node(&en.fqcn, &case_const);
1779 }
1780 }
1781
1782 for func in &slice.functions {
1783 if let Some(file) = &slice.file {
1784 Arc::make_mut(&mut self.symbol_to_file).insert(func.fqn.clone(), file.clone());
1785 }
1786 self.upsert_function_node(func);
1787 }
1788 for (fqn, ty) in &slice.constants {
1789 self.upsert_global_constant_node(fqn.clone(), ty.clone());
1790 }
1791 }
1792
1793 #[allow(clippy::too_many_arguments)]
1798 pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
1799 use salsa::Setter as _;
1800 let ClassNodeFields {
1801 fqcn,
1802 is_interface,
1803 is_trait,
1804 is_enum,
1805 is_abstract,
1806 parent,
1807 interfaces,
1808 traits,
1809 extends,
1810 template_params,
1811 require_extends,
1812 require_implements,
1813 is_backed_enum,
1814 mixins,
1815 deprecated,
1816 enum_scalar_type,
1817 is_final,
1818 is_readonly,
1819 location,
1820 extends_type_args,
1821 implements_type_args,
1822 } = fields;
1823 if let Some(&node) = self.class_nodes.get(&fqcn) {
1824 if node.active(self)
1837 && node.is_interface(self) == is_interface
1838 && node.is_trait(self) == is_trait
1839 && node.is_enum(self) == is_enum
1840 && node.is_abstract(self) == is_abstract
1841 && node.is_backed_enum(self) == is_backed_enum
1842 && node.parent(self) == parent
1843 && *node.interfaces(self) == *interfaces
1844 && *node.traits(self) == *traits
1845 && *node.extends(self) == *extends
1846 && *node.template_params(self) == *template_params
1847 && *node.require_extends(self) == *require_extends
1848 && *node.require_implements(self) == *require_implements
1849 && *node.mixins(self) == *mixins
1850 && node.deprecated(self) == deprecated
1851 && node.enum_scalar_type(self) == enum_scalar_type
1852 && node.is_final(self) == is_final
1853 && node.is_readonly(self) == is_readonly
1854 && node.location(self) == location
1855 && *node.extends_type_args(self) == *extends_type_args
1856 && *node.implements_type_args(self) == *implements_type_args
1857 {
1858 return node;
1859 }
1860 node.set_active(self).to(true);
1861 node.set_is_interface(self).to(is_interface);
1862 node.set_is_trait(self).to(is_trait);
1863 node.set_is_enum(self).to(is_enum);
1864 node.set_is_abstract(self).to(is_abstract);
1865 node.set_parent(self).to(parent);
1866 node.set_interfaces(self).to(interfaces);
1867 node.set_traits(self).to(traits);
1868 node.set_extends(self).to(extends);
1869 node.set_template_params(self).to(template_params);
1870 node.set_require_extends(self).to(require_extends);
1871 node.set_require_implements(self).to(require_implements);
1872 node.set_is_backed_enum(self).to(is_backed_enum);
1873 node.set_mixins(self).to(mixins);
1874 node.set_deprecated(self).to(deprecated);
1875 node.set_enum_scalar_type(self).to(enum_scalar_type);
1876 node.set_is_final(self).to(is_final);
1877 node.set_is_readonly(self).to(is_readonly);
1878 node.set_location(self).to(location);
1879 node.set_extends_type_args(self).to(extends_type_args);
1880 node.set_implements_type_args(self).to(implements_type_args);
1881 node
1882 } else {
1883 let node = ClassNode::new(
1884 self,
1885 fqcn.clone(),
1886 true,
1887 is_interface,
1888 is_trait,
1889 is_enum,
1890 is_abstract,
1891 parent,
1892 interfaces,
1893 traits,
1894 extends,
1895 template_params,
1896 require_extends,
1897 require_implements,
1898 is_backed_enum,
1899 mixins,
1900 deprecated,
1901 enum_scalar_type,
1902 is_final,
1903 is_readonly,
1904 location,
1905 extends_type_args,
1906 implements_type_args,
1907 );
1908 Arc::make_mut(&mut self.class_node_keys_lower)
1909 .insert(fqcn.to_ascii_lowercase(), fqcn.clone());
1910 Arc::make_mut(&mut self.class_nodes).insert(fqcn, node);
1911 node
1912 }
1913 }
1914
1915 pub fn deactivate_class_node(&mut self, fqcn: &str) {
1920 use salsa::Setter as _;
1921 if let Some(&node) = self.class_nodes.get(fqcn) {
1922 node.set_active(self).to(false);
1923 }
1924 }
1925
1926 pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
1928 use salsa::Setter as _;
1929 let fqn = &storage.fqn;
1930 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1931 if node.active(self)
1937 && node.short_name(self) == storage.short_name
1938 && node.is_pure(self) == storage.is_pure
1939 && node.deprecated(self) == storage.deprecated
1940 && node.return_type(self).as_deref() == storage.return_type.as_deref()
1941 && node.location(self) == storage.location
1942 && *node.params(self) == *storage.params.as_ref()
1943 && *node.template_params(self) == *storage.template_params
1944 && *node.assertions(self) == *storage.assertions
1945 && *node.throws(self) == *storage.throws
1946 {
1947 return node;
1948 }
1949 node.set_active(self).to(true);
1950 node.set_short_name(self).to(storage.short_name.clone());
1951 node.set_params(self).to(storage.params.clone());
1952 node.set_return_type(self).to(storage.return_type.clone());
1953 node.set_template_params(self)
1954 .to(Arc::from(storage.template_params.as_slice()));
1955 node.set_assertions(self)
1956 .to(Arc::from(storage.assertions.as_slice()));
1957 node.set_throws(self)
1958 .to(Arc::from(storage.throws.as_slice()));
1959 node.set_deprecated(self).to(storage.deprecated.clone());
1960 node.set_is_pure(self).to(storage.is_pure);
1961 node.set_location(self).to(storage.location.clone());
1962 node
1963 } else {
1964 let node = FunctionNode::new(
1965 self,
1966 fqn.clone(),
1967 storage.short_name.clone(),
1968 true,
1969 storage.params.clone(),
1970 storage.return_type.clone(),
1971 storage
1972 .inferred_return_type
1973 .as_ref()
1974 .map(|t| Arc::new(t.clone())),
1975 Arc::from(storage.template_params.as_slice()),
1976 Arc::from(storage.assertions.as_slice()),
1977 Arc::from(storage.throws.as_slice()),
1978 storage.deprecated.clone(),
1979 storage.is_pure,
1980 storage.location.clone(),
1981 );
1982 Arc::make_mut(&mut self.function_node_keys_lower)
1983 .insert(fqn.to_ascii_lowercase(), fqn.clone());
1984 Arc::make_mut(&mut self.function_nodes).insert(fqn.clone(), node);
1985 node
1986 }
1987 }
1988
1989 pub fn commit_inferred_return_types(
2003 &mut self,
2004 functions: Vec<(Arc<str>, mir_types::Union)>,
2005 methods: Vec<(Arc<str>, Arc<str>, mir_types::Union)>,
2006 ) {
2007 use salsa::Setter as _;
2008 for (fqn, inferred) in functions {
2009 if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
2010 if !node.active(self) {
2011 continue;
2012 }
2013 let new = Some(Arc::new(inferred));
2014 if node.inferred_return_type(self) == new {
2015 continue;
2016 }
2017 node.set_inferred_return_type(self).to(new);
2018 }
2019 }
2020 for (fqcn, name, inferred) in methods {
2021 let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
2022 name.clone()
2023 } else {
2024 Arc::from(name.to_lowercase().as_str())
2025 };
2026 let node = self
2027 .method_nodes
2028 .get(fqcn.as_ref())
2029 .and_then(|m| m.get(&name_lower))
2030 .copied();
2031 if let Some(node) = node {
2032 if !node.active(self) {
2033 continue;
2034 }
2035 let new = Some(Arc::new(inferred));
2036 if node.inferred_return_type(self) == new {
2037 continue;
2038 }
2039 node.set_inferred_return_type(self).to(new);
2040 }
2041 }
2042 }
2043
2044 pub fn deactivate_function_node(&mut self, fqn: &str) {
2046 use salsa::Setter as _;
2047 if let Some(&node) = self.function_nodes.get(fqn) {
2048 node.set_active(self).to(false);
2049 }
2050 }
2051
2052 pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
2054 use salsa::Setter as _;
2055 let fqcn = &storage.fqcn;
2056 let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
2057 let existing = self
2060 .method_nodes
2061 .get(fqcn.as_ref())
2062 .and_then(|m| m.get(&name_lower))
2063 .copied();
2064 if let Some(node) = existing {
2065 if node.active(self)
2069 && node.visibility(self) == storage.visibility
2070 && node.is_static(self) == storage.is_static
2071 && node.is_abstract(self) == storage.is_abstract
2072 && node.is_final(self) == storage.is_final
2073 && node.is_constructor(self) == storage.is_constructor
2074 && node.is_pure(self) == storage.is_pure
2075 && node.is_internal(self) == storage.is_internal
2076 && node.deprecated(self) == storage.deprecated
2077 && node.return_type(self).as_deref() == storage.return_type.as_deref()
2078 && node.location(self) == storage.location
2079 && *node.params(self) == *storage.params.as_ref()
2080 && *node.template_params(self) == *storage.template_params
2081 && *node.assertions(self) == *storage.assertions
2082 && *node.throws(self) == *storage.throws
2083 {
2084 return node;
2085 }
2086 node.set_active(self).to(true);
2087 node.set_params(self).to(storage.params.clone());
2088 node.set_return_type(self).to(storage.return_type.clone());
2089 node.set_template_params(self)
2090 .to(Arc::from(storage.template_params.as_slice()));
2091 node.set_assertions(self)
2092 .to(Arc::from(storage.assertions.as_slice()));
2093 node.set_throws(self)
2094 .to(Arc::from(storage.throws.as_slice()));
2095 node.set_deprecated(self).to(storage.deprecated.clone());
2096 node.set_is_internal(self).to(storage.is_internal);
2097 node.set_visibility(self).to(storage.visibility);
2098 node.set_is_static(self).to(storage.is_static);
2099 node.set_is_abstract(self).to(storage.is_abstract);
2100 node.set_is_final(self).to(storage.is_final);
2101 node.set_is_constructor(self).to(storage.is_constructor);
2102 node.set_is_pure(self).to(storage.is_pure);
2103 node.set_location(self).to(storage.location.clone());
2104 node
2105 } else {
2106 let node = MethodNode::new(
2108 self,
2109 fqcn.clone(),
2110 storage.name.clone(),
2111 true,
2112 storage.params.clone(),
2113 storage.return_type.clone(),
2114 storage
2115 .inferred_return_type
2116 .as_ref()
2117 .map(|t| Arc::new(t.clone())),
2118 Arc::from(storage.template_params.as_slice()),
2119 Arc::from(storage.assertions.as_slice()),
2120 Arc::from(storage.throws.as_slice()),
2121 storage.deprecated.clone(),
2122 storage.is_internal,
2123 storage.visibility,
2124 storage.is_static,
2125 storage.is_abstract,
2126 storage.is_final,
2127 storage.is_constructor,
2128 storage.is_pure,
2129 storage.location.clone(),
2130 );
2131 Arc::make_mut(&mut self.method_nodes)
2132 .entry(fqcn.clone())
2133 .or_default()
2134 .insert(name_lower, node);
2135 node
2136 }
2137 }
2138
2139 pub fn deactivate_class_methods(&mut self, fqcn: &str) {
2141 use salsa::Setter as _;
2142 let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
2143 Some(methods) => methods.values().copied().collect(),
2144 None => return,
2145 };
2146 for node in nodes {
2147 node.set_active(self).to(false);
2148 }
2149 }
2150
2151 pub fn prune_class_methods<T>(&mut self, fqcn: &str, keep_lower: &std::collections::HashSet<T>)
2157 where
2158 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2159 {
2160 use salsa::Setter as _;
2161 let candidates: Vec<MethodNode> = self
2162 .method_nodes
2163 .get(fqcn)
2164 .map(|m| {
2165 m.iter()
2166 .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
2167 .map(|(_, n)| *n)
2168 .collect()
2169 })
2170 .unwrap_or_default();
2171 for node in candidates {
2172 if node.active(self) {
2173 node.set_active(self).to(false);
2174 }
2175 }
2176 }
2177
2178 pub fn prune_class_properties<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2180 where
2181 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2182 {
2183 use salsa::Setter as _;
2184 let candidates: Vec<PropertyNode> = self
2185 .property_nodes
2186 .get(fqcn)
2187 .map(|m| {
2188 m.iter()
2189 .filter(|(k, _)| !keep.contains(k.as_ref()))
2190 .map(|(_, n)| *n)
2191 .collect()
2192 })
2193 .unwrap_or_default();
2194 for node in candidates {
2195 if node.active(self) {
2196 node.set_active(self).to(false);
2197 }
2198 }
2199 }
2200
2201 pub fn prune_class_constants<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
2203 where
2204 T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
2205 {
2206 use salsa::Setter as _;
2207 let candidates: Vec<ClassConstantNode> = self
2208 .class_constant_nodes
2209 .get(fqcn)
2210 .map(|m| {
2211 m.iter()
2212 .filter(|(k, _)| !keep.contains(k.as_ref()))
2213 .map(|(_, n)| *n)
2214 .collect()
2215 })
2216 .unwrap_or_default();
2217 for node in candidates {
2218 if node.active(self) {
2219 node.set_active(self).to(false);
2220 }
2221 }
2222 }
2223
2224 pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
2226 use salsa::Setter as _;
2227 let existing = self
2228 .property_nodes
2229 .get(fqcn.as_ref())
2230 .and_then(|m| m.get(storage.name.as_ref()))
2231 .copied();
2232 if let Some(node) = existing {
2233 if node.active(self)
2235 && node.visibility(self) == storage.visibility
2236 && node.is_static(self) == storage.is_static
2237 && node.is_readonly(self) == storage.is_readonly
2238 && node.ty(self) == storage.ty
2239 && node.location(self) == storage.location
2240 {
2241 return;
2242 }
2243 node.set_active(self).to(true);
2244 node.set_ty(self).to(storage.ty.clone());
2245 node.set_visibility(self).to(storage.visibility);
2246 node.set_is_static(self).to(storage.is_static);
2247 node.set_is_readonly(self).to(storage.is_readonly);
2248 node.set_location(self).to(storage.location.clone());
2249 } else {
2250 let node = PropertyNode::new(
2251 self,
2252 fqcn.clone(),
2253 storage.name.clone(),
2254 true,
2255 storage.ty.clone(),
2256 storage.visibility,
2257 storage.is_static,
2258 storage.is_readonly,
2259 storage.location.clone(),
2260 );
2261 Arc::make_mut(&mut self.property_nodes)
2262 .entry(fqcn.clone())
2263 .or_default()
2264 .insert(storage.name.clone(), node);
2265 }
2266 }
2267
2268 pub fn deactivate_class_properties(&mut self, fqcn: &str) {
2270 use salsa::Setter as _;
2271 let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
2272 Some(props) => props.values().copied().collect(),
2273 None => return,
2274 };
2275 for node in nodes {
2276 node.set_active(self).to(false);
2277 }
2278 }
2279
2280 pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
2282 use salsa::Setter as _;
2283 let existing = self
2284 .class_constant_nodes
2285 .get(fqcn.as_ref())
2286 .and_then(|m| m.get(storage.name.as_ref()))
2287 .copied();
2288 if let Some(node) = existing {
2289 if node.active(self)
2291 && node.visibility(self) == storage.visibility
2292 && node.is_final(self) == storage.is_final
2293 && node.ty(self) == storage.ty
2294 && node.location(self) == storage.location
2295 {
2296 return;
2297 }
2298 node.set_active(self).to(true);
2299 node.set_ty(self).to(storage.ty.clone());
2300 node.set_visibility(self).to(storage.visibility);
2301 node.set_is_final(self).to(storage.is_final);
2302 node.set_location(self).to(storage.location.clone());
2303 } else {
2304 let node = ClassConstantNode::new(
2305 self,
2306 fqcn.clone(),
2307 storage.name.clone(),
2308 true,
2309 storage.ty.clone(),
2310 storage.visibility,
2311 storage.is_final,
2312 storage.location.clone(),
2313 );
2314 Arc::make_mut(&mut self.class_constant_nodes)
2315 .entry(fqcn.clone())
2316 .or_default()
2317 .insert(storage.name.clone(), node);
2318 }
2319 }
2320
2321 pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
2323 use salsa::Setter as _;
2324 if let Some(&node) = self.global_constant_nodes.get(&fqn) {
2325 if node.active(self) && node.ty(self) == ty {
2327 return node;
2328 }
2329 node.set_active(self).to(true);
2330 node.set_ty(self).to(ty);
2331 node
2332 } else {
2333 let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
2334 Arc::make_mut(&mut self.global_constant_nodes).insert(fqn, node);
2335 node
2336 }
2337 }
2338
2339 pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
2341 use salsa::Setter as _;
2342 if let Some(&node) = self.global_constant_nodes.get(fqn) {
2343 node.set_active(self).to(false);
2344 }
2345 }
2346
2347 pub fn deactivate_class_constants(&mut self, fqcn: &str) {
2349 use salsa::Setter as _;
2350 let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
2351 Some(consts) => consts.values().copied().collect(),
2352 None => return,
2353 };
2354 for node in nodes {
2355 node.set_active(self).to(false);
2356 }
2357 }
2358}
2359
2360#[salsa::accumulator]
2384#[derive(Clone, Debug)]
2385pub struct IssueAccumulator(pub Issue);
2386
2387#[derive(Clone, Debug, PartialEq, Eq)]
2396pub struct RefLoc {
2397 pub symbol_key: Arc<str>,
2398 pub file: Arc<str>,
2399 pub line: u32,
2400 pub col_start: u16,
2401 pub col_end: u16,
2402}
2403
2404#[salsa::accumulator]
2411#[derive(Clone, Debug)]
2412pub struct RefLocAccumulator(pub RefLoc);
2413
2414#[salsa::input]
2419pub struct AnalyzeFileInput {
2420 pub php_version: Arc<str>,
2423}
2424
2425#[salsa::tracked]
2448pub fn inferred_function_return_type(db: &dyn MirDatabase, node: FunctionNode) -> Arc<Union> {
2449 node.inferred_return_type(db)
2452 .unwrap_or_else(|| Arc::new(Union::mixed()))
2453}
2454
2455#[salsa::tracked]
2463pub fn inferred_method_return_type(db: &dyn MirDatabase, node: MethodNode) -> Arc<Union> {
2464 node.inferred_return_type(db)
2466 .unwrap_or_else(|| Arc::new(Union::mixed()))
2467}
2468
2469#[allow(dead_code)]
2475pub(crate) fn collect_accumulated_issues(
2476 db: &dyn MirDatabase,
2477 files: &[(Arc<str>, SourceFile)],
2478 php_version: &str,
2479) -> Vec<Issue> {
2480 let mut all_issues = Vec::new();
2481 let input = AnalyzeFileInput::new(db, Arc::from(php_version));
2482
2483 for (_path, file) in files {
2484 analyze_file(db, *file, input);
2486
2487 let accumulated: Vec<&IssueAccumulator> = analyze_file::accumulated(db, *file, input);
2489 for acc in accumulated {
2490 all_issues.push(acc.0.clone());
2491 }
2492 }
2493
2494 all_issues
2495}
2496
2497#[salsa::tracked]
2508pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, input: AnalyzeFileInput) {
2509 use salsa::Accumulator as _;
2510 let path = file.path(db);
2511 let text = file.text(db);
2512
2513 let arena = bumpalo::Bump::new();
2514 let parsed = php_rs_parser::parse(&arena, &text);
2515
2516 for err in &parsed.errors {
2518 let issue = Issue::new(
2519 mir_issues::IssueKind::ParseError {
2520 message: err.to_string(),
2521 },
2522 mir_issues::Location {
2523 file: path.clone(),
2524 line: 1,
2525 line_end: 1,
2526 col_start: 0,
2527 col_end: 0,
2528 },
2529 );
2530 IssueAccumulator(issue).accumulate(db);
2531 }
2532
2533 if parsed.errors.is_empty() {
2535 use std::str::FromStr as _;
2536 let php_version =
2537 PhpVersion::from_str(input.php_version(db).as_ref()).unwrap_or(PhpVersion::LATEST);
2538 let driver = Pass2Driver::new(db, php_version);
2539 let (issues, _symbols) = driver.analyze_bodies(
2540 &parsed.program,
2541 path.clone(),
2542 text.as_ref(),
2543 &parsed.source_map,
2544 );
2545
2546 for issue in issues {
2548 IssueAccumulator(issue).accumulate(db);
2549 }
2550
2551 let ref_locs = db.extract_file_reference_locations(&path);
2553 for (symbol_key, line, col_start, col_end) in ref_locs {
2554 let ref_loc = RefLoc {
2555 symbol_key,
2556 file: path.clone(),
2557 line,
2558 col_start,
2559 col_end,
2560 };
2561 RefLocAccumulator(ref_loc).accumulate(db);
2562 }
2563 }
2564}
2565
2566#[cfg(test)]
2569mod tests {
2570 use super::*;
2571 use salsa::Setter as _;
2572
2573 fn upsert_class(
2574 db: &mut MirDb,
2575 fqcn: &str,
2576 parent: Option<Arc<str>>,
2577 extends: Arc<[Arc<str>]>,
2578 is_interface: bool,
2579 ) -> ClassNode {
2580 db.upsert_class_node(ClassNodeFields {
2581 is_interface,
2582 parent,
2583 extends,
2584 ..ClassNodeFields::for_class(Arc::from(fqcn))
2585 })
2586 }
2587
2588 #[test]
2589 fn mirdb_constructs() {
2590 let _db = MirDb::default();
2591 }
2592
2593 #[test]
2594 fn source_file_input_roundtrip() {
2595 let db = MirDb::default();
2596 let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
2597 assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
2598 assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
2599 }
2600
2601 #[test]
2602 fn collect_file_definitions_basic() {
2603 let db = MirDb::default();
2604 let src = Arc::from("<?php class Foo {}");
2605 let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
2606 let defs = collect_file_definitions(&db, file);
2607 assert!(defs.issues.is_empty());
2608 assert_eq!(defs.slice.classes.len(), 1);
2609 assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
2610 }
2611
2612 #[test]
2613 fn collect_file_definitions_memoized() {
2614 let db = MirDb::default();
2615 let file = SourceFile::new(
2616 &db,
2617 Arc::from("/tmp/memo.php"),
2618 Arc::from("<?php class Bar {}"),
2619 );
2620
2621 let defs1 = collect_file_definitions(&db, file);
2622 let defs2 = collect_file_definitions(&db, file);
2623 assert!(
2624 Arc::ptr_eq(&defs1.slice, &defs2.slice),
2625 "unchanged file must return the memoized result"
2626 );
2627 }
2628
2629 #[test]
2630 fn analyze_file_accumulates_parse_errors() {
2631 let db = MirDb::default();
2632 let file = SourceFile::new(
2634 &db,
2635 Arc::from("/tmp/parse_err.php"),
2636 Arc::from("<?php $x = \"unterminated"),
2637 );
2638 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2639 analyze_file(&db, file, input);
2640 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2641 assert!(
2642 !issues.is_empty(),
2643 "expected parse error to surface as accumulated IssueAccumulator"
2644 );
2645 assert!(matches!(
2646 issues[0].0.kind,
2647 mir_issues::IssueKind::ParseError { .. }
2648 ));
2649 }
2650
2651 #[test]
2652 fn analyze_file_clean_input_accumulates_nothing() {
2653 let db = MirDb::default();
2654 let file = SourceFile::new(
2655 &db,
2656 Arc::from("/tmp/clean.php"),
2657 Arc::from("<?php class Foo {}"),
2658 );
2659 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2660 analyze_file(&db, file, input);
2661 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2662 let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
2663 assert!(issues.is_empty());
2664 assert!(refs.is_empty());
2665 }
2666
2667 #[test]
2668 fn analyze_file_calls_pass2_for_undefined_class() {
2669 let mut db = MirDb::default();
2670 for slice in crate::stubs::builtin_stub_slices_for_version(crate::PhpVersion::LATEST) {
2672 db.ingest_stub_slice(&slice);
2673 }
2674
2675 let file = SourceFile::new(
2676 &db,
2677 Arc::from("/tmp/test_pass2.php"),
2678 Arc::from("<?php function foo() { new UndefinedClass(); }"),
2679 );
2680 let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2681 analyze_file(&db, file, input);
2682 let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2683
2684 assert!(
2685 !issues.is_empty(),
2686 "Pass2Driver should emit UndefinedClass issue"
2687 );
2688 assert!(issues
2689 .iter()
2690 .any(|acc| matches!(acc.0.kind, mir_issues::IssueKind::UndefinedClass { .. })));
2691 }
2692
2693 #[test]
2694 fn inferred_function_return_type_query_defined() {
2695 let mut db = MirDb::default();
2696
2697 let func_storage = FunctionStorage {
2699 fqn: Arc::from("test_fn"),
2700 short_name: Arc::from("test_fn"),
2701 params: Arc::from([]),
2702 return_type: None,
2703 inferred_return_type: Some(Union::int()),
2704 template_params: Vec::new(),
2705 assertions: Vec::new(),
2706 throws: Vec::new(),
2707 deprecated: None,
2708 is_pure: false,
2709 location: None,
2710 };
2711 let node = db.upsert_function_node(&func_storage);
2712
2713 let inferred = inferred_function_return_type(&db, node);
2715 assert_eq!(inferred.as_ref(), &Union::int());
2716 }
2717
2718 #[test]
2719 fn inferred_method_return_type_query_defined() {
2720 let mut db = MirDb::default();
2721
2722 let method_storage = MethodStorage {
2724 fqcn: Arc::from("TestClass"),
2725 name: Arc::from("testMethod"),
2726 params: Arc::from([]),
2727 return_type: None,
2728 inferred_return_type: Some(Union::string()),
2729 template_params: Vec::new(),
2730 assertions: Vec::new(),
2731 throws: Vec::new(),
2732 deprecated: None,
2733 visibility: Visibility::Public,
2734 is_static: false,
2735 is_abstract: false,
2736 is_final: false,
2737 is_constructor: false,
2738 is_pure: false,
2739 is_internal: false,
2740 location: None,
2741 };
2742 let node = db.upsert_method_node(&method_storage);
2743
2744 let inferred = inferred_method_return_type(&db, node);
2746 assert_eq!(inferred.as_ref(), &Union::string());
2747 }
2748
2749 #[test]
2750 fn collect_file_definitions_recomputes_on_change() {
2751 let mut db = MirDb::default();
2752 let file = SourceFile::new(
2753 &db,
2754 Arc::from("/tmp/memo2.php"),
2755 Arc::from("<?php class Foo {}"),
2756 );
2757
2758 let defs1 = collect_file_definitions(&db, file);
2759 file.set_text(&mut db)
2760 .to(Arc::from("<?php class Foo {} class Bar {}"));
2761 let defs2 = collect_file_definitions(&db, file);
2762
2763 assert!(
2764 !Arc::ptr_eq(&defs1.slice, &defs2.slice),
2765 "changed file must produce a new result"
2766 );
2767 assert_eq!(defs2.slice.classes.len(), 2);
2768 }
2769
2770 #[test]
2771 fn class_ancestors_empty_for_root_class() {
2772 let mut db = MirDb::default();
2773 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2774 let ancestors = class_ancestors(&db, node);
2775 assert!(ancestors.0.is_empty(), "root class has no ancestors");
2776 }
2777
2778 #[test]
2779 fn class_ancestors_single_parent() {
2780 let mut db = MirDb::default();
2781 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2782 let child = upsert_class(
2783 &mut db,
2784 "Child",
2785 Some(Arc::from("Base")),
2786 Arc::from([]),
2787 false,
2788 );
2789 let ancestors = class_ancestors(&db, child);
2790 assert_eq!(ancestors.0.len(), 1);
2791 assert_eq!(ancestors.0[0].as_ref(), "Base");
2792 }
2793
2794 #[test]
2795 fn class_ancestors_transitive() {
2796 let mut db = MirDb::default();
2797 upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
2798 upsert_class(
2799 &mut db,
2800 "Parent",
2801 Some(Arc::from("GrandParent")),
2802 Arc::from([]),
2803 false,
2804 );
2805 let child = upsert_class(
2806 &mut db,
2807 "Child",
2808 Some(Arc::from("Parent")),
2809 Arc::from([]),
2810 false,
2811 );
2812 let ancestors = class_ancestors(&db, child);
2813 assert_eq!(ancestors.0.len(), 2);
2814 assert_eq!(ancestors.0[0].as_ref(), "Parent");
2815 assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
2816 }
2817
2818 #[test]
2819 fn class_ancestors_cycle_returns_empty() {
2820 let mut db = MirDb::default();
2821 let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
2823 let ancestors = class_ancestors(&db, node_a);
2824 assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
2826 }
2827
2828 #[test]
2829 fn class_ancestors_inactive_node_returns_empty() {
2830 let mut db = MirDb::default();
2831 let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2832 db.deactivate_class_node("Foo");
2833 let ancestors = class_ancestors(&db, node);
2834 assert!(ancestors.0.is_empty(), "inactive node must yield empty");
2835 }
2836
2837 #[test]
2838 fn class_ancestors_recomputes_on_parent_change() {
2839 let mut db = MirDb::default();
2840 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2841 let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
2842
2843 let before = class_ancestors(&db, child);
2844 assert!(before.0.is_empty());
2845
2846 child.set_parent(&mut db).to(Some(Arc::from("Base")));
2848
2849 let after = class_ancestors(&db, child);
2850 assert_eq!(after.0.len(), 1);
2851 assert_eq!(after.0[0].as_ref(), "Base");
2852 }
2853
2854 #[test]
2855 fn interface_ancestors_via_extends() {
2856 let mut db = MirDb::default();
2857 upsert_class(&mut db, "Countable", None, Arc::from([]), true);
2858 let child_iface = upsert_class(
2859 &mut db,
2860 "Collection",
2861 None,
2862 Arc::from([Arc::from("Countable")]),
2863 true,
2864 );
2865 let ancestors = class_ancestors(&db, child_iface);
2866 assert_eq!(ancestors.0.len(), 1);
2867 assert_eq!(ancestors.0[0].as_ref(), "Countable");
2868 }
2869
2870 #[test]
2871 fn type_exists_via_db_tracks_active_state() {
2872 let mut db = MirDb::default();
2873 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2874 assert!(type_exists_via_db(&db, "Foo"));
2875 assert!(!type_exists_via_db(&db, "Bar"));
2876 db.deactivate_class_node("Foo");
2877 assert!(!type_exists_via_db(&db, "Foo"));
2878 }
2879
2880 #[test]
2881 fn clone_preserves_class_node_lookups() {
2882 let mut db = MirDb::default();
2885 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2886 let cloned = db.clone();
2887 assert!(
2888 type_exists_via_db(&cloned, "Foo"),
2889 "clone must observe nodes registered before clone()"
2890 );
2891 assert!(
2892 !type_exists_via_db(&cloned, "Bar"),
2893 "clone must not observe nodes that were never registered"
2894 );
2895 let foo_node = cloned.lookup_class_node("Foo").expect("registered");
2897 let ancestors = class_ancestors(&cloned, foo_node);
2898 assert!(ancestors.0.is_empty(), "Foo has no ancestors");
2899 }
2900
2901 fn upsert_class_with_traits(
2906 db: &mut MirDb,
2907 fqcn: &str,
2908 parent: Option<Arc<str>>,
2909 traits: &[&str],
2910 is_interface: bool,
2911 is_trait: bool,
2912 ) -> ClassNode {
2913 db.upsert_class_node(ClassNodeFields {
2914 is_interface,
2915 is_trait,
2916 parent,
2917 traits: Arc::from(
2918 traits
2919 .iter()
2920 .map(|t| Arc::<str>::from(*t))
2921 .collect::<Vec<_>>(),
2922 ),
2923 ..ClassNodeFields::for_class(Arc::from(fqcn))
2924 })
2925 }
2926
2927 fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
2928 let storage = MethodStorage {
2929 name: Arc::from(name),
2930 fqcn: Arc::from(fqcn),
2931 params: Arc::from([].as_slice()),
2932 return_type: None,
2933 inferred_return_type: None,
2934 visibility: Visibility::Public,
2935 is_static: false,
2936 is_abstract,
2937 is_final: false,
2938 is_constructor: name == "__construct",
2939 template_params: vec![],
2940 assertions: vec![],
2941 throws: vec![],
2942 deprecated: None,
2943 is_internal: false,
2944 is_pure: false,
2945 location: None,
2946 };
2947 db.upsert_method_node(&storage)
2948 }
2949
2950 fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
2951 db.upsert_class_node(ClassNodeFields {
2952 interfaces: Arc::from(
2953 interfaces
2954 .iter()
2955 .map(|i| Arc::<str>::from(*i))
2956 .collect::<Vec<_>>(),
2957 ),
2958 is_backed_enum: is_backed,
2959 ..ClassNodeFields::for_enum(Arc::from(fqcn))
2960 })
2961 }
2962
2963 #[test]
2968 fn method_exists_via_db_finds_own_method() {
2969 let mut db = MirDb::default();
2970 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2971 upsert_method(&mut db, "Foo", "bar", false);
2972 assert!(method_exists_via_db(&db, "Foo", "bar"));
2973 assert!(!method_exists_via_db(&db, "Foo", "missing"));
2974 }
2975
2976 #[test]
2977 fn method_exists_via_db_walks_parent() {
2978 let mut db = MirDb::default();
2979 upsert_class(&mut db, "Base", None, Arc::from([]), false);
2980 upsert_method(&mut db, "Base", "inherited", false);
2981 upsert_class(
2982 &mut db,
2983 "Child",
2984 Some(Arc::from("Base")),
2985 Arc::from([]),
2986 false,
2987 );
2988 assert!(method_exists_via_db(&db, "Child", "inherited"));
2989 }
2990
2991 #[test]
2992 fn method_exists_via_db_walks_traits_transitively() {
2993 let mut db = MirDb::default();
2994 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2995 upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
2996 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2997 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2998 assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
2999 }
3000
3001 #[test]
3002 fn method_exists_via_db_is_case_insensitive() {
3003 let mut db = MirDb::default();
3004 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3005 upsert_method(&mut db, "Foo", "doStuff", false);
3006 assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
3008 assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
3009 }
3010
3011 #[test]
3012 fn method_exists_via_db_unknown_class_returns_false() {
3013 let db = MirDb::default();
3014 assert!(!method_exists_via_db(&db, "Nope", "anything"));
3015 }
3016
3017 #[test]
3018 fn method_exists_via_db_inactive_class_returns_false() {
3019 let mut db = MirDb::default();
3020 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3021 upsert_method(&mut db, "Foo", "bar", false);
3022 db.deactivate_class_node("Foo");
3023 assert!(!method_exists_via_db(&db, "Foo", "bar"));
3024 }
3025
3026 #[test]
3027 fn method_exists_via_db_finds_abstract_methods() {
3028 let mut db = MirDb::default();
3031 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3032 upsert_method(&mut db, "Foo", "abstr", true);
3033 assert!(method_exists_via_db(&db, "Foo", "abstr"));
3034 }
3035
3036 #[test]
3041 fn method_is_concretely_implemented_skips_abstract() {
3042 let mut db = MirDb::default();
3043 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3044 upsert_method(&mut db, "Foo", "abstr", true);
3045 assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
3046 }
3047
3048 #[test]
3049 fn method_is_concretely_implemented_finds_concrete_in_trait() {
3050 let mut db = MirDb::default();
3051 upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
3052 upsert_method(&mut db, "MyTrait", "provided", false);
3053 upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
3054 assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
3055 }
3056
3057 #[test]
3058 fn method_is_concretely_implemented_skips_interface_definitions() {
3059 let mut db = MirDb::default();
3062 upsert_class(&mut db, "I", None, Arc::from([]), true);
3063 upsert_method(&mut db, "I", "m", false);
3064 upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
3065 assert!(!method_is_concretely_implemented(&db, "C", "m"));
3067 }
3068
3069 #[test]
3074 fn extends_or_implements_via_db_self_match() {
3075 let mut db = MirDb::default();
3076 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3077 assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
3078 }
3079
3080 #[test]
3081 fn extends_or_implements_via_db_transitive() {
3082 let mut db = MirDb::default();
3083 upsert_class(&mut db, "Animal", None, Arc::from([]), false);
3084 upsert_class(
3085 &mut db,
3086 "Mammal",
3087 Some(Arc::from("Animal")),
3088 Arc::from([]),
3089 false,
3090 );
3091 upsert_class(
3092 &mut db,
3093 "Dog",
3094 Some(Arc::from("Mammal")),
3095 Arc::from([]),
3096 false,
3097 );
3098 assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
3099 assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
3100 assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
3101 }
3102
3103 #[test]
3104 fn extends_or_implements_via_db_unknown_returns_false() {
3105 let db = MirDb::default();
3106 assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
3107 }
3108
3109 #[test]
3110 fn extends_or_implements_via_db_unit_enum_implicit() {
3111 let mut db = MirDb::default();
3112 upsert_enum(&mut db, "Status", &[], false);
3113 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
3114 assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
3115 assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
3117 }
3118
3119 #[test]
3120 fn extends_or_implements_via_db_backed_enum_implicit() {
3121 let mut db = MirDb::default();
3122 upsert_enum(&mut db, "Status", &[], true);
3123 assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
3124 assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
3125 assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
3126 }
3127
3128 #[test]
3129 fn extends_or_implements_via_db_enum_declared_interface() {
3130 let mut db = MirDb::default();
3131 upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
3132 upsert_enum(&mut db, "Status", &["Stringable"], false);
3133 assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
3134 }
3135
3136 #[test]
3141 fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
3142 let mut db = MirDb::default();
3143 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3144 upsert_class(
3145 &mut db,
3146 "Child",
3147 Some(Arc::from("Base")),
3148 Arc::from([]),
3149 false,
3150 );
3151 assert!(!has_unknown_ancestor_via_db(&db, "Child"));
3152 }
3153
3154 #[test]
3155 fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
3156 let mut db = MirDb::default();
3157 upsert_class(
3159 &mut db,
3160 "Child",
3161 Some(Arc::from("Missing")),
3162 Arc::from([]),
3163 false,
3164 );
3165 assert!(has_unknown_ancestor_via_db(&db, "Child"));
3166 }
3167
3168 #[test]
3169 fn class_template_params_via_db_returns_registered_params() {
3170 use mir_types::Variance;
3171 let mut db = MirDb::default();
3172 let tp = TemplateParam {
3173 name: Arc::from("T"),
3174 bound: None,
3175 defining_entity: Arc::from("Box"),
3176 variance: Variance::Invariant,
3177 };
3178 db.upsert_class_node(ClassNodeFields {
3179 template_params: Arc::from([tp.clone()]),
3180 ..ClassNodeFields::for_class(Arc::from("Box"))
3181 });
3182 let got = class_template_params_via_db(&db, "Box").expect("registered");
3183 assert_eq!(got.len(), 1);
3184 assert_eq!(got[0].name.as_ref(), "T");
3185
3186 assert!(class_template_params_via_db(&db, "Missing").is_none());
3187 db.deactivate_class_node("Box");
3188 assert!(class_template_params_via_db(&db, "Box").is_none());
3189 }
3190
3191 fn upsert_class_with_mixins(
3196 db: &mut MirDb,
3197 fqcn: &str,
3198 parent: Option<Arc<str>>,
3199 mixins: &[&str],
3200 ) -> ClassNode {
3201 db.upsert_class_node(ClassNodeFields {
3202 parent,
3203 mixins: Arc::from(
3204 mixins
3205 .iter()
3206 .map(|m| Arc::<str>::from(*m))
3207 .collect::<Vec<_>>(),
3208 ),
3209 ..ClassNodeFields::for_class(Arc::from(fqcn))
3210 })
3211 }
3212
3213 #[test]
3214 fn lookup_method_in_chain_finds_own_then_ancestor() {
3215 let mut db = MirDb::default();
3216 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3217 upsert_method(&mut db, "Base", "shared", false);
3218 upsert_class(
3219 &mut db,
3220 "Child",
3221 Some(Arc::from("Base")),
3222 Arc::from([]),
3223 false,
3224 );
3225 upsert_method(&mut db, "Child", "shared", false);
3226 let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
3228 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3229 upsert_method(&mut db, "Base", "only_in_base", false);
3231 let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
3232 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3233 }
3234
3235 #[test]
3236 fn lookup_method_in_chain_walks_trait_of_traits() {
3237 let mut db = MirDb::default();
3238 upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
3239 upsert_method(&mut db, "InnerTrait", "deep", false);
3240 upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
3241 upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
3242 let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
3243 assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
3244 }
3245
3246 #[test]
3247 fn lookup_method_in_chain_walks_mixins() {
3248 let mut db = MirDb::default();
3249 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3250 upsert_method(&mut db, "MixinTarget", "magic", false);
3251 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3252 let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
3253 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3254 }
3255
3256 #[test]
3257 fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
3258 let mut db = MirDb::default();
3259 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3261 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3262 assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
3263 }
3264
3265 #[test]
3266 fn lookup_method_in_chain_is_case_insensitive() {
3267 let mut db = MirDb::default();
3268 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3269 upsert_method(&mut db, "Foo", "doStuff", false);
3270 assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
3271 assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
3272 }
3273
3274 #[test]
3275 fn lookup_method_in_chain_unknown_returns_none() {
3276 let db = MirDb::default();
3277 assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
3278 }
3279
3280 fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
3285 let storage = PropertyStorage {
3286 name: Arc::from(name),
3287 ty: None,
3288 inferred_ty: None,
3289 visibility: Visibility::Public,
3290 is_static: false,
3291 is_readonly,
3292 default: None,
3293 location: None,
3294 };
3295 let owner = Arc::<str>::from(fqcn);
3296 db.upsert_property_node(&owner, &storage);
3297 db.lookup_property_node(fqcn, name).expect("registered")
3298 }
3299
3300 #[test]
3301 fn lookup_property_in_chain_own_then_ancestor() {
3302 let mut db = MirDb::default();
3303 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3304 upsert_property(&mut db, "Base", "x", false);
3305 upsert_class(
3306 &mut db,
3307 "Child",
3308 Some(Arc::from("Base")),
3309 Arc::from([]),
3310 false,
3311 );
3312 let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
3314 assert_eq!(found.fqcn(&db).as_ref(), "Base");
3315 upsert_property(&mut db, "Child", "x", true);
3317 let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
3318 assert_eq!(found.fqcn(&db).as_ref(), "Child");
3319 assert!(found.is_readonly(&db));
3320 }
3321
3322 #[test]
3323 fn lookup_property_in_chain_walks_mixins() {
3324 let mut db = MirDb::default();
3325 upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
3326 upsert_property(&mut db, "MixinTarget", "exposed", false);
3327 upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
3328 let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
3329 assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
3330 }
3331
3332 #[test]
3333 fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
3334 let mut db = MirDb::default();
3335 upsert_class_with_mixins(&mut db, "A", None, &["B"]);
3336 upsert_class_with_mixins(&mut db, "B", None, &["A"]);
3337 assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
3338 }
3339
3340 #[test]
3341 fn lookup_property_in_chain_is_case_sensitive() {
3342 let mut db = MirDb::default();
3343 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3344 upsert_property(&mut db, "Foo", "myProp", false);
3345 assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
3346 assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
3348 }
3349
3350 #[test]
3351 fn lookup_property_in_chain_inactive_returns_none() {
3352 let mut db = MirDb::default();
3353 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3354 upsert_property(&mut db, "Foo", "x", false);
3355 db.deactivate_class_node("Foo");
3356 assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
3357 }
3358
3359 fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
3364 let storage = ConstantStorage {
3365 name: Arc::from(name),
3366 ty: mir_types::Union::mixed(),
3367 visibility: None,
3368 is_final: false,
3369 location: None,
3370 };
3371 let owner = Arc::<str>::from(fqcn);
3372 db.upsert_class_constant_node(&owner, &storage);
3373 }
3374
3375 #[test]
3376 fn class_constant_exists_in_chain_finds_own() {
3377 let mut db = MirDb::default();
3378 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3379 upsert_constant(&mut db, "Foo", "MAX");
3380 assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
3381 assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
3382 }
3383
3384 #[test]
3385 fn class_constant_exists_in_chain_walks_parent() {
3386 let mut db = MirDb::default();
3387 upsert_class(&mut db, "Base", None, Arc::from([]), false);
3388 upsert_constant(&mut db, "Base", "VERSION");
3389 upsert_class(
3390 &mut db,
3391 "Child",
3392 Some(Arc::from("Base")),
3393 Arc::from([]),
3394 false,
3395 );
3396 assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
3397 }
3398
3399 #[test]
3400 fn class_constant_exists_in_chain_walks_interface() {
3401 let mut db = MirDb::default();
3402 upsert_class(&mut db, "I", None, Arc::from([]), true);
3403 upsert_constant(&mut db, "I", "TYPE");
3404 db.upsert_class_node(ClassNodeFields {
3407 interfaces: Arc::from([Arc::from("I")]),
3408 ..ClassNodeFields::for_class(Arc::from("Impl"))
3409 });
3410 assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
3411 }
3412
3413 #[test]
3414 fn class_constant_exists_in_chain_walks_direct_trait() {
3415 let mut db = MirDb::default();
3416 upsert_class_with_traits(&mut db, "T", None, &[], false, true);
3417 upsert_constant(&mut db, "T", "FROM_TRAIT");
3418 upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
3419 assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
3420 }
3421
3422 #[test]
3423 fn class_constant_exists_in_chain_unknown_class_returns_false() {
3424 let db = MirDb::default();
3425 assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
3426 }
3427
3428 #[test]
3429 fn class_constant_exists_in_chain_inactive_returns_false() {
3430 let mut db = MirDb::default();
3431 upsert_class(&mut db, "Foo", None, Arc::from([]), false);
3432 upsert_constant(&mut db, "Foo", "X");
3433 db.deactivate_class_node("Foo");
3434 db.deactivate_class_constants("Foo");
3435 assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
3436 }
3437
3438 #[test]
3444 fn parallel_reads_then_serial_write_does_not_deadlock() {
3445 use rayon::prelude::*;
3446 use std::sync::mpsc;
3447 use std::time::Duration;
3448
3449 let (tx, rx) = mpsc::channel::<()>();
3450 std::thread::spawn(move || {
3451 let mut db = MirDb::default();
3452 let storage = mir_codebase::storage::FunctionStorage {
3453 fqn: Arc::from("foo"),
3454 short_name: Arc::from("foo"),
3455 params: Arc::from([].as_slice()),
3456 return_type: None,
3457 inferred_return_type: None,
3458 template_params: vec![],
3459 assertions: vec![],
3460 throws: vec![],
3461 deprecated: None,
3462 is_pure: false,
3463 location: None,
3464 };
3465 let node = db.upsert_function_node(&storage);
3466
3467 let db_for_sweep = db.clone();
3469 (0..256u32)
3470 .into_par_iter()
3471 .for_each_with(db_for_sweep, |db, _| {
3472 let _ = node.return_type(&*db as &dyn MirDatabase);
3473 });
3474
3475 node.set_return_type(&mut db)
3479 .to(Some(Arc::new(Union::mixed())));
3480 assert_eq!(node.return_type(&db), Some(Arc::new(Union::mixed())));
3481 tx.send(()).unwrap();
3482 });
3483
3484 match rx.recv_timeout(Duration::from_secs(30)) {
3485 Ok(()) => {}
3486 Err(_) => {
3487 panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
3488 }
3489 }
3490 }
3491
3492 #[test]
3503 fn sibling_clone_blocks_setter_until_dropped() {
3504 use std::sync::mpsc;
3505 use std::time::Duration;
3506
3507 let mut db = MirDb::default();
3508 let storage = mir_codebase::storage::FunctionStorage {
3509 fqn: Arc::from("foo"),
3510 short_name: Arc::from("foo"),
3511 params: Arc::from([].as_slice()),
3512 return_type: None,
3513 inferred_return_type: None,
3514 template_params: vec![],
3515 assertions: vec![],
3516 throws: vec![],
3517 deprecated: None,
3518 is_pure: false,
3519 location: None,
3520 };
3521 let node = db.upsert_function_node(&storage);
3522
3523 let sibling = db.clone();
3524
3525 let (tx, rx) = mpsc::channel::<()>();
3528 let writer = std::thread::spawn(move || {
3529 node.set_return_type(&mut db)
3530 .to(Some(Arc::new(Union::mixed())));
3531 tx.send(()).unwrap();
3532 });
3533
3534 match rx.recv_timeout(Duration::from_millis(500)) {
3537 Err(mpsc::RecvTimeoutError::Timeout) => { }
3538 Ok(()) => panic!(
3539 "setter completed while sibling clone was alive — strong-count==1 \
3540 invariant of `cancel_others` is broken; commit_inferred_return_types \
3541 cannot rely on tight-scoping clones"
3542 ),
3543 Err(e) => panic!("unexpected channel error: {e:?}"),
3544 }
3545
3546 drop(sibling);
3548
3549 match rx.recv_timeout(Duration::from_secs(5)) {
3550 Ok(()) => {}
3551 Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
3552 }
3553 writer.join().expect("writer thread panicked");
3554 }
3555}