1use std::sync::Arc;
2
3use mir_codebase::storage::{Location, TemplateParam};
4use mir_issues::Issue;
5use mir_types::Union;
6
7use super::*;
8
9#[derive(Debug, Clone, Copy)]
16pub struct ClassKind {
17 pub is_interface: bool,
18 pub is_trait: bool,
19 pub is_enum: bool,
20 pub is_abstract: bool,
21}
22
23pub fn class_kind_via_db(db: &dyn MirDatabase, fqcn: &str) -> Option<ClassKind> {
29 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
30 Some(ClassKind {
31 is_interface: node.is_interface(db),
32 is_trait: node.is_trait(db),
33 is_enum: node.is_enum(db),
34 is_abstract: node.is_abstract(db),
35 })
36}
37
38pub fn type_exists_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
39 db.lookup_class_node(fqcn).is_some_and(|n| n.active(db))
40}
41
42pub fn function_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
43 db.lookup_function_node(fqn).is_some_and(|n| n.active(db))
44}
45
46pub fn constant_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
47 db.lookup_global_constant_node(fqn)
48 .is_some_and(|n| n.active(db))
49}
50
51pub fn resolve_name_via_db(db: &dyn MirDatabase, file: &str, name: &str) -> String {
52 if name.starts_with('\\') {
53 return name.trim_start_matches('\\').to_string();
54 }
55
56 let lower = name.to_ascii_lowercase();
57 if matches!(lower.as_str(), "self" | "static" | "parent") {
58 return name.to_string();
59 }
60
61 if name.contains('\\') {
62 if let Some(imports) = (!name.starts_with('\\')).then(|| db.file_imports(file)) {
63 if let Some((first, rest)) = name.split_once('\\') {
64 if let Some(base) = imports.get(first) {
65 return format!("{base}\\{rest}");
66 }
67 }
68 }
69 if type_exists_via_db(db, name) {
70 return name.to_string();
71 }
72 if let Some(ns) = db.file_namespace(file) {
73 let qualified = format!("{}\\{}", ns, name);
74 if type_exists_via_db(db, &qualified) {
75 return qualified;
76 }
77 }
78 return name.to_string();
79 }
80
81 let imports = db.file_imports(file);
82 if let Some(fqcn) = imports.get(name) {
83 return fqcn.clone();
84 }
85 if let Some((_, fqcn)) = imports
86 .iter()
87 .find(|(alias, _)| alias.eq_ignore_ascii_case(name))
88 {
89 return fqcn.clone();
90 }
91 if let Some(ns) = db.file_namespace(file) {
92 return format!("{}\\{}", ns, name);
93 }
94 name.to_string()
95}
96
97pub fn class_template_params_via_db(
102 db: &dyn MirDatabase,
103 fqcn: &str,
104) -> Option<Arc<[TemplateParam]>> {
105 let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
106 Some(node.template_params(db))
107}
108
109pub fn inherited_template_bindings_via_db(
116 db: &dyn MirDatabase,
117 fqcn: &str,
118) -> std::collections::HashMap<Arc<str>, Union> {
119 let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
120 let mut visited: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
121 let mut current: Arc<str> = Arc::from(fqcn);
122 loop {
123 if !visited.insert(current.clone()) {
124 break;
125 }
126 let node = match db
127 .lookup_class_node(current.as_ref())
128 .filter(|n| n.active(db))
129 {
130 Some(n) => n,
131 None => break,
132 };
133 let parent = match node.parent(db) {
134 Some(p) => p,
135 None => break,
136 };
137 let extends_type_args = node.extends_type_args(db);
138 if !extends_type_args.is_empty() {
139 if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
140 for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
141 bindings
142 .entry(tp.name.clone())
143 .or_insert_with(|| ty.clone());
144 }
145 }
146 }
147 current = parent;
148 }
149 bindings
150}
151
152pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
160 let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
161 return false;
162 };
163 class_ancestors(db, node)
164 .0
165 .iter()
166 .any(|ancestor| !type_exists_via_db(db, ancestor))
167}
168
169pub fn method_is_concretely_implemented(
170 db: &dyn MirDatabase,
171 fqcn: &str,
172 method_name: &str,
173) -> bool {
174 let lower = method_name.to_lowercase();
175 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
176 return false;
177 };
178 if self_node.is_interface(db) {
181 return false;
182 }
183 if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
185 if !m.is_abstract(db) {
186 return true;
187 }
188 }
189 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
191 for t in self_node.traits(db).iter() {
192 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
193 return true;
194 }
195 }
196 for ancestor in class_ancestors(db, self_node).0.iter() {
199 let Some(anc_node) = db
200 .lookup_class_node(ancestor.as_ref())
201 .filter(|n| n.active(db))
202 else {
203 continue;
204 };
205 if anc_node.is_interface(db) {
206 continue;
207 }
208 if !anc_node.is_trait(db) {
210 if let Some(m) = db
211 .lookup_method_node(ancestor.as_ref(), &lower)
212 .filter(|m| m.active(db))
213 {
214 if !m.is_abstract(db) {
215 return true;
216 }
217 }
218 }
219 if anc_node.is_trait(db) {
222 if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
223 return true;
224 }
225 } else {
226 for t in anc_node.traits(db).iter() {
227 if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
228 return true;
229 }
230 }
231 }
232 }
233 false
234}
235
236fn trait_provides_method(
240 db: &dyn MirDatabase,
241 trait_fqcn: &str,
242 method_lower: &str,
243 visited: &mut rustc_hash::FxHashSet<String>,
244) -> bool {
245 if !visited.insert(trait_fqcn.to_string()) {
246 return false;
247 }
248 if let Some(m) = db
249 .lookup_method_node(trait_fqcn, method_lower)
250 .filter(|m| m.active(db))
251 {
252 if !m.is_abstract(db) {
253 return true;
254 }
255 }
256 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
257 return false;
258 };
259 if !node.is_trait(db) {
260 return false;
261 }
262 for t in node.traits(db).iter() {
263 if trait_provides_method(db, t.as_ref(), method_lower, visited) {
264 return true;
265 }
266 }
267 false
268}
269
270pub fn lookup_method_in_chain(
271 db: &dyn MirDatabase,
272 fqcn: &str,
273 method_name: &str,
274) -> Option<MethodNode> {
275 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
276 lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
277}
278
279fn lookup_method_in_chain_inner(
280 db: &dyn MirDatabase,
281 fqcn: &str,
282 lower: &str,
283 visited_mixins: &mut rustc_hash::FxHashSet<String>,
284) -> Option<MethodNode> {
285 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
286
287 if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
289 return Some(node);
290 }
291 for m in self_node.mixins(db).iter() {
295 if visited_mixins.insert(m.to_string()) {
296 if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
297 {
298 return Some(node);
299 }
300 }
301 }
302 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
305 for t in self_node.traits(db).iter() {
306 if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
307 return Some(node);
308 }
309 }
310 for ancestor in class_ancestors(db, self_node).0.iter() {
312 if let Some(node) = db
313 .lookup_method_node(ancestor.as_ref(), lower)
314 .filter(|n| n.active(db))
315 {
316 return Some(node);
317 }
318 if let Some(anc_node) = db
319 .lookup_class_node(ancestor.as_ref())
320 .filter(|n| n.active(db))
321 {
322 if anc_node.is_trait(db) {
323 if let Some(node) =
324 trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
325 {
326 return Some(node);
327 }
328 } else {
329 for t in anc_node.traits(db).iter() {
330 if let Some(node) =
331 trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
332 {
333 return Some(node);
334 }
335 }
336 for m in anc_node.mixins(db).iter() {
337 if visited_mixins.insert(m.to_string()) {
338 if let Some(node) =
339 lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
340 {
341 return Some(node);
342 }
343 }
344 }
345 }
346 }
347 }
348 None
349}
350
351fn trait_provides_method_node(
355 db: &dyn MirDatabase,
356 trait_fqcn: &str,
357 method_lower: &str,
358 visited: &mut rustc_hash::FxHashSet<String>,
359) -> Option<MethodNode> {
360 if !visited.insert(trait_fqcn.to_string()) {
361 return None;
362 }
363 if let Some(node) = db
364 .lookup_method_node(trait_fqcn, method_lower)
365 .filter(|n| n.active(db))
366 {
367 return Some(node);
368 }
369 let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
370 if !node.is_trait(db) {
371 return None;
372 }
373 for t in node.traits(db).iter() {
374 if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
375 return Some(found);
376 }
377 }
378 None
379}
380
381fn trait_declares_method(
385 db: &dyn MirDatabase,
386 trait_fqcn: &str,
387 method_lower: &str,
388 visited: &mut rustc_hash::FxHashSet<String>,
389) -> bool {
390 if !visited.insert(trait_fqcn.to_string()) {
391 return false;
392 }
393 if db
394 .lookup_method_node(trait_fqcn, method_lower)
395 .is_some_and(|m| m.active(db))
396 {
397 return true;
398 }
399 let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
400 return false;
401 };
402 if !node.is_trait(db) {
403 return false;
404 }
405 for t in node.traits(db).iter() {
406 if trait_declares_method(db, t.as_ref(), method_lower, visited) {
407 return true;
408 }
409 }
410 false
411}
412
413pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
414 let lower = method_name.to_lowercase();
415 let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
416 return false;
417 };
418 if db
420 .lookup_method_node(fqcn, &lower)
421 .is_some_and(|m| m.active(db))
422 {
423 return true;
424 }
425 let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
427 for t in self_node.traits(db).iter() {
428 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
429 return true;
430 }
431 }
432 for ancestor in class_ancestors(db, self_node).0.iter() {
434 if db
435 .lookup_method_node(ancestor.as_ref(), &lower)
436 .is_some_and(|m| m.active(db))
437 {
438 return true;
439 }
440 if let Some(anc_node) = db
441 .lookup_class_node(ancestor.as_ref())
442 .filter(|n| n.active(db))
443 {
444 if anc_node.is_trait(db) {
445 if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
446 return true;
447 }
448 } else {
449 for t in anc_node.traits(db).iter() {
450 if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
451 return true;
452 }
453 }
454 }
455 }
456 }
457 false
458}
459
460pub fn lookup_property_in_chain(
461 db: &dyn MirDatabase,
462 fqcn: &str,
463 prop_name: &str,
464) -> Option<PropertyNode> {
465 let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
466 lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
467}
468
469fn lookup_property_in_chain_inner(
470 db: &dyn MirDatabase,
471 fqcn: &str,
472 prop_name: &str,
473 visited_mixins: &mut rustc_hash::FxHashSet<String>,
474) -> Option<PropertyNode> {
475 let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
476
477 if let Some(node) = db
479 .lookup_property_node(fqcn, prop_name)
480 .filter(|n| n.active(db))
481 {
482 return Some(node);
483 }
484 for m in self_node.mixins(db).iter() {
487 if visited_mixins.insert(m.to_string()) {
488 if let Some(node) =
489 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
490 {
491 return Some(node);
492 }
493 }
494 }
495 for ancestor in class_ancestors(db, self_node).0.iter() {
499 if let Some(node) = db
500 .lookup_property_node(ancestor.as_ref(), prop_name)
501 .filter(|n| n.active(db))
502 {
503 return Some(node);
504 }
505 if let Some(anc_node) = db
506 .lookup_class_node(ancestor.as_ref())
507 .filter(|n| n.active(db))
508 {
509 for m in anc_node.mixins(db).iter() {
510 if visited_mixins.insert(m.to_string()) {
511 if let Some(node) =
512 lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
513 {
514 return Some(node);
515 }
516 }
517 }
518 }
519 }
520 None
521}
522
523pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
524 if db
525 .lookup_class_constant_node(fqcn, const_name)
526 .is_some_and(|n| n.active(db))
527 {
528 return true;
529 }
530 let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
531 return false;
532 };
533 for ancestor in class_ancestors(db, class_node).0.iter() {
534 if db
535 .lookup_class_constant_node(ancestor.as_ref(), const_name)
536 .is_some_and(|n| n.active(db))
537 {
538 return true;
539 }
540 }
541 false
542}
543
544pub fn member_location_via_db(
545 db: &dyn MirDatabase,
546 fqcn: &str,
547 member_name: &str,
548) -> Option<Location> {
549 if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
550 if let Some(loc) = node.location(db) {
551 return Some(loc);
552 }
553 }
554 if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
555 if let Some(loc) = node.location(db) {
556 return Some(loc);
557 }
558 }
559 if let Some(node) = db
561 .lookup_class_constant_node(fqcn, member_name)
562 .filter(|n| n.active(db))
563 {
564 if let Some(loc) = node.location(db) {
565 return Some(loc);
566 }
567 }
568 let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
569 for ancestor in class_ancestors(db, class_node).0.iter() {
570 if let Some(node) = db
571 .lookup_class_constant_node(ancestor.as_ref(), member_name)
572 .filter(|n| n.active(db))
573 {
574 if let Some(loc) = node.location(db) {
575 return Some(loc);
576 }
577 }
578 }
579 None
580}
581
582pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
583 if child == ancestor {
584 return true;
585 }
586 let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
587 return false;
588 };
589 if node.is_enum(db) {
590 if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
594 return true;
595 }
596 if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
597 return true;
598 }
599 if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
600 return true;
601 }
602 return false;
603 }
604 class_ancestors(db, node)
605 .0
606 .iter()
607 .any(|p| p.as_ref() == ancestor)
608}
609
610pub fn collect_file_definitions_uncached(
617 db: &dyn MirDatabase,
618 file: SourceFile,
619) -> FileDefinitions {
620 let path = file.path(db);
621 let text = file.text(db);
622
623 let arena = crate::arena::create_parse_arena(text.len());
624 let parsed = php_rs_parser::parse(&arena, &text);
625
626 let mut all_issues: Vec<Issue> = parsed
627 .errors
628 .iter()
629 .map(|err| {
630 Issue::new(
631 mir_issues::IssueKind::ParseError {
632 message: err.to_string(),
633 },
634 mir_issues::Location {
635 file: path.clone(),
636 line: 1,
637 line_end: 1,
638 col_start: 0,
639 col_end: 0,
640 },
641 )
642 })
643 .collect();
644
645 let collector =
646 crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
647 let (slice, collector_issues) = collector.collect_slice(&parsed.program);
648 all_issues.extend(collector_issues);
649
650 FileDefinitions {
651 slice: Arc::new(slice),
652 issues: Arc::new(all_issues),
653 }
654}
655
656#[salsa::tracked]
657pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
658 collect_file_definitions_uncached(db, file)
659}
660
661#[salsa::tracked]
684pub fn inferred_function_return_type(db: &dyn MirDatabase, node: FunctionNode) -> Arc<Union> {
685 node.inferred_return_type(db)
688 .unwrap_or_else(|| Arc::new(Union::mixed()))
689}
690
691#[salsa::tracked]
699pub fn inferred_method_return_type(db: &dyn MirDatabase, node: MethodNode) -> Arc<Union> {
700 node.inferred_return_type(db)
702 .unwrap_or_else(|| Arc::new(Union::mixed()))
703}
704
705#[allow(dead_code)]
711pub(crate) fn collect_accumulated_issues(
712 db: &dyn MirDatabase,
713 files: &[(Arc<str>, SourceFile)],
714 php_version: &str,
715) -> Vec<Issue> {
716 let mut all_issues = Vec::new();
717 let input = AnalyzeFileInput::new(db, Arc::from(php_version));
718
719 for (_path, file) in files {
720 analyze_file(db, *file, input);
722
723 let accumulated: Vec<&IssueAccumulator> = analyze_file::accumulated(db, *file, input);
725 for acc in accumulated {
726 all_issues.push(acc.0.clone());
727 }
728 }
729
730 all_issues
731}