Skip to main content

mir_analyzer/
db.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use mir_codebase::storage::{
5    Assertion, ConstantStorage, FnParam, FunctionStorage, Location, MethodStorage, PropertyStorage,
6    TemplateParam, Visibility,
7};
8use mir_codebase::{Codebase, StubSlice};
9use mir_issues::Issue;
10use mir_types::Union;
11
12// ---------------------------------------------------------------------------
13// MirDatabase trait
14// ---------------------------------------------------------------------------
15
16/// Salsa database trait for mir incremental analysis.
17#[salsa::db]
18pub trait MirDatabase: salsa::Database {
19    /// The PHP version configured for this analysis run.
20    fn php_version_str(&self) -> Arc<str>;
21
22    /// Look up the [`ClassNode`] handle registered for `fqcn`, if any.
23    ///
24    /// This is an untracked read — the DashMap holds Salsa input *handles*
25    /// (cheap IDs), not data.  Changes to a class's *fields* (parent,
26    /// interfaces, active state) are tracked through the `ClassNode` input
27    /// itself, so downstream queries are still correctly invalidated.
28    fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode>;
29
30    /// Look up the [`FunctionNode`] handle registered for `fqn`, if any.
31    fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode>;
32
33    /// Look up the [`MethodNode`] for `(fqcn, method_name_lower)`, if any.
34    ///
35    /// `method_name_lower` must already be lowercased.  This is an untracked
36    /// read — changes to a method's fields are tracked through the `MethodNode`
37    /// input itself.
38    fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode>;
39
40    /// Look up the [`PropertyNode`] for `(fqcn, prop_name)`, if any.
41    fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode>;
42
43    /// Look up the [`ClassConstantNode`] for `(fqcn, const_name)`, if any.
44    fn lookup_class_constant_node(&self, fqcn: &str, const_name: &str)
45        -> Option<ClassConstantNode>;
46
47    /// Look up the [`GlobalConstantNode`] for `fqn`, if any.
48    fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode>;
49
50    /// Return all own-method nodes for `fqcn`.  Empty if no class is
51    /// registered.  Untracked iteration of a per-class HashMap.
52    fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode>;
53
54    /// Return all own-property nodes for `fqcn`.  Empty if no class is
55    /// registered.  Untracked iteration of a per-class HashMap.
56    fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode>;
57
58    /// Return all class-FQCNs currently registered as active `ClassNode`s,
59    /// optionally filtered by kind.  Untracked snapshot — callers should
60    /// treat the returned `Vec` as a one-shot view.
61    fn active_class_node_fqcns(&self) -> Vec<Arc<str>>;
62
63    /// Return all function-FQNs currently registered as active
64    /// `FunctionNode`s.  Untracked snapshot.
65    fn active_function_node_fqns(&self) -> Vec<Arc<str>>;
66}
67
68// ---------------------------------------------------------------------------
69// SourceFile input (S1)
70// ---------------------------------------------------------------------------
71
72/// Source file registered as a Salsa input.
73/// Setting `text` on an existing `SourceFile` is the single write that drives
74/// all downstream query invalidation.
75#[salsa::input]
76pub struct SourceFile {
77    pub path: Arc<str>,
78    pub text: Arc<str>,
79}
80
81// ---------------------------------------------------------------------------
82// FileDefinitions (S1)
83// ---------------------------------------------------------------------------
84
85/// Result of the `collect_file_definitions` tracked query.
86#[derive(Clone, Debug)]
87pub struct FileDefinitions {
88    pub slice: Arc<StubSlice>,
89    pub issues: Arc<Vec<Issue>>,
90}
91
92impl PartialEq for FileDefinitions {
93    fn eq(&self, other: &Self) -> bool {
94        Arc::ptr_eq(&self.slice, &other.slice) && Arc::ptr_eq(&self.issues, &other.issues)
95    }
96}
97
98unsafe impl salsa::Update for FileDefinitions {
99    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
100        unsafe { *old_pointer = new_value };
101        true
102    }
103}
104
105// ---------------------------------------------------------------------------
106// ClassNode input (S2)
107// ---------------------------------------------------------------------------
108
109/// `(interface_fqcn, type_args)` pairs from `@implements Iface<T1, T2>`
110/// docblocks.  Stored on `ClassNode` for classes only.
111pub type ImplementsTypeArgs = Arc<[(Arc<str>, Arc<[Union]>)]>;
112
113/// Salsa input representing a single class or interface in the inheritance
114/// graph.  Fields are kept minimal — only what `class_ancestors` needs.
115///
116/// Invariant: every FQCN in the codebase that is known to the Salsa DB has
117/// exactly one `ClassNode` handle, stored in `MirDb::class_nodes`.  When a
118/// class is removed (file deleted or re-indexed), its node is marked
119/// `active = false` rather than dropped, so dependent `class_ancestors` queries
120/// can still observe the change and re-run.
121#[salsa::input]
122pub struct ClassNode {
123    pub fqcn: Arc<str>,
124    /// `false` when the class has been removed from the codebase.  Dependent
125    /// queries observe this change and re-run, returning empty ancestors.
126    pub active: bool,
127    pub is_interface: bool,
128    /// `true` for trait nodes.  Traits don't currently participate in the
129    /// `class_ancestors` query (matching `Codebase::ensure_finalized` which
130    /// returns empty for traits), but registering them as `ClassNode`s lets
131    /// callers answer `type_exists`-style questions through the db.
132    pub is_trait: bool,
133    /// `true` for enum nodes.  See note on `is_trait`.
134    pub is_enum: bool,
135    /// `true` if the class is declared `abstract`.  Always `false` for
136    /// interfaces, traits, and enums.
137    pub is_abstract: bool,
138    /// Direct parent class (classes only; `None` for interfaces).
139    pub parent: Option<Arc<str>>,
140    /// Directly implemented interfaces (classes only).
141    pub interfaces: Arc<[Arc<str>]>,
142    /// Used traits (classes only).  Traits are added to the ancestor list but
143    /// their own ancestors are not recursed into, matching PHP semantics.
144    pub traits: Arc<[Arc<str>]>,
145    /// Directly extended interfaces (interfaces only).
146    pub extends: Arc<[Arc<str>]>,
147    /// Declared `@template` parameters from the class/interface/trait
148    /// docblock.  Empty for classes without templates.
149    pub template_params: Arc<[TemplateParam]>,
150    /// `@psalm-require-extends` / `@phpstan-require-extends` — FQCNs that
151    /// using classes must extend.  Populated for trait nodes only; empty for
152    /// classes/interfaces/enums.
153    pub require_extends: Arc<[Arc<str>]>,
154    /// `@psalm-require-implements` / `@phpstan-require-implements` — FQCNs
155    /// that using classes must implement.  Populated for trait nodes only;
156    /// empty for classes/interfaces/enums.
157    pub require_implements: Arc<[Arc<str>]>,
158    /// `true` if this is a *backed* enum (declared with a scalar type).
159    /// Always `false` for non-enum nodes and pure (unbacked) enums.  Used by
160    /// `extends_or_implements_via_db` to answer the implicit `BackedEnum`
161    /// interface check.
162    pub is_backed_enum: bool,
163    /// `@mixin` / `@psalm-mixin` FQCNs declared on the class docblock.
164    /// Used by `lookup_method_in_chain` for delegated magic-method lookup,
165    /// matching `Codebase::get_method`'s mixin walk.  Empty for interfaces,
166    /// traits, and enums (mixin is a class-only docblock concept).
167    pub mixins: Arc<[Arc<str>]>,
168    /// `@deprecated` message from the class docblock, if any.  Mirrors
169    /// `ClassStorage::deprecated`.  Empty / `None` for interfaces, traits,
170    /// and enums (S5-PR42 only mirrors the class-level field — those storages
171    /// don't carry a deprecated message).
172    pub deprecated: Option<Arc<str>>,
173    /// For backed-enum nodes: the declared scalar type (`int`/`string`).
174    /// Mirrors `EnumStorage::scalar_type`.  `None` for non-enum nodes and
175    /// for unbacked (pure) enums.  Used by the `Enum->value` property read
176    /// in `expr.rs` to return the backed scalar type instead of `mixed`.
177    pub enum_scalar_type: Option<Union>,
178    /// `true` if the class is declared `final`.  Always `false` for
179    /// interfaces, traits, and enums (PHP enums are implicitly final but the
180    /// codebase doesn't currently track that on `EnumStorage`).
181    pub is_final: bool,
182    /// `true` if the class is declared `readonly`.  Always `false` for
183    /// non-class kinds.
184    pub is_readonly: bool,
185    /// Source location of the class declaration.  Mirrors
186    /// `ClassStorage::location` (and `InterfaceStorage::location`,
187    /// `TraitStorage::location`, `EnumStorage::location`).  Used by
188    /// `ClassAnalyzer` to attribute issues to the right span.
189    pub location: Option<Location>,
190    /// Type arguments from `@extends Parent<T1, T2>` — populated for
191    /// classes only.  Mirrors `ClassStorage::extends_type_args`.
192    pub extends_type_args: Arc<[Union]>,
193    /// Type arguments from `@implements Iface<T1, T2>` — populated for
194    /// classes only.  Mirrors `ClassStorage::implements_type_args`.
195    pub implements_type_args: ImplementsTypeArgs,
196}
197
198/// Snapshot of a class's discriminator + abstractness, read from a
199/// registered active `ClassNode`.
200///
201/// Returned by [`class_kind_via_db`] when an active node exists for the
202/// given FQCN — call sites can use this in place of the corresponding
203/// `Codebase` lookups.
204#[derive(Debug, Clone, Copy)]
205pub struct ClassKind {
206    pub is_interface: bool,
207    pub is_trait: bool,
208    pub is_enum: bool,
209    pub is_abstract: bool,
210}
211
212/// Read class kind/abstractness from an active `ClassNode`, if one is
213/// registered for `fqcn`.  Returns `None` for unregistered or inactive
214/// nodes.  All bundled and user types are mirrored into `ClassNode` by
215/// `MirDb::ingest_codebase`, so a `None` here means the type genuinely
216/// doesn't exist (or is inactive after a `deactivate_class_node` pass).
217pub fn class_kind_via_db(db: &dyn MirDatabase, fqcn: &str) -> Option<ClassKind> {
218    let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
219    Some(ClassKind {
220        is_interface: node.is_interface(db),
221        is_trait: node.is_trait(db),
222        is_enum: node.is_enum(db),
223        is_abstract: node.is_abstract(db),
224    })
225}
226
227/// Whether a class/interface/trait/enum is registered as an active
228/// `ClassNode` in the db.  Returns `false` for unregistered or inactive
229/// nodes.  After `MirDb::ingest_codebase` runs (S5-PR8/PR9), this is
230/// the authoritative answer — bundled and user types are both mirrored.
231pub fn type_exists_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
232    db.lookup_class_node(fqcn).is_some_and(|n| n.active(db))
233}
234
235/// Return the declared `@template` parameters for `fqcn` from an active
236/// `ClassNode`, if one is registered.  Returns `None` for unregistered
237/// or inactive nodes.  Authoritative after `ingest_codebase`.
238pub fn class_template_params_via_db(
239    db: &dyn MirDatabase,
240    fqcn: &str,
241) -> Option<Arc<[TemplateParam]>> {
242    let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
243    Some(node.template_params(db))
244}
245
246/// Walk the parent chain collecting template bindings from `@extends` type
247/// args.  Mirrors `Codebase::get_inherited_template_bindings`.
248///
249/// For `class UserRepo extends BaseRepo` with `@extends BaseRepo<User>`, this
250/// returns `{ T → User }` where `T` is `BaseRepo`'s declared template
251/// parameter.  Cycle-safe via a visited set.
252pub fn inherited_template_bindings_via_db(
253    db: &dyn MirDatabase,
254    fqcn: &str,
255) -> std::collections::HashMap<Arc<str>, Union> {
256    let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
257    let mut visited: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
258    let mut current: Arc<str> = Arc::from(fqcn);
259    loop {
260        if !visited.insert(current.clone()) {
261            break;
262        }
263        let node = match db
264            .lookup_class_node(current.as_ref())
265            .filter(|n| n.active(db))
266        {
267            Some(n) => n,
268            None => break,
269        };
270        let parent = match node.parent(db) {
271            Some(p) => p,
272            None => break,
273        };
274        let extends_type_args = node.extends_type_args(db);
275        if !extends_type_args.is_empty() {
276            if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
277                for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
278                    bindings
279                        .entry(tp.name.clone())
280                        .or_insert_with(|| ty.clone());
281                }
282            }
283        }
284        current = parent;
285    }
286    bindings
287}
288
289// ---------------------------------------------------------------------------
290// FunctionNode input (S5-PR2)
291// ---------------------------------------------------------------------------
292
293/// Salsa input representing a single global function.
294///
295/// `inferred_return_type` is the Pass-2-derived return type, populated
296/// per-function by the priming sweep.  It is committed to Salsa serially
297/// after the parallel sweep returns (so worker db clones have dropped
298/// and `Storage::cancel_others` sees strong-count==1).  The buffer-and-
299/// commit pattern lives in [`InferredReturnTypes`] and
300/// [`MirDb::commit_inferred_return_types`].
301///
302/// Invariant: every FQN known to the Salsa DB has exactly one `FunctionNode`
303/// handle in `MirDb::function_nodes`.  Removed functions are marked
304/// `active = false` rather than dropped.
305#[salsa::input]
306pub struct FunctionNode {
307    pub fqn: Arc<str>,
308    pub short_name: Arc<str>,
309    pub active: bool,
310    pub params: Arc<[FnParam]>,
311    pub return_type: Option<Union>,
312    pub inferred_return_type: Option<Union>,
313    pub template_params: Arc<[TemplateParam]>,
314    pub assertions: Arc<[Assertion]>,
315    pub throws: Arc<[Arc<str>]>,
316    pub deprecated: Option<Arc<str>>,
317    pub is_pure: bool,
318    /// Source location of the declaration.  `None` for functions registered
319    /// without a known origin (e.g. some legacy test fixtures).
320    pub location: Option<Location>,
321}
322
323// ---------------------------------------------------------------------------
324// MethodNode input (S5-PR3)
325// ---------------------------------------------------------------------------
326
327/// Salsa input representing a single method or interface/trait method.
328///
329/// `inferred_return_type` is the Pass-2-derived return type, populated per
330/// method by the priming sweep.  Committed to Salsa serially after the
331/// parallel sweep returns; see [`FunctionNode`] for the buffer-and-commit
332/// pattern that resolves the historical "S3 deadlock".
333///
334/// The node is keyed by `(fqcn, method_name_lower)` where `fqcn` is the
335/// FQCN of the **owning** class/interface/trait and `method_name_lower` is
336/// the PHP-normalised (lowercased) method name.  Nodes for classes that are
337/// removed from the codebase are marked `active = false` via
338/// `deactivate_class_methods` rather than being dropped.
339#[salsa::input]
340pub struct MethodNode {
341    pub fqcn: Arc<str>,
342    pub name: Arc<str>,
343    pub active: bool,
344    pub params: Arc<[FnParam]>,
345    pub return_type: Option<Union>,
346    pub inferred_return_type: Option<Union>,
347    pub template_params: Arc<[TemplateParam]>,
348    pub assertions: Arc<[Assertion]>,
349    pub throws: Arc<[Arc<str>]>,
350    pub deprecated: Option<Arc<str>>,
351    pub visibility: Visibility,
352    pub is_static: bool,
353    pub is_abstract: bool,
354    pub is_final: bool,
355    pub is_constructor: bool,
356    pub is_pure: bool,
357    /// Source location of the declaration.  `None` for synthesized methods
358    /// (e.g. enum implicit `cases`/`from`/`tryFrom`).
359    pub location: Option<Location>,
360}
361
362// ---------------------------------------------------------------------------
363// PropertyNode input (S5-PR4)
364// ---------------------------------------------------------------------------
365
366/// Salsa input representing a single class/trait property.
367///
368/// `inferred_ty` is intentionally absent — it stays in `PropertyStorage` until
369/// a future S3-style tracked query promotes it.
370///
371/// Keyed by `(owner fqcn, prop_name)` — property names are case-sensitive.
372#[salsa::input]
373pub struct PropertyNode {
374    pub fqcn: Arc<str>,
375    pub name: Arc<str>,
376    pub active: bool,
377    pub ty: Option<Union>,
378    pub visibility: Visibility,
379    pub is_static: bool,
380    pub is_readonly: bool,
381    pub location: Option<Location>,
382}
383
384// ---------------------------------------------------------------------------
385// ClassConstantNode input (S5-PR4)
386// ---------------------------------------------------------------------------
387
388/// Salsa input representing a single class/interface/enum constant.
389///
390/// Keyed by `(owner fqcn, const_name)` — constant names are case-sensitive.
391#[salsa::input]
392pub struct ClassConstantNode {
393    pub fqcn: Arc<str>,
394    pub name: Arc<str>,
395    pub active: bool,
396    pub ty: Union,
397    pub visibility: Option<Visibility>,
398    pub is_final: bool,
399    /// Source location of the declaration.  Mirrors `ConstantStorage::location`
400    /// for class/interface/trait constants, and `EnumCaseStorage::location` for
401    /// enum cases.  `None` for nodes registered without a source span.
402    pub location: Option<Location>,
403}
404
405// ---------------------------------------------------------------------------
406// GlobalConstantNode input (S5-PR47)
407// ---------------------------------------------------------------------------
408
409/// Salsa input representing a global PHP constant (e.g. `PHP_EOL`).
410/// Mirrors `Codebase::constants`.
411#[salsa::input]
412pub struct GlobalConstantNode {
413    pub fqn: Arc<str>,
414    pub active: bool,
415    pub ty: Union,
416}
417
418// ---------------------------------------------------------------------------
419// Ancestors return type (S2)
420// ---------------------------------------------------------------------------
421
422/// The computed ancestor list for a class or interface.
423///
424/// Uses content equality so Salsa's cycle-convergence check can detect
425/// fixpoints correctly (two empty lists from different iterations are equal).
426#[derive(Clone, Debug, Default)]
427pub struct Ancestors(pub Vec<Arc<str>>);
428
429impl PartialEq for Ancestors {
430    fn eq(&self, other: &Self) -> bool {
431        self.0.len() == other.0.len()
432            && self
433                .0
434                .iter()
435                .zip(&other.0)
436                .all(|(a, b)| a.as_ref() == b.as_ref())
437    }
438}
439
440unsafe impl salsa::Update for Ancestors {
441    unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
442        let old = unsafe { &mut *old_ptr };
443        if *old == new_val {
444            return false;
445        }
446        *old = new_val;
447        true
448    }
449}
450
451// ---------------------------------------------------------------------------
452// class_ancestors tracked query (S2)
453// ---------------------------------------------------------------------------
454
455fn ancestors_initial(_db: &dyn MirDatabase, _id: salsa::Id, _node: ClassNode) -> Ancestors {
456    Ancestors(vec![])
457}
458
459fn ancestors_cycle(
460    _db: &dyn MirDatabase,
461    _cycle: &salsa::Cycle,
462    _last: &Ancestors,
463    _value: Ancestors,
464    _node: ClassNode,
465) -> Ancestors {
466    // PHP class cycles are a compile-time error.  Break immediately with an
467    // empty list so the fixpoint converges on the first iteration.
468    Ancestors(vec![])
469}
470
471/// Salsa tracked query: compute the transitive ancestor list for a class or
472/// interface.
473///
474/// Ancestors are accumulated in the same order as `Codebase::ensure_finalized`:
475/// parent → parent's ancestors → implemented interfaces + their ancestors →
476/// used traits (class); or: extended interfaces + their ancestors (interface).
477///
478/// Cycle recovery returns an empty list on the first iteration, which is
479/// correct because PHP forbids circular inheritance.
480#[salsa::tracked(cycle_fn = ancestors_cycle, cycle_initial = ancestors_initial)]
481pub fn class_ancestors(db: &dyn MirDatabase, node: ClassNode) -> Ancestors {
482    if !node.active(db) {
483        return Ancestors(vec![]);
484    }
485    // Invariant: enums and traits always return empty here.
486    // - Enums: matches `Codebase::ensure_finalized`.  Enum membership
487    //   questions go through `extends_or_implements_via_db`, which reads
488    //   `interfaces` / `is_backed_enum` directly.
489    // - Traits: matches `Codebase::ensure_finalized` (which only computes
490    //   ancestors for classes/interfaces).  Trait-of-trait walking is
491    //   handled by `method_is_concretely_implemented` /
492    //   `trait_provides_method` directly via the `traits` field.
493    // Do not lift either short-circuit without also auditing every caller
494    // of `class_ancestors`.
495    if node.is_enum(db) || node.is_trait(db) {
496        return Ancestors(vec![]);
497    }
498
499    let mut all: Vec<Arc<str>> = Vec::new();
500    let mut seen: HashSet<String> = HashSet::new();
501
502    let add = |fqcn: &Arc<str>, all: &mut Vec<Arc<str>>, seen: &mut HashSet<String>| {
503        if seen.insert(fqcn.to_string()) {
504            all.push(fqcn.clone());
505        }
506    };
507
508    if node.is_interface(db) {
509        for e in node.extends(db).iter() {
510            add(e, &mut all, &mut seen);
511            if let Some(parent_node) = db.lookup_class_node(e) {
512                for a in class_ancestors(db, parent_node).0 {
513                    add(&a, &mut all, &mut seen);
514                }
515            }
516        }
517    } else {
518        if let Some(ref p) = node.parent(db) {
519            add(p, &mut all, &mut seen);
520            if let Some(parent_node) = db.lookup_class_node(p) {
521                for a in class_ancestors(db, parent_node).0 {
522                    add(&a, &mut all, &mut seen);
523                }
524            }
525        }
526        for iface in node.interfaces(db).iter() {
527            add(iface, &mut all, &mut seen);
528            if let Some(iface_node) = db.lookup_class_node(iface) {
529                for a in class_ancestors(db, iface_node).0 {
530                    add(&a, &mut all, &mut seen);
531                }
532            }
533        }
534        for t in node.traits(db).iter() {
535            add(t, &mut all, &mut seen);
536        }
537    }
538
539    Ancestors(all)
540}
541
542/// Predicate variant of [`Codebase::has_unknown_ancestor`] backed by the
543/// Salsa db.
544///
545/// `ingest_codebase` (S5-PR8/PR9 / PR11a) mirrors bundled stubs, user
546/// stubs, and PSR-4 lazy-loaded definitions into the db before any
547/// Pass 2 driver runs, so a class with no active `ClassNode` is one
548/// that genuinely doesn't exist — and an unknown class trivially has
549/// no known ancestors.
550pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
551    let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
552        return false;
553    };
554    class_ancestors(db, node)
555        .0
556        .iter()
557        .any(|ancestor| !type_exists_via_db(db, ancestor))
558}
559
560/// Returns `true` iff `fqcn` (or any non-interface ancestor) declares a
561/// *concrete* (non-abstract) implementation of `method_name`.  Methods
562/// declared on interface ancestors are treated as abstract — interfaces don't
563/// supply implementations even though their `MethodStorage` is collected with
564/// `is_abstract = false`.  Mirrors the implemented-method semantics that
565/// [`Codebase::get_method`] hand-rolls via its `ms.is_abstract = true`
566/// rewrite for interface ancestors.
567///
568/// Method names are PHP-case-insensitive; the lookup lower-cases internally.
569/// Cycle-safe: relies on `class_ancestors` cycle recovery.
570pub fn method_is_concretely_implemented(
571    db: &dyn MirDatabase,
572    fqcn: &str,
573    method_name: &str,
574) -> bool {
575    let lower = method_name.to_lowercase();
576    let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
577        return false;
578    };
579    // Interfaces don't supply implementations, regardless of how their methods
580    // are stored.
581    if self_node.is_interface(db) {
582        return false;
583    }
584    // 1. Direct own method.
585    if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
586        if !m.is_abstract(db) {
587            return true;
588        }
589    }
590    // 2. Traits used directly by this class — walk transitively.
591    let mut visited_traits: HashSet<String> = HashSet::new();
592    for t in self_node.traits(db).iter() {
593        if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
594            return true;
595        }
596    }
597    // 3. Ancestor chain (classes only — interfaces skipped, trait nodes here
598    //    are owning-class trait references already handled by their own walk).
599    for ancestor in class_ancestors(db, self_node).0.iter() {
600        let Some(anc_node) = db
601            .lookup_class_node(ancestor.as_ref())
602            .filter(|n| n.active(db))
603        else {
604            continue;
605        };
606        if anc_node.is_interface(db) {
607            continue;
608        }
609        // Ancestor's own method.
610        if !anc_node.is_trait(db) {
611            if let Some(m) = db
612                .lookup_method_node(ancestor.as_ref(), &lower)
613                .filter(|m| m.active(db))
614            {
615                if !m.is_abstract(db) {
616                    return true;
617                }
618            }
619        }
620        // Ancestor's used traits — walk transitively.  (For trait nodes in
621        // the ancestor list, this re-checks their own_methods + sub-traits.)
622        if anc_node.is_trait(db) {
623            if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
624                return true;
625            }
626        } else {
627            for t in anc_node.traits(db).iter() {
628                if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
629                    return true;
630                }
631            }
632        }
633    }
634    false
635}
636
637/// Helper for [`method_is_concretely_implemented`]: walk a trait's own methods
638/// and recursively its used traits.  Returns true iff any provides a
639/// non-abstract method named `method_lower`.  Cycle-safe via `visited`.
640fn trait_provides_method(
641    db: &dyn MirDatabase,
642    trait_fqcn: &str,
643    method_lower: &str,
644    visited: &mut HashSet<String>,
645) -> bool {
646    if !visited.insert(trait_fqcn.to_string()) {
647        return false;
648    }
649    if let Some(m) = db
650        .lookup_method_node(trait_fqcn, method_lower)
651        .filter(|m| m.active(db))
652    {
653        if !m.is_abstract(db) {
654            return true;
655        }
656    }
657    let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
658        return false;
659    };
660    if !node.is_trait(db) {
661        return false;
662    }
663    for t in node.traits(db).iter() {
664        if trait_provides_method(db, t.as_ref(), method_lower, visited) {
665            return true;
666        }
667    }
668    false
669}
670
671/// Returns `true` iff `fqcn` (or any ancestor / used trait, transitively)
672/// declares a method named `method_name` (abstract or concrete).  Used by
673/// magic-method existence checks (`__call`, `__callStatic`, `__invoke`,
674/// `__construct`) and intersection-type method lookups.
675///
676/// Method names are PHP-case-insensitive; the lookup lower-cases internally.
677/// Cycle-safe: relies on `class_ancestors` cycle recovery and a per-call
678/// `visited` set across trait-of-trait walks.
679/// Walk `fqcn`'s own MethodNode then the class-ancestor chain, returning the
680/// first active [`MethodNode`] whose name matches `method_name` (case-
681/// insensitive).  Mirrors [`Codebase::get_method`]'s ancestor walk.
682///
683/// Used when a caller needs the full method node (params, return type,
684/// visibility, etc.), not just an existence check.
685pub fn lookup_method_in_chain(
686    db: &dyn MirDatabase,
687    fqcn: &str,
688    method_name: &str,
689) -> Option<MethodNode> {
690    let mut visited_mixins: HashSet<String> = HashSet::new();
691    lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
692}
693
694fn lookup_method_in_chain_inner(
695    db: &dyn MirDatabase,
696    fqcn: &str,
697    lower: &str,
698    visited_mixins: &mut HashSet<String>,
699) -> Option<MethodNode> {
700    let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
701
702    // 1. Direct own method.
703    if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
704        return Some(node);
705    }
706    // 2. Docblock @mixin chains (delegated magic-method lookup) — recurse so
707    //    each mixin's own walk includes its own mixins, traits, ancestors.
708    //    Cycle-safe via `visited_mixins`.
709    for m in self_node.mixins(db).iter() {
710        if visited_mixins.insert(m.to_string()) {
711            if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
712            {
713                return Some(node);
714            }
715        }
716    }
717    // 3. Traits used directly — walk transitively (trait-of-traits is *not*
718    //    included in `class_ancestors`, by design — see that fn's comments).
719    let mut visited_traits: HashSet<String> = HashSet::new();
720    for t in self_node.traits(db).iter() {
721        if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
722            return Some(node);
723        }
724    }
725    // 4. Ancestor chain (parents, interfaces, traits — empty for enums).
726    for ancestor in class_ancestors(db, self_node).0.iter() {
727        if let Some(node) = db
728            .lookup_method_node(ancestor.as_ref(), lower)
729            .filter(|n| n.active(db))
730        {
731            return Some(node);
732        }
733        if let Some(anc_node) = db
734            .lookup_class_node(ancestor.as_ref())
735            .filter(|n| n.active(db))
736        {
737            if anc_node.is_trait(db) {
738                if let Some(node) =
739                    trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
740                {
741                    return Some(node);
742                }
743            } else {
744                for t in anc_node.traits(db).iter() {
745                    if let Some(node) =
746                        trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
747                    {
748                        return Some(node);
749                    }
750                }
751                for m in anc_node.mixins(db).iter() {
752                    if visited_mixins.insert(m.to_string()) {
753                        if let Some(node) =
754                            lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
755                        {
756                            return Some(node);
757                        }
758                    }
759                }
760            }
761        }
762    }
763    None
764}
765
766/// Node-returning sibling of [`trait_declares_method`] used by
767/// [`lookup_method_in_chain`].  Walks `trait_fqcn`'s own MethodNode then its
768/// used traits transitively.  Cycle-safe via `visited`.
769fn trait_provides_method_node(
770    db: &dyn MirDatabase,
771    trait_fqcn: &str,
772    method_lower: &str,
773    visited: &mut HashSet<String>,
774) -> Option<MethodNode> {
775    if !visited.insert(trait_fqcn.to_string()) {
776        return None;
777    }
778    if let Some(node) = db
779        .lookup_method_node(trait_fqcn, method_lower)
780        .filter(|n| n.active(db))
781    {
782        return Some(node);
783    }
784    let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
785    if !node.is_trait(db) {
786        return None;
787    }
788    for t in node.traits(db).iter() {
789        if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
790            return Some(found);
791        }
792    }
793    None
794}
795
796pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
797    let lower = method_name.to_lowercase();
798    let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
799        return false;
800    };
801    // Direct own method.
802    if db
803        .lookup_method_node(fqcn, &lower)
804        .is_some_and(|m| m.active(db))
805    {
806        return true;
807    }
808    // Traits used directly — walk transitively.
809    let mut visited_traits: HashSet<String> = HashSet::new();
810    for t in self_node.traits(db).iter() {
811        if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
812            return true;
813        }
814    }
815    // Ancestor chain (parents, interfaces, traits).
816    for ancestor in class_ancestors(db, self_node).0.iter() {
817        if db
818            .lookup_method_node(ancestor.as_ref(), &lower)
819            .is_some_and(|m| m.active(db))
820        {
821            return true;
822        }
823        if let Some(anc_node) = db
824            .lookup_class_node(ancestor.as_ref())
825            .filter(|n| n.active(db))
826        {
827            if anc_node.is_trait(db) {
828                if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
829                    return true;
830                }
831            } else {
832                for t in anc_node.traits(db).iter() {
833                    if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
834                        return true;
835                    }
836                }
837            }
838        }
839    }
840    false
841}
842
843/// Existence-only sibling of [`trait_provides_method`].  Returns true iff the
844/// trait or any sub-trait declares a method named `method_lower` (abstract
845/// counts).  Cycle-safe via `visited`.
846fn trait_declares_method(
847    db: &dyn MirDatabase,
848    trait_fqcn: &str,
849    method_lower: &str,
850    visited: &mut HashSet<String>,
851) -> bool {
852    if !visited.insert(trait_fqcn.to_string()) {
853        return false;
854    }
855    if db
856        .lookup_method_node(trait_fqcn, method_lower)
857        .is_some_and(|m| m.active(db))
858    {
859        return true;
860    }
861    let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
862        return false;
863    };
864    if !node.is_trait(db) {
865        return false;
866    }
867    for t in node.traits(db).iter() {
868        if trait_declares_method(db, t.as_ref(), method_lower, visited) {
869            return true;
870        }
871    }
872    false
873}
874
875/// Walk `fqcn`'s own [`PropertyNode`] then mixins, traits, and ancestors,
876/// returning the first active node whose name matches `prop_name`.
877/// Mirrors [`Codebase::get_property`]'s walk: own → mixins (recursive) →
878/// each ancestor's own + mixins → direct traits' own.  `class_ancestors`
879/// already includes parents, interfaces, and direct traits in its returned
880/// list, so the ancestor loop covers traits' `own_properties`.
881///
882/// Property names are case-sensitive in PHP.  Cycle-safe via a per-call
883/// `visited_mixins` set; `class_ancestors` itself is cycle-safe.
884pub fn lookup_property_in_chain(
885    db: &dyn MirDatabase,
886    fqcn: &str,
887    prop_name: &str,
888) -> Option<PropertyNode> {
889    let mut visited_mixins: HashSet<String> = HashSet::new();
890    lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
891}
892
893fn lookup_property_in_chain_inner(
894    db: &dyn MirDatabase,
895    fqcn: &str,
896    prop_name: &str,
897    visited_mixins: &mut HashSet<String>,
898) -> Option<PropertyNode> {
899    let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
900
901    // 1. Own property.
902    if let Some(node) = db
903        .lookup_property_node(fqcn, prop_name)
904        .filter(|n| n.active(db))
905    {
906        return Some(node);
907    }
908    // 2. Docblock @mixin chains — recurse so each mixin's own walk includes
909    //    its own mixins, traits, ancestors.  Cycle-safe via `visited_mixins`.
910    for m in self_node.mixins(db).iter() {
911        if visited_mixins.insert(m.to_string()) {
912            if let Some(node) =
913                lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
914            {
915                return Some(node);
916            }
917        }
918    }
919    // 3. Ancestor chain (parents + interfaces + direct traits).  Each
920    //    ancestor may itself have `@mixin` declarations that forward
921    //    property access — recurse into those too.
922    for ancestor in class_ancestors(db, self_node).0.iter() {
923        if let Some(node) = db
924            .lookup_property_node(ancestor.as_ref(), prop_name)
925            .filter(|n| n.active(db))
926        {
927            return Some(node);
928        }
929        if let Some(anc_node) = db
930            .lookup_class_node(ancestor.as_ref())
931            .filter(|n| n.active(db))
932        {
933            for m in anc_node.mixins(db).iter() {
934                if visited_mixins.insert(m.to_string()) {
935                    if let Some(node) =
936                        lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
937                    {
938                        return Some(node);
939                    }
940                }
941            }
942        }
943    }
944    None
945}
946
947/// Returns `true` iff `fqcn` (or any class/interface in its ancestor chain)
948/// declares a class constant named `const_name`.  Mirrors
949/// [`Codebase::get_class_constant`]'s walk for existence purposes:
950/// own → traits → ancestors (incl. interfaces).  `class_ancestors` already
951/// includes direct traits and interfaces in its returned list, so a single
952/// walk is sufficient.
953///
954/// Constant names are case-sensitive in PHP.  Cycle-safe via
955/// `class_ancestors`'s own cycle recovery.
956pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
957    if db
958        .lookup_class_constant_node(fqcn, const_name)
959        .is_some_and(|n| n.active(db))
960    {
961        return true;
962    }
963    let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
964        return false;
965    };
966    for ancestor in class_ancestors(db, class_node).0.iter() {
967        if db
968            .lookup_class_constant_node(ancestor.as_ref(), const_name)
969            .is_some_and(|n| n.active(db))
970        {
971            return true;
972        }
973    }
974    false
975}
976
977/// Look up the source location of a class member (method, property, or
978/// class/interface/trait/enum constant including enum cases).  Walks the
979/// inheritance chain via the same helpers used by analyzer call sites
980/// (`lookup_method_in_chain`, `lookup_property_in_chain`,
981/// `class_ancestors` for constants), so members defined on an ancestor
982/// are still found.  Returns `None` if no member with that name exists,
983/// or if the member exists but has no recorded location (e.g. a
984/// synthesized enum implicit method).
985pub fn member_location_via_db(
986    db: &dyn MirDatabase,
987    fqcn: &str,
988    member_name: &str,
989) -> Option<Location> {
990    if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
991        if let Some(loc) = node.location(db) {
992            return Some(loc);
993        }
994    }
995    if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
996        if let Some(loc) = node.location(db) {
997            return Some(loc);
998        }
999    }
1000    // Class/interface/trait/enum constants and enum cases.
1001    if let Some(node) = db
1002        .lookup_class_constant_node(fqcn, member_name)
1003        .filter(|n| n.active(db))
1004    {
1005        if let Some(loc) = node.location(db) {
1006            return Some(loc);
1007        }
1008    }
1009    let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
1010    for ancestor in class_ancestors(db, class_node).0.iter() {
1011        if let Some(node) = db
1012            .lookup_class_constant_node(ancestor.as_ref(), member_name)
1013            .filter(|n| n.active(db))
1014        {
1015            if let Some(loc) = node.location(db) {
1016                return Some(loc);
1017            }
1018        }
1019    }
1020    None
1021}
1022
1023/// Predicate variant of [`Codebase::extends_or_implements`] backed by the
1024/// Salsa db.
1025///
1026/// Returns `true` iff `child` is `ancestor`, or `child`'s transitive
1027/// ancestor list (via [`class_ancestors`]) contains `ancestor`.  For enums
1028/// the ancestor list is empty by construction (matching
1029/// `Codebase::ensure_finalized`); membership is answered directly from
1030/// the enum's directly-declared interfaces and the implicit
1031/// `UnitEnum` / `BackedEnum` interfaces.
1032///
1033/// Unregistered classes return `false` — `ingest_codebase` populates
1034/// the db before any Pass 2 driver runs, so a class with no active
1035/// `ClassNode` genuinely doesn't exist.
1036pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
1037    if child == ancestor {
1038        return true;
1039    }
1040    let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
1041        return false;
1042    };
1043    if node.is_enum(db) {
1044        // Match `Codebase::extends_or_implements` enum semantics: only
1045        // directly-declared interfaces participate (no transitive walk),
1046        // plus the implicit UnitEnum / BackedEnum interfaces.
1047        if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
1048            return true;
1049        }
1050        if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
1051            return true;
1052        }
1053        if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
1054            return true;
1055        }
1056        return false;
1057    }
1058    class_ancestors(db, node)
1059        .0
1060        .iter()
1061        .any(|p| p.as_ref() == ancestor)
1062}
1063
1064// ---------------------------------------------------------------------------
1065// collect_file_definitions tracked query (S1)
1066// ---------------------------------------------------------------------------
1067
1068#[salsa::tracked]
1069pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
1070    let path = file.path(db);
1071    let text = file.text(db);
1072
1073    let arena = bumpalo::Bump::new();
1074    let parsed = php_rs_parser::parse(&arena, &text);
1075
1076    let mut all_issues: Vec<Issue> = parsed
1077        .errors
1078        .iter()
1079        .map(|err| {
1080            Issue::new(
1081                mir_issues::IssueKind::ParseError {
1082                    message: err.to_string(),
1083                },
1084                mir_issues::Location {
1085                    file: path.clone(),
1086                    line: 1,
1087                    line_end: 1,
1088                    col_start: 0,
1089                    col_end: 0,
1090                },
1091            )
1092        })
1093        .collect();
1094
1095    let collector =
1096        crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
1097    let (slice, collector_issues) = collector.collect_slice(&parsed.program);
1098    all_issues.extend(collector_issues);
1099
1100    FileDefinitions {
1101        slice: Arc::new(slice),
1102        issues: Arc::new(all_issues),
1103    }
1104}
1105
1106// ---------------------------------------------------------------------------
1107// MirDb concrete database
1108// ---------------------------------------------------------------------------
1109
1110/// Concrete in-process Salsa database.
1111///
1112/// `Clone` is required for parallel batch analysis: salsa's supported
1113/// pattern for sharing a db across threads is to give each worker its
1114/// own clone (each clone gets a fresh `ZalsaLocal`, sharing the
1115/// underlying memoization storage).  Sharing `&MirDb` across threads is
1116/// **not** supported because `salsa::Database: Send` (not `Sync`).
1117#[salsa::db]
1118#[derive(Default, Clone)]
1119pub struct MirDb {
1120    storage: salsa::Storage<Self>,
1121    /// FQCN → ClassNode handle registry (not tracked by Salsa; see
1122    /// `lookup_class_node` for the rationale).
1123    class_nodes: HashMap<Arc<str>, ClassNode>,
1124    /// FQN → FunctionNode handle registry.
1125    function_nodes: HashMap<Arc<str>, FunctionNode>,
1126    /// (owner FQCN) → (method_name_lower → MethodNode) handle registry.
1127    method_nodes: HashMap<Arc<str>, HashMap<Arc<str>, MethodNode>>,
1128    /// (owner FQCN) → (prop_name → PropertyNode) handle registry.
1129    property_nodes: HashMap<Arc<str>, HashMap<Arc<str>, PropertyNode>>,
1130    /// (owner FQCN) → (const_name → ClassConstantNode) handle registry.
1131    class_constant_nodes: HashMap<Arc<str>, HashMap<Arc<str>, ClassConstantNode>>,
1132    /// FQN → GlobalConstantNode handle registry.
1133    global_constant_nodes: HashMap<Arc<str>, GlobalConstantNode>,
1134}
1135
1136#[salsa::db]
1137impl salsa::Database for MirDb {}
1138
1139#[salsa::db]
1140impl MirDatabase for MirDb {
1141    fn php_version_str(&self) -> Arc<str> {
1142        Arc::from("8.2")
1143    }
1144
1145    fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
1146        self.class_nodes.get(fqcn).copied()
1147    }
1148
1149    fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
1150        self.function_nodes.get(fqn).copied()
1151    }
1152
1153    fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
1154        self.method_nodes
1155            .get(fqcn)
1156            .and_then(|m| m.get(method_name_lower).copied())
1157    }
1158
1159    fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
1160        self.property_nodes
1161            .get(fqcn)
1162            .and_then(|m| m.get(prop_name).copied())
1163    }
1164
1165    fn lookup_class_constant_node(
1166        &self,
1167        fqcn: &str,
1168        const_name: &str,
1169    ) -> Option<ClassConstantNode> {
1170        self.class_constant_nodes
1171            .get(fqcn)
1172            .and_then(|m| m.get(const_name).copied())
1173    }
1174
1175    fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
1176        self.global_constant_nodes.get(fqn).copied()
1177    }
1178
1179    fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
1180        self.method_nodes
1181            .get(fqcn)
1182            .map(|m| m.values().copied().collect())
1183            .unwrap_or_default()
1184    }
1185
1186    fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
1187        self.property_nodes
1188            .get(fqcn)
1189            .map(|m| m.values().copied().collect())
1190            .unwrap_or_default()
1191    }
1192
1193    fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
1194        self.class_nodes
1195            .iter()
1196            .filter_map(|(fqcn, node)| {
1197                if node.active(self) {
1198                    Some(fqcn.clone())
1199                } else {
1200                    None
1201                }
1202            })
1203            .collect()
1204    }
1205
1206    fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
1207        self.function_nodes
1208            .iter()
1209            .filter_map(|(fqn, node)| {
1210                if node.active(self) {
1211                    Some(fqn.clone())
1212                } else {
1213                    None
1214                }
1215            })
1216            .collect()
1217    }
1218}
1219
1220// ---------------------------------------------------------------------------
1221// Inferred-return-type buffer (S3 deadlock resolution)
1222// ---------------------------------------------------------------------------
1223
1224/// Thread-safe buffer used by Pass 2's priming sweep to record inferred
1225/// return types per (function|method).  The sweep runs in parallel across
1226/// rayon workers each holding its own `MirDb` clone, so writing setters
1227/// from inside the closure would deadlock against `Storage::cancel_others`
1228/// (which waits for all clones to drop before allowing a write).
1229///
1230/// Instead, workers push into this buffer (a `Mutex<Vec<…>>` — pushes are
1231/// fast, contention is negligible vs the work each worker does).  After
1232/// the parallel sweep returns, all worker clones are dropped and
1233/// [`MirDb::commit_inferred_return_types`] drains the buffer into Salsa
1234/// setters on the canonical db (which now has strong-count==1).
1235#[derive(Default)]
1236#[allow(clippy::type_complexity)]
1237pub struct InferredReturnTypes {
1238    /// `(fqn, inferred)` pairs for free functions.
1239    functions: std::sync::Mutex<Vec<(Arc<str>, Union)>>,
1240    /// `(fqcn, method_name, inferred)` triples for methods.  `method_name`
1241    /// is the original (non-lowercased) name; `commit` lowercases at
1242    /// lookup time to match `MirDb::method_nodes`' key convention.
1243    methods: std::sync::Mutex<Vec<(Arc<str>, Arc<str>, Union)>>,
1244}
1245
1246impl InferredReturnTypes {
1247    pub fn new() -> Self {
1248        Self::default()
1249    }
1250
1251    pub fn push_function(&self, fqn: Arc<str>, inferred: Union) {
1252        if let Ok(mut g) = self.functions.lock() {
1253            g.push((fqn, inferred));
1254        }
1255    }
1256
1257    pub fn push_method(&self, fqcn: Arc<str>, name: Arc<str>, inferred: Union) {
1258        if let Ok(mut g) = self.methods.lock() {
1259            g.push((fqcn, name, inferred));
1260        }
1261    }
1262}
1263
1264/// Field bag for [`MirDb::upsert_class_node`].  Construct with `..Default::default()`
1265/// to fill in the fields that don't apply to your kind (e.g. interfaces leave
1266/// `parent`, `traits`, `mixins`, `is_abstract`, etc. at their defaults).
1267///
1268/// Per-kind constructors (`for_class` / `for_interface` / `for_trait` /
1269/// `for_enum`) seed the kind discriminators so the caller only has to populate
1270/// kind-specific fields.
1271#[derive(Debug, Clone, Default)]
1272pub struct ClassNodeFields {
1273    pub fqcn: Arc<str>,
1274    pub is_interface: bool,
1275    pub is_trait: bool,
1276    pub is_enum: bool,
1277    pub is_abstract: bool,
1278    pub parent: Option<Arc<str>>,
1279    pub interfaces: Arc<[Arc<str>]>,
1280    pub traits: Arc<[Arc<str>]>,
1281    pub extends: Arc<[Arc<str>]>,
1282    pub template_params: Arc<[TemplateParam]>,
1283    pub require_extends: Arc<[Arc<str>]>,
1284    pub require_implements: Arc<[Arc<str>]>,
1285    pub is_backed_enum: bool,
1286    pub mixins: Arc<[Arc<str>]>,
1287    pub deprecated: Option<Arc<str>>,
1288    pub enum_scalar_type: Option<Union>,
1289    pub is_final: bool,
1290    pub is_readonly: bool,
1291    pub location: Option<Location>,
1292    pub extends_type_args: Arc<[Union]>,
1293    pub implements_type_args: ImplementsTypeArgs,
1294}
1295
1296impl ClassNodeFields {
1297    pub fn for_class(fqcn: Arc<str>) -> Self {
1298        Self {
1299            fqcn,
1300            ..Self::default()
1301        }
1302    }
1303
1304    pub fn for_interface(fqcn: Arc<str>) -> Self {
1305        Self {
1306            fqcn,
1307            is_interface: true,
1308            ..Self::default()
1309        }
1310    }
1311
1312    pub fn for_trait(fqcn: Arc<str>) -> Self {
1313        Self {
1314            fqcn,
1315            is_trait: true,
1316            ..Self::default()
1317        }
1318    }
1319
1320    pub fn for_enum(fqcn: Arc<str>) -> Self {
1321        Self {
1322            fqcn,
1323            is_enum: true,
1324            ..Self::default()
1325        }
1326    }
1327}
1328
1329impl MirDb {
1330    /// Create or update the `ClassNode` for `fqcn`.
1331    ///
1332    /// If a handle already exists, its fields are updated in-place so Salsa
1333    /// can track the change.  A new handle is created only on first registration.
1334    #[allow(clippy::too_many_arguments)]
1335    pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
1336        use salsa::Setter as _;
1337        let ClassNodeFields {
1338            fqcn,
1339            is_interface,
1340            is_trait,
1341            is_enum,
1342            is_abstract,
1343            parent,
1344            interfaces,
1345            traits,
1346            extends,
1347            template_params,
1348            require_extends,
1349            require_implements,
1350            is_backed_enum,
1351            mixins,
1352            deprecated,
1353            enum_scalar_type,
1354            is_final,
1355            is_readonly,
1356            location,
1357            extends_type_args,
1358            implements_type_args,
1359        } = fields;
1360        if let Some(&node) = self.class_nodes.get(&fqcn) {
1361            // Fast-skip: an already-active node whose Salsa-tracked fields
1362            // match the upsert input.  Bulk re-ingest paths
1363            // (`ingest_codebase` / `lazy_load_*`) call this for every class
1364            // on every iteration; without the skip each call fires 13
1365            // setters, each acquiring the Salsa write lock.  Schema doesn't
1366            // mutate after Pass 1 (Pass 2 only writes `inferred_return_type`
1367            // which lives on `Codebase`, not the db), so an active node with
1368            // matching fields is by construction up to date.
1369            //
1370            // Mutation paths (LSP re-analyze) call `deactivate_class_node`
1371            // first; that flips `active=false`, defeating this guard so the
1372            // setters run as before.
1373            if node.active(self)
1374                && node.is_interface(self) == is_interface
1375                && node.is_trait(self) == is_trait
1376                && node.is_enum(self) == is_enum
1377                && node.is_abstract(self) == is_abstract
1378                && node.is_backed_enum(self) == is_backed_enum
1379                && node.parent(self) == parent
1380                && *node.interfaces(self) == *interfaces
1381                && *node.traits(self) == *traits
1382                && *node.extends(self) == *extends
1383                && *node.template_params(self) == *template_params
1384                && *node.require_extends(self) == *require_extends
1385                && *node.require_implements(self) == *require_implements
1386                && *node.mixins(self) == *mixins
1387                && node.deprecated(self) == deprecated
1388                && node.enum_scalar_type(self) == enum_scalar_type
1389                && node.is_final(self) == is_final
1390                && node.is_readonly(self) == is_readonly
1391                && node.location(self) == location
1392                && *node.extends_type_args(self) == *extends_type_args
1393                && *node.implements_type_args(self) == *implements_type_args
1394            {
1395                return node;
1396            }
1397            node.set_active(self).to(true);
1398            node.set_is_interface(self).to(is_interface);
1399            node.set_is_trait(self).to(is_trait);
1400            node.set_is_enum(self).to(is_enum);
1401            node.set_is_abstract(self).to(is_abstract);
1402            node.set_parent(self).to(parent);
1403            node.set_interfaces(self).to(interfaces);
1404            node.set_traits(self).to(traits);
1405            node.set_extends(self).to(extends);
1406            node.set_template_params(self).to(template_params);
1407            node.set_require_extends(self).to(require_extends);
1408            node.set_require_implements(self).to(require_implements);
1409            node.set_is_backed_enum(self).to(is_backed_enum);
1410            node.set_mixins(self).to(mixins);
1411            node.set_deprecated(self).to(deprecated);
1412            node.set_enum_scalar_type(self).to(enum_scalar_type);
1413            node.set_is_final(self).to(is_final);
1414            node.set_is_readonly(self).to(is_readonly);
1415            node.set_location(self).to(location);
1416            node.set_extends_type_args(self).to(extends_type_args);
1417            node.set_implements_type_args(self).to(implements_type_args);
1418            node
1419        } else {
1420            let node = ClassNode::new(
1421                self,
1422                fqcn.clone(),
1423                true,
1424                is_interface,
1425                is_trait,
1426                is_enum,
1427                is_abstract,
1428                parent,
1429                interfaces,
1430                traits,
1431                extends,
1432                template_params,
1433                require_extends,
1434                require_implements,
1435                is_backed_enum,
1436                mixins,
1437                deprecated,
1438                enum_scalar_type,
1439                is_final,
1440                is_readonly,
1441                location,
1442                extends_type_args,
1443                implements_type_args,
1444            );
1445            self.class_nodes.insert(fqcn, node);
1446            node
1447        }
1448    }
1449
1450    /// Mark the `ClassNode` for `fqcn` as inactive.
1451    ///
1452    /// Dependent `class_ancestors` queries will observe the change and re-run,
1453    /// returning an empty list.
1454    pub fn deactivate_class_node(&mut self, fqcn: &str) {
1455        use salsa::Setter as _;
1456        if let Some(&node) = self.class_nodes.get(fqcn) {
1457            node.set_active(self).to(false);
1458        }
1459    }
1460
1461    /// Create or update the `FunctionNode` for the given `FunctionStorage`.
1462    pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
1463        use salsa::Setter as _;
1464        let fqn = &storage.fqn;
1465        if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1466            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1467            // `inferred_return_type` is intentionally NOT compared / written:
1468            // it is owned by the priming sweep's serial commit phase
1469            // (`commit_inferred_return_types`) and Pass-1 re-ingest must not
1470            // clobber a previously-inferred value.
1471            if node.active(self)
1472                && node.short_name(self) == storage.short_name
1473                && node.is_pure(self) == storage.is_pure
1474                && node.deprecated(self) == storage.deprecated
1475                && node.return_type(self) == storage.return_type
1476                && node.location(self) == storage.location
1477                && *node.params(self) == *storage.params.as_slice()
1478                && *node.template_params(self) == *storage.template_params.as_slice()
1479                && *node.assertions(self) == *storage.assertions.as_slice()
1480                && *node.throws(self) == *storage.throws.as_slice()
1481            {
1482                return node;
1483            }
1484            node.set_active(self).to(true);
1485            node.set_short_name(self).to(storage.short_name.clone());
1486            node.set_params(self)
1487                .to(Arc::from(storage.params.as_slice()));
1488            node.set_return_type(self).to(storage.return_type.clone());
1489            node.set_template_params(self)
1490                .to(Arc::from(storage.template_params.as_slice()));
1491            node.set_assertions(self)
1492                .to(Arc::from(storage.assertions.as_slice()));
1493            node.set_throws(self)
1494                .to(Arc::from(storage.throws.as_slice()));
1495            node.set_deprecated(self).to(storage.deprecated.clone());
1496            node.set_is_pure(self).to(storage.is_pure);
1497            node.set_location(self).to(storage.location.clone());
1498            node
1499        } else {
1500            let node = FunctionNode::new(
1501                self,
1502                fqn.clone(),
1503                storage.short_name.clone(),
1504                true,
1505                Arc::from(storage.params.as_slice()),
1506                storage.return_type.clone(),
1507                storage.inferred_return_type.clone(),
1508                Arc::from(storage.template_params.as_slice()),
1509                Arc::from(storage.assertions.as_slice()),
1510                Arc::from(storage.throws.as_slice()),
1511                storage.deprecated.clone(),
1512                storage.is_pure,
1513                storage.location.clone(),
1514            );
1515            self.function_nodes.insert(fqn.clone(), node);
1516            node
1517        }
1518    }
1519
1520    /// Commit a parallel-sweep-collected [`InferredReturnTypes`] buffer
1521    /// into the Salsa db.  **Must be called serially**, after all rayon
1522    /// workers from the priming sweep have dropped their db clones, so
1523    /// that `Storage::cancel_others` sees strong-count==1 inside the
1524    /// setter.  Calling this from inside a `for_each_with` / `map_with`
1525    /// closure will deadlock.
1526    ///
1527    /// Skips writes whose value already matches the current Salsa-tracked
1528    /// value (preserves PR21's fast-skip semantics).  Skips inactive
1529    /// nodes — there's no point committing an inferred return for a node
1530    /// that has been deactivated by a re-analyze.
1531    pub fn commit_inferred_return_types(&mut self, buf: &InferredReturnTypes) {
1532        use salsa::Setter as _;
1533        let funcs = std::mem::take(&mut *buf.functions.lock().expect("inferred buffer poisoned"));
1534        for (fqn, inferred) in funcs {
1535            if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
1536                if !node.active(self) {
1537                    continue;
1538                }
1539                let new = Some(inferred);
1540                if node.inferred_return_type(self) == new {
1541                    continue;
1542                }
1543                node.set_inferred_return_type(self).to(new);
1544            }
1545        }
1546        let methods = std::mem::take(&mut *buf.methods.lock().expect("inferred buffer poisoned"));
1547        for (fqcn, name, inferred) in methods {
1548            let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
1549                name.clone()
1550            } else {
1551                Arc::from(name.to_lowercase().as_str())
1552            };
1553            let node = self
1554                .method_nodes
1555                .get(fqcn.as_ref())
1556                .and_then(|m| m.get(&name_lower))
1557                .copied();
1558            if let Some(node) = node {
1559                if !node.active(self) {
1560                    continue;
1561                }
1562                let new = Some(inferred);
1563                if node.inferred_return_type(self) == new {
1564                    continue;
1565                }
1566                node.set_inferred_return_type(self).to(new);
1567            }
1568        }
1569    }
1570
1571    /// Mark the `FunctionNode` for `fqn` as inactive.
1572    pub fn deactivate_function_node(&mut self, fqn: &str) {
1573        use salsa::Setter as _;
1574        if let Some(&node) = self.function_nodes.get(fqn) {
1575            node.set_active(self).to(false);
1576        }
1577    }
1578
1579    /// Create or update the `MethodNode` for `(storage.fqcn, storage.name.to_lowercase())`.
1580    pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
1581        use salsa::Setter as _;
1582        let fqcn = &storage.fqcn;
1583        let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
1584        // Copy the existing handle out to release the immutable borrow before
1585        // calling node.set_*(self), which needs &mut self.
1586        let existing = self
1587            .method_nodes
1588            .get(fqcn.as_ref())
1589            .and_then(|m| m.get(&name_lower))
1590            .copied();
1591        if let Some(node) = existing {
1592            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1593            // `inferred_return_type` intentionally not compared / written here;
1594            // ownership is in the priming-sweep commit phase.
1595            if node.active(self)
1596                && node.visibility(self) == storage.visibility
1597                && node.is_static(self) == storage.is_static
1598                && node.is_abstract(self) == storage.is_abstract
1599                && node.is_final(self) == storage.is_final
1600                && node.is_constructor(self) == storage.is_constructor
1601                && node.is_pure(self) == storage.is_pure
1602                && node.deprecated(self) == storage.deprecated
1603                && node.return_type(self) == storage.return_type
1604                && node.location(self) == storage.location
1605                && *node.params(self) == *storage.params.as_slice()
1606                && *node.template_params(self) == *storage.template_params.as_slice()
1607                && *node.assertions(self) == *storage.assertions.as_slice()
1608                && *node.throws(self) == *storage.throws.as_slice()
1609            {
1610                return node;
1611            }
1612            node.set_active(self).to(true);
1613            node.set_params(self)
1614                .to(Arc::from(storage.params.as_slice()));
1615            node.set_return_type(self).to(storage.return_type.clone());
1616            node.set_template_params(self)
1617                .to(Arc::from(storage.template_params.as_slice()));
1618            node.set_assertions(self)
1619                .to(Arc::from(storage.assertions.as_slice()));
1620            node.set_throws(self)
1621                .to(Arc::from(storage.throws.as_slice()));
1622            node.set_deprecated(self).to(storage.deprecated.clone());
1623            node.set_visibility(self).to(storage.visibility);
1624            node.set_is_static(self).to(storage.is_static);
1625            node.set_is_abstract(self).to(storage.is_abstract);
1626            node.set_is_final(self).to(storage.is_final);
1627            node.set_is_constructor(self).to(storage.is_constructor);
1628            node.set_is_pure(self).to(storage.is_pure);
1629            node.set_location(self).to(storage.location.clone());
1630            node
1631        } else {
1632            // MethodNode::new takes &mut self; insert after it returns.
1633            let node = MethodNode::new(
1634                self,
1635                fqcn.clone(),
1636                storage.name.clone(),
1637                true,
1638                Arc::from(storage.params.as_slice()),
1639                storage.return_type.clone(),
1640                storage.inferred_return_type.clone(),
1641                Arc::from(storage.template_params.as_slice()),
1642                Arc::from(storage.assertions.as_slice()),
1643                Arc::from(storage.throws.as_slice()),
1644                storage.deprecated.clone(),
1645                storage.visibility,
1646                storage.is_static,
1647                storage.is_abstract,
1648                storage.is_final,
1649                storage.is_constructor,
1650                storage.is_pure,
1651                storage.location.clone(),
1652            );
1653            self.method_nodes
1654                .entry(fqcn.clone())
1655                .or_default()
1656                .insert(name_lower, node);
1657            node
1658        }
1659    }
1660
1661    /// Mark all `MethodNode`s owned by `fqcn` as inactive.
1662    pub fn deactivate_class_methods(&mut self, fqcn: &str) {
1663        use salsa::Setter as _;
1664        let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
1665            Some(methods) => methods.values().copied().collect(),
1666            None => return,
1667        };
1668        for node in nodes {
1669            node.set_active(self).to(false);
1670        }
1671    }
1672
1673    /// Deactivate `MethodNode`s for `fqcn` whose lowercased name is not in
1674    /// `keep_lower`.  Used by `ingest_codebase` to prune stale stub methods
1675    /// when a user file shadows a bundled-stub class with a different method
1676    /// set.  Active-only check preserves PR21's fast-skip — already-inactive
1677    /// nodes don't fire a setter.
1678    pub fn prune_class_methods(
1679        &mut self,
1680        fqcn: &str,
1681        keep_lower: &std::collections::HashSet<Arc<str>>,
1682    ) {
1683        use salsa::Setter as _;
1684        let candidates: Vec<MethodNode> = self
1685            .method_nodes
1686            .get(fqcn)
1687            .map(|m| {
1688                m.iter()
1689                    .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
1690                    .map(|(_, n)| *n)
1691                    .collect()
1692            })
1693            .unwrap_or_default();
1694        for node in candidates {
1695            if node.active(self) {
1696                node.set_active(self).to(false);
1697            }
1698        }
1699    }
1700
1701    /// Deactivate `PropertyNode`s for `fqcn` whose name is not in `keep`.
1702    pub fn prune_class_properties(
1703        &mut self,
1704        fqcn: &str,
1705        keep: &std::collections::HashSet<Arc<str>>,
1706    ) {
1707        use salsa::Setter as _;
1708        let candidates: Vec<PropertyNode> = self
1709            .property_nodes
1710            .get(fqcn)
1711            .map(|m| {
1712                m.iter()
1713                    .filter(|(k, _)| !keep.contains(k.as_ref()))
1714                    .map(|(_, n)| *n)
1715                    .collect()
1716            })
1717            .unwrap_or_default();
1718        for node in candidates {
1719            if node.active(self) {
1720                node.set_active(self).to(false);
1721            }
1722        }
1723    }
1724
1725    /// Deactivate `ClassConstantNode`s for `fqcn` whose name is not in `keep`.
1726    pub fn prune_class_constants(
1727        &mut self,
1728        fqcn: &str,
1729        keep: &std::collections::HashSet<Arc<str>>,
1730    ) {
1731        use salsa::Setter as _;
1732        let candidates: Vec<ClassConstantNode> = self
1733            .class_constant_nodes
1734            .get(fqcn)
1735            .map(|m| {
1736                m.iter()
1737                    .filter(|(k, _)| !keep.contains(k.as_ref()))
1738                    .map(|(_, n)| *n)
1739                    .collect()
1740            })
1741            .unwrap_or_default();
1742        for node in candidates {
1743            if node.active(self) {
1744                node.set_active(self).to(false);
1745            }
1746        }
1747    }
1748
1749    /// Create or update the `PropertyNode` for `(storage.fqcn, storage.name)`.
1750    pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
1751        use salsa::Setter as _;
1752        let existing = self
1753            .property_nodes
1754            .get(fqcn.as_ref())
1755            .and_then(|m| m.get(storage.name.as_ref()))
1756            .copied();
1757        if let Some(node) = existing {
1758            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1759            if node.active(self)
1760                && node.visibility(self) == storage.visibility
1761                && node.is_static(self) == storage.is_static
1762                && node.is_readonly(self) == storage.is_readonly
1763                && node.ty(self) == storage.ty
1764                && node.location(self) == storage.location
1765            {
1766                return;
1767            }
1768            node.set_active(self).to(true);
1769            node.set_ty(self).to(storage.ty.clone());
1770            node.set_visibility(self).to(storage.visibility);
1771            node.set_is_static(self).to(storage.is_static);
1772            node.set_is_readonly(self).to(storage.is_readonly);
1773            node.set_location(self).to(storage.location.clone());
1774        } else {
1775            let node = PropertyNode::new(
1776                self,
1777                fqcn.clone(),
1778                storage.name.clone(),
1779                true,
1780                storage.ty.clone(),
1781                storage.visibility,
1782                storage.is_static,
1783                storage.is_readonly,
1784                storage.location.clone(),
1785            );
1786            self.property_nodes
1787                .entry(fqcn.clone())
1788                .or_default()
1789                .insert(storage.name.clone(), node);
1790        }
1791    }
1792
1793    /// Mark all `PropertyNode`s owned by `fqcn` as inactive.
1794    pub fn deactivate_class_properties(&mut self, fqcn: &str) {
1795        use salsa::Setter as _;
1796        let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
1797            Some(props) => props.values().copied().collect(),
1798            None => return,
1799        };
1800        for node in nodes {
1801            node.set_active(self).to(false);
1802        }
1803    }
1804
1805    /// Create or update the `ClassConstantNode` for `(fqcn, storage.name)`.
1806    pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
1807        use salsa::Setter as _;
1808        let existing = self
1809            .class_constant_nodes
1810            .get(fqcn.as_ref())
1811            .and_then(|m| m.get(storage.name.as_ref()))
1812            .copied();
1813        if let Some(node) = existing {
1814            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1815            if node.active(self)
1816                && node.visibility(self) == storage.visibility
1817                && node.is_final(self) == storage.is_final
1818                && node.ty(self) == storage.ty
1819                && node.location(self) == storage.location
1820            {
1821                return;
1822            }
1823            node.set_active(self).to(true);
1824            node.set_ty(self).to(storage.ty.clone());
1825            node.set_visibility(self).to(storage.visibility);
1826            node.set_is_final(self).to(storage.is_final);
1827            node.set_location(self).to(storage.location.clone());
1828        } else {
1829            let node = ClassConstantNode::new(
1830                self,
1831                fqcn.clone(),
1832                storage.name.clone(),
1833                true,
1834                storage.ty.clone(),
1835                storage.visibility,
1836                storage.is_final,
1837                storage.location.clone(),
1838            );
1839            self.class_constant_nodes
1840                .entry(fqcn.clone())
1841                .or_default()
1842                .insert(storage.name.clone(), node);
1843        }
1844    }
1845
1846    /// Walk every entry in `codebase` and upsert the corresponding db node.
1847    ///
1848    /// Used after bundled / user stubs are loaded into `Codebase` so that
1849    /// `type_exists_via_db` / `class_kind_via_db` / `class_template_params_via_db`
1850    /// see them too.  Idempotent — re-running upserts existing nodes in place
1851    /// without invalidating downstream queries when fields are unchanged.
1852    pub fn ingest_codebase(&mut self, codebase: &Codebase) {
1853        use std::collections::HashSet;
1854        for entry in codebase.classes.iter() {
1855            let cls = entry.value();
1856            self.upsert_class_node(ClassNodeFields {
1857                is_abstract: cls.is_abstract,
1858                parent: cls.parent.clone(),
1859                interfaces: Arc::from(cls.interfaces.as_slice()),
1860                traits: Arc::from(cls.traits.as_slice()),
1861                template_params: Arc::from(cls.template_params.as_slice()),
1862                mixins: Arc::from(cls.mixins.as_slice()),
1863                deprecated: cls.deprecated.clone(),
1864                is_final: cls.is_final,
1865                is_readonly: cls.is_readonly,
1866                location: cls.location.clone(),
1867                extends_type_args: Arc::from(cls.extends_type_args.as_slice()),
1868                implements_type_args: Arc::from(
1869                    cls.implements_type_args
1870                        .iter()
1871                        .map(|(iface, args)| (iface.clone(), Arc::from(args.as_slice())))
1872                        .collect::<Vec<_>>(),
1873                ),
1874                ..ClassNodeFields::for_class(cls.fqcn.clone())
1875            });
1876            let method_keep: HashSet<Arc<str>> = cls
1877                .own_methods
1878                .values()
1879                .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1880                .collect();
1881            self.prune_class_methods(&cls.fqcn, &method_keep);
1882            for method in cls.own_methods.values() {
1883                self.upsert_method_node(method.as_ref());
1884            }
1885            let prop_keep: HashSet<Arc<str>> = cls
1886                .own_properties
1887                .values()
1888                .map(|p| p.name.clone())
1889                .collect();
1890            self.prune_class_properties(&cls.fqcn, &prop_keep);
1891            for prop in cls.own_properties.values() {
1892                self.upsert_property_node(&cls.fqcn, prop);
1893            }
1894            let const_keep: HashSet<Arc<str>> =
1895                cls.own_constants.values().map(|c| c.name.clone()).collect();
1896            self.prune_class_constants(&cls.fqcn, &const_keep);
1897            for constant in cls.own_constants.values() {
1898                self.upsert_class_constant_node(&cls.fqcn, constant);
1899            }
1900        }
1901        for entry in codebase.interfaces.iter() {
1902            let iface = entry.value();
1903            self.upsert_class_node(ClassNodeFields {
1904                extends: Arc::from(iface.extends.as_slice()),
1905                template_params: Arc::from(iface.template_params.as_slice()),
1906                location: iface.location.clone(),
1907                ..ClassNodeFields::for_interface(iface.fqcn.clone())
1908            });
1909            let method_keep: HashSet<Arc<str>> = iface
1910                .own_methods
1911                .values()
1912                .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1913                .collect();
1914            self.prune_class_methods(&iface.fqcn, &method_keep);
1915            for method in iface.own_methods.values() {
1916                self.upsert_method_node(method.as_ref());
1917            }
1918            let const_keep: HashSet<Arc<str>> = iface
1919                .own_constants
1920                .values()
1921                .map(|c| c.name.clone())
1922                .collect();
1923            self.prune_class_constants(&iface.fqcn, &const_keep);
1924            for constant in iface.own_constants.values() {
1925                self.upsert_class_constant_node(&iface.fqcn, constant);
1926            }
1927        }
1928        for entry in codebase.traits.iter() {
1929            let tr = entry.value();
1930            self.upsert_class_node(ClassNodeFields {
1931                traits: Arc::from(tr.traits.as_slice()),
1932                template_params: Arc::from(tr.template_params.as_slice()),
1933                require_extends: Arc::from(tr.require_extends.as_slice()),
1934                require_implements: Arc::from(tr.require_implements.as_slice()),
1935                location: tr.location.clone(),
1936                ..ClassNodeFields::for_trait(tr.fqcn.clone())
1937            });
1938            let method_keep: HashSet<Arc<str>> = tr
1939                .own_methods
1940                .values()
1941                .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1942                .collect();
1943            self.prune_class_methods(&tr.fqcn, &method_keep);
1944            for method in tr.own_methods.values() {
1945                self.upsert_method_node(method.as_ref());
1946            }
1947            let prop_keep: HashSet<Arc<str>> =
1948                tr.own_properties.values().map(|p| p.name.clone()).collect();
1949            self.prune_class_properties(&tr.fqcn, &prop_keep);
1950            for prop in tr.own_properties.values() {
1951                self.upsert_property_node(&tr.fqcn, prop);
1952            }
1953            let const_keep: HashSet<Arc<str>> =
1954                tr.own_constants.values().map(|c| c.name.clone()).collect();
1955            self.prune_class_constants(&tr.fqcn, &const_keep);
1956            for constant in tr.own_constants.values() {
1957                self.upsert_class_constant_node(&tr.fqcn, constant);
1958            }
1959        }
1960        for entry in codebase.enums.iter() {
1961            let en = entry.value();
1962            self.upsert_class_node(ClassNodeFields {
1963                interfaces: Arc::from(en.interfaces.as_slice()),
1964                is_backed_enum: en.scalar_type.is_some(),
1965                enum_scalar_type: en.scalar_type.clone(),
1966                location: en.location.clone(),
1967                ..ClassNodeFields::for_enum(en.fqcn.clone())
1968            });
1969            let mut method_keep: HashSet<Arc<str>> = en
1970                .own_methods
1971                .values()
1972                .map(|m| Arc::<str>::from(m.name.to_lowercase().as_str()))
1973                .collect();
1974            method_keep.insert(Arc::from("cases"));
1975            if en.scalar_type.is_some() {
1976                method_keep.insert(Arc::from("from"));
1977                method_keep.insert(Arc::from("tryfrom"));
1978            }
1979            self.prune_class_methods(&en.fqcn, &method_keep);
1980            for method in en.own_methods.values() {
1981                self.upsert_method_node(method.as_ref());
1982            }
1983            // Synthesize PHP 8.1 implicit enum methods (`cases`, plus `from` /
1984            // `tryFrom` for backed enums) so `lookup_method_node` finds them
1985            // — mirrors the on-the-fly synthesis in `Codebase::get_method`.
1986            // Only register when the user hasn't shadowed the name (PHP forbids
1987            // it but be defensive).
1988            let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
1989                fqcn: en.fqcn.clone(),
1990                name: Arc::from(name),
1991                params: vec![],
1992                return_type: Some(Union::mixed()),
1993                inferred_return_type: None,
1994                visibility: Visibility::Public,
1995                is_static: true,
1996                is_abstract: false,
1997                is_constructor: false,
1998                template_params: vec![],
1999                assertions: vec![],
2000                throws: vec![],
2001                is_final: false,
2002                is_internal: false,
2003                is_pure: false,
2004                deprecated: None,
2005                location: None,
2006            };
2007            let already = |name: &str| {
2008                en.own_methods
2009                    .keys()
2010                    .any(|k| k.as_ref().eq_ignore_ascii_case(name))
2011            };
2012            if !already("cases") {
2013                self.upsert_method_node(&synth_method("cases"));
2014            }
2015            if en.scalar_type.is_some() {
2016                if !already("from") {
2017                    self.upsert_method_node(&synth_method("from"));
2018                }
2019                if !already("tryFrom") {
2020                    self.upsert_method_node(&synth_method("tryFrom"));
2021                }
2022            }
2023            let mut const_keep: HashSet<Arc<str>> =
2024                en.own_constants.values().map(|c| c.name.clone()).collect();
2025            for case in en.cases.values() {
2026                const_keep.insert(case.name.clone());
2027            }
2028            self.prune_class_constants(&en.fqcn, &const_keep);
2029            for constant in en.own_constants.values() {
2030                self.upsert_class_constant_node(&en.fqcn, constant);
2031            }
2032            for case in en.cases.values() {
2033                let case_const = ConstantStorage {
2034                    name: case.name.clone(),
2035                    ty: mir_types::Union::mixed(),
2036                    visibility: None,
2037                    is_final: false,
2038                    location: case.location.clone(),
2039                };
2040                self.upsert_class_constant_node(&en.fqcn, &case_const);
2041            }
2042        }
2043        for entry in codebase.functions.iter() {
2044            self.upsert_function_node(entry.value());
2045        }
2046        for entry in codebase.constants.iter() {
2047            self.upsert_global_constant_node(entry.key().clone(), entry.value().clone());
2048        }
2049    }
2050
2051    /// Create or update the `GlobalConstantNode` for `fqn`.
2052    pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
2053        use salsa::Setter as _;
2054        if let Some(&node) = self.global_constant_nodes.get(&fqn) {
2055            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
2056            if node.active(self) && node.ty(self) == ty {
2057                return node;
2058            }
2059            node.set_active(self).to(true);
2060            node.set_ty(self).to(ty);
2061            node
2062        } else {
2063            let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
2064            self.global_constant_nodes.insert(fqn, node);
2065            node
2066        }
2067    }
2068
2069    /// Mark the `GlobalConstantNode` for `fqn` as inactive.
2070    pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
2071        use salsa::Setter as _;
2072        if let Some(&node) = self.global_constant_nodes.get(fqn) {
2073            node.set_active(self).to(false);
2074        }
2075    }
2076
2077    /// Mark all `ClassConstantNode`s owned by `fqcn` as inactive.
2078    pub fn deactivate_class_constants(&mut self, fqcn: &str) {
2079        use salsa::Setter as _;
2080        let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
2081            Some(consts) => consts.values().copied().collect(),
2082            None => return,
2083        };
2084        for node in nodes {
2085            node.set_active(self).to(false);
2086        }
2087    }
2088}
2089
2090// ---------------------------------------------------------------------------
2091// S4 Step 1: analyze_file accumulators + tracked-query skeleton
2092// ---------------------------------------------------------------------------
2093//
2094// First step toward S4 (issues + reference locations as Salsa accumulators,
2095// `analyze_file` as a tracked query).  This step is purely additive:
2096//
2097//   1. Defines `IssueAccumulator` and `RefLocAccumulator` salsa accumulator
2098//      types — push targets for analyzer-emitted issues and reference-index
2099//      entries during tracked-query evaluation.
2100//   2. Defines `analyze_file` as a tracked-query stub keyed on a
2101//      `(SourceFile, AnalyzeFileInput)` pair.  The stub does NOT perform
2102//      analysis — it accumulates only the parse errors (a strict subset of
2103//      what `collect_file_definitions` already produces, so semantics are
2104//      unchanged).  The full analyzer wiring follows in subsequent S4 PRs.
2105//
2106// Nothing in this module is wired into the batch (`analyze`) or LSP
2107// (`re_analyze_file`) paths yet.  Behavior change: zero.
2108
2109/// Salsa accumulator carrying analyzer-emitted issues.  In the eventual
2110/// S4 design, every site that today calls `IssueBuffer::add` / `Vec::push`
2111/// from inside a tracked query will instead call
2112/// `IssueAccumulator(issue).accumulate(db)`, and `re_analyze_file` will read
2113/// the accumulated issues for the file with
2114/// `analyze_file::accumulated::<IssueAccumulator>(db, file, ...)`.
2115#[salsa::accumulator]
2116#[derive(Clone, Debug)]
2117pub struct IssueAccumulator(pub Issue);
2118
2119/// Reference-index entry as produced during analysis.  Mirrors the tuple
2120/// shape that `Codebase::record_ref` accepts:
2121///
2122/// - `symbol_key`: interner-bound string (`"fn:foo"`, `"cls:Foo"`,
2123///   `"prop:Foo::$bar"`, `"cnst:Foo::BAR"`, `"meth:Foo::bar"` — same keys
2124///   `Codebase::mark_*_referenced_at` use).
2125/// - `file`: the file in which the reference appears.
2126/// - `(line, col_start, col_end)`: span within the file.
2127#[derive(Clone, Debug, PartialEq, Eq)]
2128pub struct RefLoc {
2129    pub symbol_key: Arc<str>,
2130    pub file: Arc<str>,
2131    pub line: u32,
2132    pub col_start: u16,
2133    pub col_end: u16,
2134}
2135
2136/// Salsa accumulator carrying reference-index entries.  In the eventual
2137/// S4 design this replaces the `Codebase::mark_*_referenced_at` side
2138/// effects: instead of mutating the codebase's reference index inside a
2139/// tracked query (which Salsa cannot observe), the analyzer pushes
2140/// `RefLocAccumulator(loc)` and the consumer (LSP / dead-code) reads via
2141/// `analyze_file::accumulated::<RefLocAccumulator>(db, …)`.
2142#[salsa::accumulator]
2143#[derive(Clone, Debug)]
2144pub struct RefLocAccumulator(pub RefLoc);
2145
2146/// Salsa tracked-query input for `analyze_file`.  Carries the analysis
2147/// parameters that aren't already captured by `SourceFile` itself.  Kept
2148/// minimal in this PR; subsequent PRs in the S4 chain will extend it as
2149/// the query body grows to call the full analyzer pipeline.
2150#[salsa::input]
2151pub struct AnalyzeFileInput {
2152    /// Resolved PHP version (`"8.1"`, `"8.2"`, …) used by the analyzer.
2153    /// Mirrors `ProjectAnalyzer::resolved_php_version`.
2154    pub php_version: Arc<str>,
2155}
2156
2157/// Tracked-query skeleton for `analyze_file`.
2158///
2159/// **Current behavior (S4 step 1):** parses the file and emits parse-error
2160/// issues via `IssueAccumulator`.  Does NOT call into Pass 2 / the
2161/// statement / expression analyzer; full body analysis stays in
2162/// `Pass2Driver` until later S4 PRs migrate it.
2163///
2164/// The query exists at this stage to:
2165/// - validate that accumulators compile and accumulate against the
2166///   concrete `MirDb`,
2167/// - give subsequent PRs a stable signature to extend without churning
2168///   the public surface of `db.rs` again,
2169/// - provide a readable test of the accumulator round-trip
2170///   (`accumulate` → `accumulated::<…>(db, …)`).
2171#[salsa::tracked]
2172pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, _input: AnalyzeFileInput) {
2173    use salsa::Accumulator as _;
2174    let path = file.path(db);
2175    let text = file.text(db);
2176
2177    let arena = bumpalo::Bump::new();
2178    let parsed = php_rs_parser::parse(&arena, &text);
2179
2180    for err in &parsed.errors {
2181        let issue = Issue::new(
2182            mir_issues::IssueKind::ParseError {
2183                message: err.to_string(),
2184            },
2185            mir_issues::Location {
2186                file: path.clone(),
2187                line: 1,
2188                line_end: 1,
2189                col_start: 0,
2190                col_end: 0,
2191            },
2192        );
2193        IssueAccumulator(issue).accumulate(db);
2194    }
2195}
2196
2197// ---------------------------------------------------------------------------
2198// Tests
2199// ---------------------------------------------------------------------------
2200
2201#[cfg(test)]
2202mod tests {
2203    use super::*;
2204    use salsa::Setter as _;
2205
2206    fn upsert_class(
2207        db: &mut MirDb,
2208        fqcn: &str,
2209        parent: Option<Arc<str>>,
2210        extends: Arc<[Arc<str>]>,
2211        is_interface: bool,
2212    ) -> ClassNode {
2213        db.upsert_class_node(ClassNodeFields {
2214            is_interface,
2215            parent,
2216            extends,
2217            ..ClassNodeFields::for_class(Arc::from(fqcn))
2218        })
2219    }
2220
2221    #[test]
2222    fn mirdb_constructs() {
2223        let _db = MirDb::default();
2224    }
2225
2226    #[test]
2227    fn source_file_input_roundtrip() {
2228        let db = MirDb::default();
2229        let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
2230        assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
2231        assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
2232    }
2233
2234    #[test]
2235    fn collect_file_definitions_basic() {
2236        let db = MirDb::default();
2237        let src = Arc::from("<?php class Foo {}");
2238        let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
2239        let defs = collect_file_definitions(&db, file);
2240        assert!(defs.issues.is_empty());
2241        assert_eq!(defs.slice.classes.len(), 1);
2242        assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
2243    }
2244
2245    #[test]
2246    fn collect_file_definitions_memoized() {
2247        let db = MirDb::default();
2248        let file = SourceFile::new(
2249            &db,
2250            Arc::from("/tmp/memo.php"),
2251            Arc::from("<?php class Bar {}"),
2252        );
2253
2254        let defs1 = collect_file_definitions(&db, file);
2255        let defs2 = collect_file_definitions(&db, file);
2256        assert!(
2257            Arc::ptr_eq(&defs1.slice, &defs2.slice),
2258            "unchanged file must return the memoized result"
2259        );
2260    }
2261
2262    #[test]
2263    fn analyze_file_accumulates_parse_errors() {
2264        let db = MirDb::default();
2265        // Unterminated string literal — guaranteed parser diagnostic.
2266        let file = SourceFile::new(
2267            &db,
2268            Arc::from("/tmp/parse_err.php"),
2269            Arc::from("<?php $x = \"unterminated"),
2270        );
2271        let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2272        analyze_file(&db, file, input);
2273        let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2274        assert!(
2275            !issues.is_empty(),
2276            "expected parse error to surface as accumulated IssueAccumulator"
2277        );
2278        assert!(matches!(
2279            issues[0].0.kind,
2280            mir_issues::IssueKind::ParseError { .. }
2281        ));
2282    }
2283
2284    #[test]
2285    fn analyze_file_clean_input_accumulates_nothing() {
2286        let db = MirDb::default();
2287        let file = SourceFile::new(
2288            &db,
2289            Arc::from("/tmp/clean.php"),
2290            Arc::from("<?php class Foo {}"),
2291        );
2292        let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
2293        analyze_file(&db, file, input);
2294        let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
2295        let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
2296        assert!(issues.is_empty());
2297        assert!(refs.is_empty());
2298    }
2299
2300    #[test]
2301    fn collect_file_definitions_recomputes_on_change() {
2302        let mut db = MirDb::default();
2303        let file = SourceFile::new(
2304            &db,
2305            Arc::from("/tmp/memo2.php"),
2306            Arc::from("<?php class Foo {}"),
2307        );
2308
2309        let defs1 = collect_file_definitions(&db, file);
2310        file.set_text(&mut db)
2311            .to(Arc::from("<?php class Foo {} class Bar {}"));
2312        let defs2 = collect_file_definitions(&db, file);
2313
2314        assert!(
2315            !Arc::ptr_eq(&defs1.slice, &defs2.slice),
2316            "changed file must produce a new result"
2317        );
2318        assert_eq!(defs2.slice.classes.len(), 2);
2319    }
2320
2321    #[test]
2322    fn class_ancestors_empty_for_root_class() {
2323        let mut db = MirDb::default();
2324        let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2325        let ancestors = class_ancestors(&db, node);
2326        assert!(ancestors.0.is_empty(), "root class has no ancestors");
2327    }
2328
2329    #[test]
2330    fn class_ancestors_single_parent() {
2331        let mut db = MirDb::default();
2332        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2333        let child = upsert_class(
2334            &mut db,
2335            "Child",
2336            Some(Arc::from("Base")),
2337            Arc::from([]),
2338            false,
2339        );
2340        let ancestors = class_ancestors(&db, child);
2341        assert_eq!(ancestors.0.len(), 1);
2342        assert_eq!(ancestors.0[0].as_ref(), "Base");
2343    }
2344
2345    #[test]
2346    fn class_ancestors_transitive() {
2347        let mut db = MirDb::default();
2348        upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
2349        upsert_class(
2350            &mut db,
2351            "Parent",
2352            Some(Arc::from("GrandParent")),
2353            Arc::from([]),
2354            false,
2355        );
2356        let child = upsert_class(
2357            &mut db,
2358            "Child",
2359            Some(Arc::from("Parent")),
2360            Arc::from([]),
2361            false,
2362        );
2363        let ancestors = class_ancestors(&db, child);
2364        assert_eq!(ancestors.0.len(), 2);
2365        assert_eq!(ancestors.0[0].as_ref(), "Parent");
2366        assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
2367    }
2368
2369    #[test]
2370    fn class_ancestors_cycle_returns_empty() {
2371        let mut db = MirDb::default();
2372        // A extends A — not valid PHP, but we must not panic.
2373        let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
2374        let ancestors = class_ancestors(&db, node_a);
2375        // Cycle recovery: empty list (A's ancestors exclude itself).
2376        assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
2377    }
2378
2379    #[test]
2380    fn class_ancestors_inactive_node_returns_empty() {
2381        let mut db = MirDb::default();
2382        let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2383        db.deactivate_class_node("Foo");
2384        let ancestors = class_ancestors(&db, node);
2385        assert!(ancestors.0.is_empty(), "inactive node must yield empty");
2386    }
2387
2388    #[test]
2389    fn class_ancestors_recomputes_on_parent_change() {
2390        let mut db = MirDb::default();
2391        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2392        let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
2393
2394        let before = class_ancestors(&db, child);
2395        assert!(before.0.is_empty());
2396
2397        // Add Base as parent of Child.
2398        child.set_parent(&mut db).to(Some(Arc::from("Base")));
2399
2400        let after = class_ancestors(&db, child);
2401        assert_eq!(after.0.len(), 1);
2402        assert_eq!(after.0[0].as_ref(), "Base");
2403    }
2404
2405    #[test]
2406    fn interface_ancestors_via_extends() {
2407        let mut db = MirDb::default();
2408        upsert_class(&mut db, "Countable", None, Arc::from([]), true);
2409        let child_iface = upsert_class(
2410            &mut db,
2411            "Collection",
2412            None,
2413            Arc::from([Arc::from("Countable")]),
2414            true,
2415        );
2416        let ancestors = class_ancestors(&db, child_iface);
2417        assert_eq!(ancestors.0.len(), 1);
2418        assert_eq!(ancestors.0[0].as_ref(), "Countable");
2419    }
2420
2421    #[test]
2422    fn type_exists_via_db_tracks_active_state() {
2423        let mut db = MirDb::default();
2424        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2425        assert!(type_exists_via_db(&db, "Foo"));
2426        assert!(!type_exists_via_db(&db, "Bar"));
2427        db.deactivate_class_node("Foo");
2428        assert!(!type_exists_via_db(&db, "Foo"));
2429    }
2430
2431    #[test]
2432    fn clone_preserves_class_node_lookups() {
2433        // PR10a: each parallel batch worker gets its own MirDb clone.
2434        // Verify the clone observes the same registered nodes.
2435        let mut db = MirDb::default();
2436        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2437        let cloned = db.clone();
2438        assert!(
2439            type_exists_via_db(&cloned, "Foo"),
2440            "clone must observe nodes registered before clone()"
2441        );
2442        assert!(
2443            !type_exists_via_db(&cloned, "Bar"),
2444            "clone must not observe nodes that were never registered"
2445        );
2446        // Clones must also resolve ancestors through the same shared storage.
2447        let foo_node = cloned.lookup_class_node("Foo").expect("registered");
2448        let ancestors = class_ancestors(&cloned, foo_node);
2449        assert!(ancestors.0.is_empty(), "Foo has no ancestors");
2450    }
2451
2452    // -----------------------------------------------------------------
2453    // Helpers for method-related fixtures
2454    // -----------------------------------------------------------------
2455
2456    fn upsert_class_with_traits(
2457        db: &mut MirDb,
2458        fqcn: &str,
2459        parent: Option<Arc<str>>,
2460        traits: &[&str],
2461        is_interface: bool,
2462        is_trait: bool,
2463    ) -> ClassNode {
2464        db.upsert_class_node(ClassNodeFields {
2465            is_interface,
2466            is_trait,
2467            parent,
2468            traits: Arc::from(
2469                traits
2470                    .iter()
2471                    .map(|t| Arc::<str>::from(*t))
2472                    .collect::<Vec<_>>(),
2473            ),
2474            ..ClassNodeFields::for_class(Arc::from(fqcn))
2475        })
2476    }
2477
2478    fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
2479        let storage = MethodStorage {
2480            name: Arc::from(name),
2481            fqcn: Arc::from(fqcn),
2482            params: vec![],
2483            return_type: None,
2484            inferred_return_type: None,
2485            visibility: Visibility::Public,
2486            is_static: false,
2487            is_abstract,
2488            is_final: false,
2489            is_constructor: name == "__construct",
2490            template_params: vec![],
2491            assertions: vec![],
2492            throws: vec![],
2493            deprecated: None,
2494            is_internal: false,
2495            is_pure: false,
2496            location: None,
2497        };
2498        db.upsert_method_node(&storage)
2499    }
2500
2501    fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
2502        db.upsert_class_node(ClassNodeFields {
2503            interfaces: Arc::from(
2504                interfaces
2505                    .iter()
2506                    .map(|i| Arc::<str>::from(*i))
2507                    .collect::<Vec<_>>(),
2508            ),
2509            is_backed_enum: is_backed,
2510            ..ClassNodeFields::for_enum(Arc::from(fqcn))
2511        })
2512    }
2513
2514    // -----------------------------------------------------------------
2515    // method_exists_via_db
2516    // -----------------------------------------------------------------
2517
2518    #[test]
2519    fn method_exists_via_db_finds_own_method() {
2520        let mut db = MirDb::default();
2521        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2522        upsert_method(&mut db, "Foo", "bar", false);
2523        assert!(method_exists_via_db(&db, "Foo", "bar"));
2524        assert!(!method_exists_via_db(&db, "Foo", "missing"));
2525    }
2526
2527    #[test]
2528    fn method_exists_via_db_walks_parent() {
2529        let mut db = MirDb::default();
2530        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2531        upsert_method(&mut db, "Base", "inherited", false);
2532        upsert_class(
2533            &mut db,
2534            "Child",
2535            Some(Arc::from("Base")),
2536            Arc::from([]),
2537            false,
2538        );
2539        assert!(method_exists_via_db(&db, "Child", "inherited"));
2540    }
2541
2542    #[test]
2543    fn method_exists_via_db_walks_traits_transitively() {
2544        let mut db = MirDb::default();
2545        upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2546        upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
2547        upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2548        upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2549        assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
2550    }
2551
2552    #[test]
2553    fn method_exists_via_db_is_case_insensitive() {
2554        let mut db = MirDb::default();
2555        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2556        upsert_method(&mut db, "Foo", "doStuff", false);
2557        // Stored with original case; lookup must lowercase internally.
2558        assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
2559        assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
2560    }
2561
2562    #[test]
2563    fn method_exists_via_db_unknown_class_returns_false() {
2564        let db = MirDb::default();
2565        assert!(!method_exists_via_db(&db, "Nope", "anything"));
2566    }
2567
2568    #[test]
2569    fn method_exists_via_db_inactive_class_returns_false() {
2570        let mut db = MirDb::default();
2571        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2572        upsert_method(&mut db, "Foo", "bar", false);
2573        db.deactivate_class_node("Foo");
2574        assert!(!method_exists_via_db(&db, "Foo", "bar"));
2575    }
2576
2577    #[test]
2578    fn method_exists_via_db_finds_abstract_methods() {
2579        // Existence-only: abstracts count.  This is the difference vs.
2580        // method_is_concretely_implemented.
2581        let mut db = MirDb::default();
2582        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2583        upsert_method(&mut db, "Foo", "abstr", true);
2584        assert!(method_exists_via_db(&db, "Foo", "abstr"));
2585    }
2586
2587    // -----------------------------------------------------------------
2588    // method_is_concretely_implemented
2589    // -----------------------------------------------------------------
2590
2591    #[test]
2592    fn method_is_concretely_implemented_skips_abstract() {
2593        let mut db = MirDb::default();
2594        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2595        upsert_method(&mut db, "Foo", "abstr", true);
2596        assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
2597    }
2598
2599    #[test]
2600    fn method_is_concretely_implemented_finds_concrete_in_trait() {
2601        let mut db = MirDb::default();
2602        upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
2603        upsert_method(&mut db, "MyTrait", "provided", false);
2604        upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
2605        assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
2606    }
2607
2608    #[test]
2609    fn method_is_concretely_implemented_skips_interface_definitions() {
2610        // Interfaces don't supply implementations, regardless of how
2611        // their methods are stored.
2612        let mut db = MirDb::default();
2613        upsert_class(&mut db, "I", None, Arc::from([]), true);
2614        upsert_method(&mut db, "I", "m", false);
2615        upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
2616        // C "implements" I but has no own implementation.
2617        assert!(!method_is_concretely_implemented(&db, "C", "m"));
2618    }
2619
2620    // -----------------------------------------------------------------
2621    // extends_or_implements_via_db
2622    // -----------------------------------------------------------------
2623
2624    #[test]
2625    fn extends_or_implements_via_db_self_match() {
2626        let mut db = MirDb::default();
2627        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2628        assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
2629    }
2630
2631    #[test]
2632    fn extends_or_implements_via_db_transitive() {
2633        let mut db = MirDb::default();
2634        upsert_class(&mut db, "Animal", None, Arc::from([]), false);
2635        upsert_class(
2636            &mut db,
2637            "Mammal",
2638            Some(Arc::from("Animal")),
2639            Arc::from([]),
2640            false,
2641        );
2642        upsert_class(
2643            &mut db,
2644            "Dog",
2645            Some(Arc::from("Mammal")),
2646            Arc::from([]),
2647            false,
2648        );
2649        assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
2650        assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
2651        assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
2652    }
2653
2654    #[test]
2655    fn extends_or_implements_via_db_unknown_returns_false() {
2656        let db = MirDb::default();
2657        assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
2658    }
2659
2660    #[test]
2661    fn extends_or_implements_via_db_unit_enum_implicit() {
2662        let mut db = MirDb::default();
2663        upsert_enum(&mut db, "Status", &[], false);
2664        assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2665        assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
2666        // Pure enum is NOT a BackedEnum.
2667        assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2668    }
2669
2670    #[test]
2671    fn extends_or_implements_via_db_backed_enum_implicit() {
2672        let mut db = MirDb::default();
2673        upsert_enum(&mut db, "Status", &[], true);
2674        assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
2675        assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
2676        assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
2677    }
2678
2679    #[test]
2680    fn extends_or_implements_via_db_enum_declared_interface() {
2681        let mut db = MirDb::default();
2682        upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
2683        upsert_enum(&mut db, "Status", &["Stringable"], false);
2684        assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
2685    }
2686
2687    // -----------------------------------------------------------------
2688    // has_unknown_ancestor_via_db
2689    // -----------------------------------------------------------------
2690
2691    #[test]
2692    fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
2693        let mut db = MirDb::default();
2694        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2695        upsert_class(
2696            &mut db,
2697            "Child",
2698            Some(Arc::from("Base")),
2699            Arc::from([]),
2700            false,
2701        );
2702        assert!(!has_unknown_ancestor_via_db(&db, "Child"));
2703    }
2704
2705    #[test]
2706    fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
2707        let mut db = MirDb::default();
2708        // Child claims to extend Missing, but Missing isn't registered.
2709        upsert_class(
2710            &mut db,
2711            "Child",
2712            Some(Arc::from("Missing")),
2713            Arc::from([]),
2714            false,
2715        );
2716        assert!(has_unknown_ancestor_via_db(&db, "Child"));
2717    }
2718
2719    #[test]
2720    fn class_template_params_via_db_returns_registered_params() {
2721        use mir_types::Variance;
2722        let mut db = MirDb::default();
2723        let tp = TemplateParam {
2724            name: Arc::from("T"),
2725            bound: None,
2726            defining_entity: Arc::from("Box"),
2727            variance: Variance::Invariant,
2728        };
2729        db.upsert_class_node(ClassNodeFields {
2730            template_params: Arc::from([tp.clone()]),
2731            ..ClassNodeFields::for_class(Arc::from("Box"))
2732        });
2733        let got = class_template_params_via_db(&db, "Box").expect("registered");
2734        assert_eq!(got.len(), 1);
2735        assert_eq!(got[0].name.as_ref(), "T");
2736
2737        assert!(class_template_params_via_db(&db, "Missing").is_none());
2738        db.deactivate_class_node("Box");
2739        assert!(class_template_params_via_db(&db, "Box").is_none());
2740    }
2741
2742    // -----------------------------------------------------------------
2743    // lookup_method_in_chain
2744    // -----------------------------------------------------------------
2745
2746    fn upsert_class_with_mixins(
2747        db: &mut MirDb,
2748        fqcn: &str,
2749        parent: Option<Arc<str>>,
2750        mixins: &[&str],
2751    ) -> ClassNode {
2752        db.upsert_class_node(ClassNodeFields {
2753            parent,
2754            mixins: Arc::from(
2755                mixins
2756                    .iter()
2757                    .map(|m| Arc::<str>::from(*m))
2758                    .collect::<Vec<_>>(),
2759            ),
2760            ..ClassNodeFields::for_class(Arc::from(fqcn))
2761        })
2762    }
2763
2764    #[test]
2765    fn lookup_method_in_chain_finds_own_then_ancestor() {
2766        let mut db = MirDb::default();
2767        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2768        upsert_method(&mut db, "Base", "shared", false);
2769        upsert_class(
2770            &mut db,
2771            "Child",
2772            Some(Arc::from("Base")),
2773            Arc::from([]),
2774            false,
2775        );
2776        upsert_method(&mut db, "Child", "shared", false);
2777        // Own wins over ancestor.
2778        let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
2779        assert_eq!(found.fqcn(&db).as_ref(), "Child");
2780        // Inherited-only resolves to ancestor.
2781        upsert_method(&mut db, "Base", "only_in_base", false);
2782        let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
2783        assert_eq!(found.fqcn(&db).as_ref(), "Base");
2784    }
2785
2786    #[test]
2787    fn lookup_method_in_chain_walks_trait_of_traits() {
2788        let mut db = MirDb::default();
2789        upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
2790        upsert_method(&mut db, "InnerTrait", "deep", false);
2791        upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
2792        upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
2793        let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
2794        assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
2795    }
2796
2797    #[test]
2798    fn lookup_method_in_chain_walks_mixins() {
2799        let mut db = MirDb::default();
2800        upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
2801        upsert_method(&mut db, "MixinTarget", "magic", false);
2802        upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
2803        let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
2804        assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
2805    }
2806
2807    #[test]
2808    fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
2809        let mut db = MirDb::default();
2810        // A → B → A (mutual @mixin); neither defines the method.
2811        upsert_class_with_mixins(&mut db, "A", None, &["B"]);
2812        upsert_class_with_mixins(&mut db, "B", None, &["A"]);
2813        assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
2814    }
2815
2816    #[test]
2817    fn lookup_method_in_chain_is_case_insensitive() {
2818        let mut db = MirDb::default();
2819        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2820        upsert_method(&mut db, "Foo", "doStuff", false);
2821        assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
2822        assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
2823    }
2824
2825    #[test]
2826    fn lookup_method_in_chain_unknown_returns_none() {
2827        let db = MirDb::default();
2828        assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
2829    }
2830
2831    // -----------------------------------------------------------------
2832    // lookup_property_in_chain
2833    // -----------------------------------------------------------------
2834
2835    fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
2836        let storage = PropertyStorage {
2837            name: Arc::from(name),
2838            ty: None,
2839            inferred_ty: None,
2840            visibility: Visibility::Public,
2841            is_static: false,
2842            is_readonly,
2843            default: None,
2844            location: None,
2845        };
2846        let owner = Arc::<str>::from(fqcn);
2847        db.upsert_property_node(&owner, &storage);
2848        db.lookup_property_node(fqcn, name).expect("registered")
2849    }
2850
2851    #[test]
2852    fn lookup_property_in_chain_own_then_ancestor() {
2853        let mut db = MirDb::default();
2854        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2855        upsert_property(&mut db, "Base", "x", false);
2856        upsert_class(
2857            &mut db,
2858            "Child",
2859            Some(Arc::from("Base")),
2860            Arc::from([]),
2861            false,
2862        );
2863        // Inherited resolves to Base.
2864        let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
2865        assert_eq!(found.fqcn(&db).as_ref(), "Base");
2866        // Own override wins.
2867        upsert_property(&mut db, "Child", "x", true);
2868        let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
2869        assert_eq!(found.fqcn(&db).as_ref(), "Child");
2870        assert!(found.is_readonly(&db));
2871    }
2872
2873    #[test]
2874    fn lookup_property_in_chain_walks_mixins() {
2875        let mut db = MirDb::default();
2876        upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
2877        upsert_property(&mut db, "MixinTarget", "exposed", false);
2878        upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
2879        let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
2880        assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
2881    }
2882
2883    #[test]
2884    fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
2885        let mut db = MirDb::default();
2886        upsert_class_with_mixins(&mut db, "A", None, &["B"]);
2887        upsert_class_with_mixins(&mut db, "B", None, &["A"]);
2888        assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
2889    }
2890
2891    #[test]
2892    fn lookup_property_in_chain_is_case_sensitive() {
2893        let mut db = MirDb::default();
2894        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2895        upsert_property(&mut db, "Foo", "myProp", false);
2896        assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
2897        // Property names are case-sensitive in PHP.
2898        assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
2899    }
2900
2901    #[test]
2902    fn lookup_property_in_chain_inactive_returns_none() {
2903        let mut db = MirDb::default();
2904        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2905        upsert_property(&mut db, "Foo", "x", false);
2906        db.deactivate_class_node("Foo");
2907        assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
2908    }
2909
2910    // -----------------------------------------------------------------
2911    // class_constant_exists_in_chain
2912    // -----------------------------------------------------------------
2913
2914    fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
2915        let storage = ConstantStorage {
2916            name: Arc::from(name),
2917            ty: mir_types::Union::mixed(),
2918            visibility: None,
2919            is_final: false,
2920            location: None,
2921        };
2922        let owner = Arc::<str>::from(fqcn);
2923        db.upsert_class_constant_node(&owner, &storage);
2924    }
2925
2926    #[test]
2927    fn class_constant_exists_in_chain_finds_own() {
2928        let mut db = MirDb::default();
2929        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2930        upsert_constant(&mut db, "Foo", "MAX");
2931        assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
2932        assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
2933    }
2934
2935    #[test]
2936    fn class_constant_exists_in_chain_walks_parent() {
2937        let mut db = MirDb::default();
2938        upsert_class(&mut db, "Base", None, Arc::from([]), false);
2939        upsert_constant(&mut db, "Base", "VERSION");
2940        upsert_class(
2941            &mut db,
2942            "Child",
2943            Some(Arc::from("Base")),
2944            Arc::from([]),
2945            false,
2946        );
2947        assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
2948    }
2949
2950    #[test]
2951    fn class_constant_exists_in_chain_walks_interface() {
2952        let mut db = MirDb::default();
2953        upsert_class(&mut db, "I", None, Arc::from([]), true);
2954        upsert_constant(&mut db, "I", "TYPE");
2955        // A class that implements I — interfaces go in the `interfaces`
2956        // slot, not the `extends` slot which is interface-only.
2957        db.upsert_class_node(ClassNodeFields {
2958            interfaces: Arc::from([Arc::from("I")]),
2959            ..ClassNodeFields::for_class(Arc::from("Impl"))
2960        });
2961        assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
2962    }
2963
2964    #[test]
2965    fn class_constant_exists_in_chain_walks_direct_trait() {
2966        let mut db = MirDb::default();
2967        upsert_class_with_traits(&mut db, "T", None, &[], false, true);
2968        upsert_constant(&mut db, "T", "FROM_TRAIT");
2969        upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
2970        assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
2971    }
2972
2973    #[test]
2974    fn class_constant_exists_in_chain_unknown_class_returns_false() {
2975        let db = MirDb::default();
2976        assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
2977    }
2978
2979    #[test]
2980    fn class_constant_exists_in_chain_inactive_returns_false() {
2981        let mut db = MirDb::default();
2982        upsert_class(&mut db, "Foo", None, Arc::from([]), false);
2983        upsert_constant(&mut db, "Foo", "X");
2984        db.deactivate_class_node("Foo");
2985        db.deactivate_class_constants("Foo");
2986        assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
2987    }
2988
2989    /// Validates the S3-deadlock premise.  After `for_each_with` returns,
2990    /// all worker clones must drop so that a subsequent setter on the
2991    /// canonical db (strong-count==1) does not block on
2992    /// `Storage::cancel_others`.  Wrapped in a join-with-timeout so a
2993    /// regression hangs for at most 30s instead of forever.
2994    #[test]
2995    fn parallel_reads_then_serial_write_does_not_deadlock() {
2996        use rayon::prelude::*;
2997        use std::sync::mpsc;
2998        use std::time::Duration;
2999
3000        let (tx, rx) = mpsc::channel::<()>();
3001        std::thread::spawn(move || {
3002            let mut db = MirDb::default();
3003            let storage = mir_codebase::storage::FunctionStorage {
3004                fqn: Arc::from("foo"),
3005                short_name: Arc::from("foo"),
3006                params: vec![],
3007                return_type: None,
3008                inferred_return_type: None,
3009                template_params: vec![],
3010                assertions: vec![],
3011                throws: vec![],
3012                deprecated: None,
3013                is_pure: false,
3014                location: None,
3015            };
3016            let node = db.upsert_function_node(&storage);
3017
3018            // Parallel sweep with cloned dbs; each worker reads via &dyn MirDatabase.
3019            let db_for_sweep = db.clone();
3020            (0..256u32)
3021                .into_par_iter()
3022                .for_each_with(db_for_sweep, |db, _| {
3023                    let _ = node.return_type(&*db as &dyn MirDatabase);
3024                });
3025
3026            // Sweep is done — clones owned by `for_each_with` are dropped.
3027            // If any worker-thread retains thread-local Salsa state pointing
3028            // at a clone, this setter will hang in `Storage::cancel_others`.
3029            node.set_return_type(&mut db).to(Some(Union::mixed()));
3030            assert_eq!(node.return_type(&db), Some(Union::mixed()));
3031            tx.send(()).unwrap();
3032        });
3033
3034        match rx.recv_timeout(Duration::from_secs(30)) {
3035            Ok(()) => {}
3036            Err(_) => {
3037                panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
3038            }
3039        }
3040    }
3041
3042    /// Pins the actual root cause of the original S3 deadlock: a sibling
3043    /// `MirDb` clone (e.g. the `class_db` used by `ClassAnalyzer` in
3044    /// `project.rs`) being alive when a setter runs blocks
3045    /// `Storage::cancel_others` indefinitely.  Dropping the sibling before
3046    /// the setter unblocks it.
3047    ///
3048    /// This is the regression guard for `commit_inferred_return_types`: if
3049    /// a future refactor hoists a clone past the commit point, this test
3050    /// fails (either the "while sibling alive, setter is blocked" half
3051    /// or the "after drop, setter completes" half).
3052    #[test]
3053    fn sibling_clone_blocks_setter_until_dropped() {
3054        use std::sync::mpsc;
3055        use std::time::Duration;
3056
3057        let mut db = MirDb::default();
3058        let storage = mir_codebase::storage::FunctionStorage {
3059            fqn: Arc::from("foo"),
3060            short_name: Arc::from("foo"),
3061            params: vec![],
3062            return_type: None,
3063            inferred_return_type: None,
3064            template_params: vec![],
3065            assertions: vec![],
3066            throws: vec![],
3067            deprecated: None,
3068            is_pure: false,
3069            location: None,
3070        };
3071        let node = db.upsert_function_node(&storage);
3072
3073        let sibling = db.clone();
3074
3075        // Move the writer into a worker thread so we can probe its progress
3076        // without blocking the test.  Channel signals when the setter returns.
3077        let (tx, rx) = mpsc::channel::<()>();
3078        let writer = std::thread::spawn(move || {
3079            node.set_return_type(&mut db).to(Some(Union::mixed()));
3080            tx.send(()).unwrap();
3081        });
3082
3083        // While the sibling clone is alive the setter must NOT make progress —
3084        // strong-count > 1 forces `cancel_others` to wait.
3085        match rx.recv_timeout(Duration::from_millis(500)) {
3086            Err(mpsc::RecvTimeoutError::Timeout) => { /* expected */ }
3087            Ok(()) => panic!(
3088                "setter completed while sibling clone was alive — strong-count==1 \
3089                 invariant of `cancel_others` is broken; commit_inferred_return_types \
3090                 cannot rely on tight-scoping clones"
3091            ),
3092            Err(e) => panic!("unexpected channel error: {e:?}"),
3093        }
3094
3095        // Drop the sibling.  Strong-count drops to 1 and the setter unblocks.
3096        drop(sibling);
3097
3098        match rx.recv_timeout(Duration::from_secs(5)) {
3099            Ok(()) => {}
3100            Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
3101        }
3102        writer.join().expect("writer thread panicked");
3103    }
3104}