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