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