Skip to main content

mir_analyzer/db/
queries.rs

1use std::sync::Arc;
2
3use mir_codebase::storage::{Location, TemplateParam};
4use mir_issues::Issue;
5use mir_types::Union;
6
7use super::*;
8
9/// Snapshot of a class's discriminator + abstractness, read from a
10/// registered active `ClassNode`.
11///
12/// Returned by [`class_kind_via_db`] when an active node exists for the
13/// given FQCN — call sites can use this in place of the corresponding
14/// `Codebase` lookups.
15#[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
23/// Read class kind/abstractness from an active `ClassNode`, if one is
24/// registered for `fqcn`.  Returns `None` for unregistered or inactive
25/// nodes.  All bundled and user types are mirrored into `ClassNode` by
26/// `MirDb::ingest_stub_slice`, so a `None` here means the type genuinely
27/// doesn't exist (or is inactive after a `deactivate_class_node` pass).
28pub 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
97/// Return the declared `@template` parameters for `fqcn` from an active
98/// `ClassNode`, if one is registered.  Returns `None` for unregistered
99/// or inactive nodes.  Authoritative after all collected slices have been
100/// fed through `ingest_stub_slice`.
101pub 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
109/// Walk the parent chain collecting template bindings from `@extends` type
110/// args.  Mirrors `Codebase::get_inherited_template_bindings`.
111///
112/// For `class UserRepo extends BaseRepo` with `@extends BaseRepo<User>`, this
113/// returns `{ T → User }` where `T` is `BaseRepo`'s declared template
114/// parameter.  Cycle-safe via a visited set.
115pub 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
152/// Predicate: does `fqcn` have any registered ancestor that lacks a
153/// `ClassNode` in the db?
154///
155/// `ingest_stub_slice` mirrors bundled stubs, user stubs, and PSR-4
156/// lazy-loaded definitions into the db before any Pass 2 driver runs, so
157/// a class with no active `ClassNode` is one that genuinely doesn't
158/// exist — and an unknown class trivially has no known ancestors.
159pub 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    // Interfaces don't supply implementations, regardless of how their methods
179    // are stored.
180    if self_node.is_interface(db) {
181        return false;
182    }
183    // 1. Direct own method.
184    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    // 2. Traits used directly by this class — walk transitively.
190    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    // 3. Ancestor chain (classes only — interfaces skipped, trait nodes here
197    //    are owning-class trait references already handled by their own walk).
198    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        // Ancestor's own method.
209        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        // Ancestor's used traits — walk transitively.  (For trait nodes in
220        // the ancestor list, this re-checks their own_methods + sub-traits.)
221        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
236/// Helper for [`method_is_concretely_implemented`]: walk a trait's own methods
237/// and recursively its used traits.  Returns true iff any provides a
238/// non-abstract method named `method_lower`.  Cycle-safe via `visited`.
239fn 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    // 1. Direct own method.
288    if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
289        return Some(node);
290    }
291    // 2. Docblock @mixin chains (delegated magic-method lookup) — recurse so
292    //    each mixin's own walk includes its own mixins, traits, ancestors.
293    //    Cycle-safe via `visited_mixins`.
294    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    // 3. Traits used directly — walk transitively (trait-of-traits is *not*
303    //    included in `class_ancestors`, by design — see that fn's comments).
304    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    // 4. Ancestor chain (parents, interfaces, traits — empty for enums).
311    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
351/// Node-returning sibling of [`trait_declares_method`] used by
352/// [`lookup_method_in_chain`].  Walks `trait_fqcn`'s own MethodNode then its
353/// used traits transitively.  Cycle-safe via `visited`.
354fn 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
381/// Existence-only sibling of [`trait_provides_method`].  Returns true iff the
382/// trait or any sub-trait declares a method named `method_lower` (abstract
383/// counts).  Cycle-safe via `visited`.
384fn 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    // Direct own method.
419    if db
420        .lookup_method_node(fqcn, &lower)
421        .is_some_and(|m| m.active(db))
422    {
423        return true;
424    }
425    // Traits used directly — walk transitively.
426    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    // Ancestor chain (parents, interfaces, traits).
433    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    // 1. Own property.
478    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    // 2. Docblock @mixin chains — recurse so each mixin's own walk includes
485    //    its own mixins, traits, ancestors.  Cycle-safe via `visited_mixins`.
486    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    // 3. Ancestor chain (parents + interfaces + direct traits).  Each
496    //    ancestor may itself have `@mixin` declarations that forward
497    //    property access — recurse into those too.
498    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    // Class/interface/trait/enum constants and enum cases.
560    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        // Enum semantics: only directly-declared interfaces participate
591        // (no transitive walk), plus the implicit UnitEnum / BackedEnum
592        // interfaces.
593        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
610// collect_file_definitions tracked query (S1)
611
612/// Uncached version of collect_file_definitions for bulk operations like vendor
613/// collection, where we don't need Salsa to cache the intermediate StubSlice
614/// results. This avoids holding Arc<StubSlice> in Salsa's query cache after
615/// ingestion.
616pub 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// S4 Step 3: Lazy inferred-type queries
662//
663// These tracked queries compute inferred return types on-demand during Pass 2.
664// When `Pass2Driver` encounters a function/method call, it reads the inferred
665// type via these queries instead of from a pre-computed buffer.
666//
667// This enables two key optimizations:
668// 1. Single-pass execution: inferred types are computed as needed, not upfront
669// 2. Incremental caching: if a dependent file doesn't call a function, its
670//    inferred type is never computed (Salsa skips the query)
671
672/// Lazily computes the inferred return type for a function.
673/// Called on-demand during Pass 2 analysis when we encounter a call to this function.
674/// Results are cached by Salsa; re-analysis of dependent files that don't call this
675/// function re-uses the cached inferred type.
676///
677/// **Current behavior (S4 PR3):** Reads from the already-committed `inferred_return_type`
678/// field on `FunctionNode`. Double-pass orchestration (Pass 2a inference + commit) still
679/// happens in `project.rs::analyze()`.
680///
681/// **Future (S4 PR4):** Will compute types on-demand by extracting the function body
682/// from source and running inference-only Pass 2, eliminating the double-pass.
683#[salsa::tracked]
684pub fn inferred_function_return_type(db: &dyn MirDatabase, node: FunctionNode) -> Arc<Union> {
685    // For now, read the already-committed inferred type from the FunctionNode input.
686    // This is set via commit_inferred_return_types() after Pass 2a completes.
687    node.inferred_return_type(db)
688        .unwrap_or_else(|| Arc::new(Union::mixed()))
689}
690
691/// Lazily computes the inferred return type for a method.
692///
693/// **Current behavior (S4 PR3):** Reads from the already-committed `inferred_return_type`
694/// field on `MethodNode`.
695///
696/// **Future (S4 PR4):** Will compute types on-demand by extracting the method body
697/// from source and running inference-only Pass 2.
698#[salsa::tracked]
699pub fn inferred_method_return_type(db: &dyn MirDatabase, node: MethodNode) -> Arc<Union> {
700    // For now, read the already-committed inferred type from the MethodNode input.
701    node.inferred_return_type(db)
702        .unwrap_or_else(|| Arc::new(Union::mixed()))
703}
704
705// Helper: collect analysis results via tracked query accumulators
706
707/// Collects all accumulated issues from a set of files analyzed via the
708/// `analyze_file` tracked query. Used during batch analysis to read issues
709/// that were emitted during tracked-query evaluation.
710#[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        // Call the tracked query to trigger analysis + accumulation
721        analyze_file(db, *file, input);
722
723        // Read back the accumulated issues for this file
724        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}