Skip to main content

mir_analyzer/db/
mirdb.rs

1use std::collections::{HashMap, HashSet};
2
3use parking_lot::Mutex;
4use rustc_hash::FxHashMap;
5use std::sync::Arc;
6
7use mir_codebase::storage::{
8    ConstantStorage, FunctionStorage, Location, MethodStorage, PropertyStorage, TemplateParam,
9    Visibility,
10};
11use mir_codebase::StubSlice;
12use mir_types::Union;
13
14use super::*;
15
16// MirDb concrete database
17
18/// Concrete in-process Salsa database.
19///
20/// `Clone` is required for parallel batch analysis: salsa's supported
21/// pattern for sharing a db across threads is to give each worker its
22/// own clone (each clone gets a fresh `ZalsaLocal`, sharing the
23/// underlying memoization storage).  Sharing `&MirDb` across threads is
24/// **not** supported because `salsa::Database: Send` (not `Sync`).
25type MemberRegistry<V> = Arc<FxHashMap<Arc<str>, FxHashMap<Arc<str>, V>>>;
26type ReferenceLocations = Arc<Mutex<FxHashMap<Arc<str>, Vec<(Arc<str>, u32, u16, u16)>>>>;
27
28#[salsa::db]
29#[derive(Default, Clone)]
30pub struct MirDb {
31    storage: salsa::Storage<Self>,
32    // Keep registries behind `Arc`s so `MirDb::clone()` stays cheap for
33    // parallel analysis workers. The salsa storage is already shared by clone;
34    // these maps only hold stable input handles, so copy-on-write insertion is
35    // enough for the canonical mutable db paths.
36    /// FQCN → ClassNode handle registry (not tracked by Salsa; see
37    /// `lookup_class_node` for the rationale). Keys are canonical FQCNs;
38    /// case-insensitive lookups go through `class_node_keys_lower`.
39    class_nodes: Arc<FxHashMap<Arc<str>, ClassNode>>,
40    /// Lowercased FQCN → canonical FQCN. Maintained in lockstep with
41    /// `class_nodes` so callers can resolve PHP's case-insensitive class
42    /// names (`new arrayobject()` → `ArrayObject`).
43    class_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
44    /// FQN → FunctionNode handle registry. Keys are canonical FQNs;
45    /// case-insensitive lookups go through `function_node_keys_lower`.
46    function_nodes: Arc<FxHashMap<Arc<str>, FunctionNode>>,
47    /// Lowercased FQN → canonical FQN. Maintained in lockstep with
48    /// `function_nodes` so callers can resolve PHP's case-insensitive
49    /// function names (`STRLEN($x)` → `strlen`).
50    function_node_keys_lower: Arc<FxHashMap<String, Arc<str>>>,
51    /// (owner FQCN) → (method_name_lower → MethodNode) handle registry.
52    method_nodes: MemberRegistry<MethodNode>,
53    /// (owner FQCN) → (prop_name → PropertyNode) handle registry.
54    property_nodes: MemberRegistry<PropertyNode>,
55    /// (owner FQCN) → (const_name → ClassConstantNode) handle registry.
56    class_constant_nodes: MemberRegistry<ClassConstantNode>,
57    /// FQN → GlobalConstantNode handle registry.
58    global_constant_nodes: Arc<FxHashMap<Arc<str>, GlobalConstantNode>>,
59    /// File path → first declared namespace.
60    file_namespaces: Arc<FxHashMap<Arc<str>, Arc<str>>>,
61    /// File path → use-alias imports.
62    file_imports: Arc<FxHashMap<Arc<str>, HashMap<String, String>>>,
63    /// Global variable name (without `$`) → collected type.
64    global_vars: Arc<FxHashMap<Arc<str>, Union>>,
65    /// Symbol FQN → defining file.
66    symbol_to_file: Arc<FxHashMap<Arc<str>, Arc<str>>>,
67    /// Public symbol key → reference locations.
68    reference_locations: ReferenceLocations,
69}
70
71#[salsa::db]
72impl salsa::Database for MirDb {}
73
74#[salsa::db]
75impl MirDatabase for MirDb {
76    fn php_version_str(&self) -> Arc<str> {
77        Arc::from("8.2")
78    }
79
80    fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
81        if let Some(&node) = self.class_nodes.get(fqcn) {
82            return Some(node);
83        }
84        let lower = fqcn.to_ascii_lowercase();
85        let canonical = self.class_node_keys_lower.get(&lower)?;
86        self.class_nodes.get(canonical.as_ref()).copied()
87    }
88
89    fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
90        if let Some(&node) = self.function_nodes.get(fqn) {
91            return Some(node);
92        }
93        let lower = fqn.to_ascii_lowercase();
94        let canonical = self.function_node_keys_lower.get(&lower)?;
95        self.function_nodes.get(canonical.as_ref()).copied()
96    }
97
98    fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
99        self.method_nodes
100            .get(fqcn)
101            .and_then(|m| m.get(method_name_lower).copied())
102    }
103
104    fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
105        self.property_nodes
106            .get(fqcn)
107            .and_then(|m| m.get(prop_name).copied())
108    }
109
110    fn lookup_class_constant_node(
111        &self,
112        fqcn: &str,
113        const_name: &str,
114    ) -> Option<ClassConstantNode> {
115        self.class_constant_nodes
116            .get(fqcn)
117            .and_then(|m| m.get(const_name).copied())
118    }
119
120    fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
121        self.global_constant_nodes.get(fqn).copied()
122    }
123
124    fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
125        self.method_nodes
126            .get(fqcn)
127            .map(|m| m.values().copied().collect())
128            .unwrap_or_default()
129    }
130
131    fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
132        self.property_nodes
133            .get(fqcn)
134            .map(|m| m.values().copied().collect())
135            .unwrap_or_default()
136    }
137
138    fn class_own_constants(&self, fqcn: &str) -> Vec<ClassConstantNode> {
139        self.class_constant_nodes
140            .get(fqcn)
141            .map(|m| m.values().copied().collect())
142            .unwrap_or_default()
143    }
144
145    fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
146        self.class_nodes
147            .iter()
148            .filter_map(|(fqcn, node)| {
149                if node.active(self) {
150                    Some(fqcn.clone())
151                } else {
152                    None
153                }
154            })
155            .collect()
156    }
157
158    fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
159        self.function_nodes
160            .iter()
161            .filter_map(|(fqn, node)| {
162                if node.active(self) {
163                    Some(fqn.clone())
164                } else {
165                    None
166                }
167            })
168            .collect()
169    }
170
171    fn file_namespace(&self, file: &str) -> Option<Arc<str>> {
172        self.file_namespaces.get(file).cloned()
173    }
174
175    fn file_imports(&self, file: &str) -> HashMap<String, String> {
176        self.file_imports.get(file).cloned().unwrap_or_default()
177    }
178
179    fn global_var_type(&self, name: &str) -> Option<Union> {
180        self.global_vars.get(name).cloned()
181    }
182
183    fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)> {
184        self.file_imports
185            .iter()
186            .map(|(file, imports)| (file.clone(), imports.clone()))
187            .collect()
188    }
189
190    fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>> {
191        self.symbol_to_file.get(symbol).cloned()
192    }
193
194    fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>> {
195        self.symbol_to_file
196            .iter()
197            .filter_map(|(sym, defining_file)| {
198                if defining_file.as_ref() == file {
199                    Some(sym.clone())
200                } else {
201                    None
202                }
203            })
204            .collect()
205    }
206
207    fn record_reference_location(&self, loc: RefLoc) {
208        let mut refs = self.reference_locations.lock();
209        let entry = refs.entry(loc.symbol_key).or_default();
210        let tuple = (loc.file, loc.line, loc.col_start, loc.col_end);
211        if !entry.iter().any(|existing| existing == &tuple) {
212            entry.push(tuple);
213        }
214    }
215
216    fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
217        for (symbol, line, col_start, col_end) in locs {
218            self.record_reference_location(RefLoc {
219                symbol_key: Arc::from(symbol.as_str()),
220                file: file.clone(),
221                line: *line,
222                col_start: *col_start,
223                col_end: *col_end,
224            });
225        }
226    }
227
228    fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
229        let refs = self.reference_locations.lock();
230        let mut out = Vec::new();
231        for (symbol, locs) in refs.iter() {
232            for (loc_file, line, col_start, col_end) in locs {
233                if loc_file.as_ref() == file {
234                    out.push((symbol.clone(), *line, *col_start, *col_end));
235                }
236            }
237        }
238        out
239    }
240
241    fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
242        let refs = self.reference_locations.lock();
243        refs.get(symbol).cloned().unwrap_or_default()
244    }
245
246    fn has_reference(&self, symbol: &str) -> bool {
247        let refs = self.reference_locations.lock();
248        refs.get(symbol).is_some_and(|locs| !locs.is_empty())
249    }
250
251    fn clear_file_references(&self, file: &str) {
252        let mut refs = self.reference_locations.lock();
253        for locs in refs.values_mut() {
254            locs.retain(|(loc_file, _, _, _)| loc_file.as_ref() != file);
255        }
256    }
257}
258
259/// Field bag for [`MirDb::upsert_class_node`].  Construct with `..Default::default()`
260/// to fill in the fields that don't apply to your kind (e.g. interfaces leave
261/// `parent`, `traits`, `mixins`, `is_abstract`, etc. at their defaults).
262///
263/// Per-kind constructors (`for_class` / `for_interface` / `for_trait` /
264/// `for_enum`) seed the kind discriminators so the caller only has to populate
265/// kind-specific fields.
266#[derive(Debug, Clone, Default)]
267pub struct ClassNodeFields {
268    pub fqcn: Arc<str>,
269    pub is_interface: bool,
270    pub is_trait: bool,
271    pub is_enum: bool,
272    pub is_abstract: bool,
273    pub parent: Option<Arc<str>>,
274    pub interfaces: Arc<[Arc<str>]>,
275    pub traits: Arc<[Arc<str>]>,
276    pub extends: Arc<[Arc<str>]>,
277    pub template_params: Arc<[TemplateParam]>,
278    pub require_extends: Arc<[Arc<str>]>,
279    pub require_implements: Arc<[Arc<str>]>,
280    pub is_backed_enum: bool,
281    pub mixins: Arc<[Arc<str>]>,
282    pub deprecated: Option<Arc<str>>,
283    pub enum_scalar_type: Option<Union>,
284    pub is_final: bool,
285    pub is_readonly: bool,
286    pub location: Option<Location>,
287    pub extends_type_args: Arc<[Union]>,
288    pub implements_type_args: ImplementsTypeArgs,
289}
290
291impl ClassNodeFields {
292    pub fn for_class(fqcn: Arc<str>) -> Self {
293        Self {
294            fqcn,
295            ..Self::default()
296        }
297    }
298
299    pub fn for_interface(fqcn: Arc<str>) -> Self {
300        Self {
301            fqcn,
302            is_interface: true,
303            ..Self::default()
304        }
305    }
306
307    pub fn for_trait(fqcn: Arc<str>) -> Self {
308        Self {
309            fqcn,
310            is_trait: true,
311            ..Self::default()
312        }
313    }
314
315    pub fn for_enum(fqcn: Arc<str>) -> Self {
316        Self {
317            fqcn,
318            is_enum: true,
319            ..Self::default()
320        }
321    }
322}
323
324impl MirDb {
325    pub fn remove_file_definitions(&mut self, file: &str) {
326        let symbols = self.symbols_defined_in_file(file);
327        for symbol in &symbols {
328            self.deactivate_class_node(symbol);
329            self.deactivate_function_node(symbol);
330            self.deactivate_class_methods(symbol);
331            self.deactivate_class_properties(symbol);
332            self.deactivate_class_constants(symbol);
333            self.deactivate_global_constant_node(symbol);
334        }
335        let symbol_set: HashSet<Arc<str>> = symbols.into_iter().collect();
336        Arc::make_mut(&mut self.symbol_to_file).retain(|sym, defining_file| {
337            defining_file.as_ref() != file && !symbol_set.contains(sym)
338        });
339        Arc::make_mut(&mut self.file_namespaces).retain(|path, _| path.as_ref() != file);
340        Arc::make_mut(&mut self.file_imports).retain(|path, _| path.as_ref() != file);
341        Arc::make_mut(&mut self.global_vars).retain(|name, _| !symbol_set.contains(name));
342        self.clear_file_references(file);
343    }
344
345    pub fn type_count(&self) -> usize {
346        self.class_nodes
347            .values()
348            .filter(|node| node.active(self))
349            .count()
350    }
351
352    pub fn function_count(&self) -> usize {
353        self.function_nodes
354            .values()
355            .filter(|node| node.active(self))
356            .count()
357    }
358
359    pub fn constant_count(&self) -> usize {
360        self.global_constant_nodes
361            .values()
362            .filter(|node| node.active(self))
363            .count()
364    }
365
366    /// Walk one collected [`StubSlice`] and upsert the corresponding db nodes.
367    ///
368    /// This is the canonical post-Pass-1 ingestion path: each file's slice is
369    /// fed in directly, so batch analysis does not need any intermediate
370    /// mutable codebase store between Pass 1 and Pass 2.
371    pub fn ingest_stub_slice(&mut self, slice: &StubSlice) {
372        use std::collections::HashSet;
373
374        // Deduplicate param lists to save memory (many methods share identical
375        // signatures). This reduces cold-start memory usage by ~100-150 MiB
376        // when analyzing vendor code. But the dedup requires owning a clone of
377        // the slice, which is pure overhead for small per-keystroke LSP
378        // ingests (1-2 methods). Skip the dedup + clone for small slices.
379        let total_methods: usize = slice
380            .classes
381            .iter()
382            .map(|c| c.own_methods.len())
383            .sum::<usize>()
384            + slice
385                .interfaces
386                .iter()
387                .map(|i| i.own_methods.len())
388                .sum::<usize>()
389            + slice
390                .traits
391                .iter()
392                .map(|t| t.own_methods.len())
393                .sum::<usize>()
394            + slice
395                .enums
396                .iter()
397                .map(|e| e.own_methods.len())
398                .sum::<usize>()
399            + slice.functions.len();
400
401        let owned_slice;
402        let slice: &StubSlice = if total_methods >= 8 {
403            let mut s = slice.clone();
404            mir_codebase::storage::deduplicate_params_in_slice(&mut s);
405            owned_slice = s;
406            &owned_slice
407        } else {
408            slice
409        };
410
411        if let Some(file) = &slice.file {
412            if let Some(namespace) = &slice.namespace {
413                Arc::make_mut(&mut self.file_namespaces).insert(file.clone(), namespace.clone());
414            }
415            if !slice.imports.is_empty() {
416                Arc::make_mut(&mut self.file_imports).insert(file.clone(), slice.imports.clone());
417            }
418            for (name, _) in &slice.global_vars {
419                let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
420                Arc::make_mut(&mut self.symbol_to_file)
421                    .insert(Arc::from(global_name), file.clone());
422            }
423        }
424        for (name, ty) in &slice.global_vars {
425            let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
426            Arc::make_mut(&mut self.global_vars).insert(Arc::from(global_name), ty.clone());
427        }
428
429        let slice_file = slice.file.clone();
430        for cls in &slice.classes {
431            if let Some(file) = &slice_file {
432                Arc::make_mut(&mut self.symbol_to_file).insert(cls.fqcn.clone(), file.clone());
433            }
434            self.upsert_class_node(ClassNodeFields {
435                is_abstract: cls.is_abstract,
436                parent: cls.parent.clone(),
437                interfaces: Arc::from(cls.interfaces.as_ref()),
438                traits: Arc::from(cls.traits.as_ref()),
439                template_params: Arc::from(cls.template_params.as_ref()),
440                mixins: Arc::from(cls.mixins.as_ref()),
441                deprecated: cls.deprecated.clone(),
442                is_final: cls.is_final,
443                is_readonly: cls.is_readonly,
444                location: cls.location.clone(),
445                extends_type_args: Arc::from(cls.extends_type_args.as_ref()),
446                implements_type_args: Arc::from(
447                    cls.implements_type_args
448                        .iter()
449                        .map(|(iface, args)| (iface.clone(), Arc::from(args.as_ref())))
450                        .collect::<Vec<_>>(),
451                ),
452                ..ClassNodeFields::for_class(cls.fqcn.clone())
453            });
454            if self.method_nodes.contains_key(cls.fqcn.as_ref()) {
455                let method_keep: HashSet<&str> =
456                    cls.own_methods.keys().map(|m| m.as_ref()).collect();
457                self.prune_class_methods(&cls.fqcn, &method_keep);
458            }
459            for method in cls.own_methods.values() {
460                // Avoid cloning complex return type Unions during vendor ingestion
461                // by wrapping in Arc upfront. This is a per-method operation during
462                // vendor type collection (rare after initialization), so the Arc
463                // allocation is amortized.
464                self.upsert_method_node(method.as_ref());
465            }
466            if self.property_nodes.contains_key(cls.fqcn.as_ref()) {
467                let prop_keep: HashSet<&str> =
468                    cls.own_properties.keys().map(|p| p.as_ref()).collect();
469                self.prune_class_properties(&cls.fqcn, &prop_keep);
470            }
471            for prop in cls.own_properties.values() {
472                self.upsert_property_node(&cls.fqcn, prop);
473            }
474            if self.class_constant_nodes.contains_key(cls.fqcn.as_ref()) {
475                let const_keep: HashSet<&str> =
476                    cls.own_constants.keys().map(|c| c.as_ref()).collect();
477                self.prune_class_constants(&cls.fqcn, &const_keep);
478            }
479            for constant in cls.own_constants.values() {
480                self.upsert_class_constant_node(&cls.fqcn, constant);
481            }
482        }
483
484        for iface in &slice.interfaces {
485            if let Some(file) = &slice_file {
486                Arc::make_mut(&mut self.symbol_to_file).insert(iface.fqcn.clone(), file.clone());
487            }
488            self.upsert_class_node(ClassNodeFields {
489                extends: Arc::from(iface.extends.as_ref()),
490                template_params: Arc::from(iface.template_params.as_ref()),
491                location: iface.location.clone(),
492                ..ClassNodeFields::for_interface(iface.fqcn.clone())
493            });
494            if self.method_nodes.contains_key(iface.fqcn.as_ref()) {
495                let method_keep: HashSet<&str> =
496                    iface.own_methods.keys().map(|m| m.as_ref()).collect();
497                self.prune_class_methods(&iface.fqcn, &method_keep);
498            }
499            for method in iface.own_methods.values() {
500                self.upsert_method_node(method.as_ref());
501            }
502            if self.class_constant_nodes.contains_key(iface.fqcn.as_ref()) {
503                let const_keep: HashSet<&str> =
504                    iface.own_constants.keys().map(|c| c.as_ref()).collect();
505                self.prune_class_constants(&iface.fqcn, &const_keep);
506            }
507            for constant in iface.own_constants.values() {
508                self.upsert_class_constant_node(&iface.fqcn, constant);
509            }
510        }
511
512        for tr in &slice.traits {
513            if let Some(file) = &slice_file {
514                Arc::make_mut(&mut self.symbol_to_file).insert(tr.fqcn.clone(), file.clone());
515            }
516            self.upsert_class_node(ClassNodeFields {
517                traits: Arc::from(tr.traits.as_ref()),
518                template_params: Arc::from(tr.template_params.as_ref()),
519                require_extends: Arc::from(tr.require_extends.as_ref()),
520                require_implements: Arc::from(tr.require_implements.as_ref()),
521                location: tr.location.clone(),
522                ..ClassNodeFields::for_trait(tr.fqcn.clone())
523            });
524            if self.method_nodes.contains_key(tr.fqcn.as_ref()) {
525                let method_keep: HashSet<&str> =
526                    tr.own_methods.keys().map(|m| m.as_ref()).collect();
527                self.prune_class_methods(&tr.fqcn, &method_keep);
528            }
529            for method in tr.own_methods.values() {
530                self.upsert_method_node(method.as_ref());
531            }
532            if self.property_nodes.contains_key(tr.fqcn.as_ref()) {
533                let prop_keep: HashSet<&str> =
534                    tr.own_properties.keys().map(|p| p.as_ref()).collect();
535                self.prune_class_properties(&tr.fqcn, &prop_keep);
536            }
537            for prop in tr.own_properties.values() {
538                self.upsert_property_node(&tr.fqcn, prop);
539            }
540            if self.class_constant_nodes.contains_key(tr.fqcn.as_ref()) {
541                let const_keep: HashSet<&str> =
542                    tr.own_constants.keys().map(|c| c.as_ref()).collect();
543                self.prune_class_constants(&tr.fqcn, &const_keep);
544            }
545            for constant in tr.own_constants.values() {
546                self.upsert_class_constant_node(&tr.fqcn, constant);
547            }
548        }
549
550        for en in &slice.enums {
551            if let Some(file) = &slice_file {
552                Arc::make_mut(&mut self.symbol_to_file).insert(en.fqcn.clone(), file.clone());
553            }
554            self.upsert_class_node(ClassNodeFields {
555                interfaces: Arc::from(en.interfaces.as_ref()),
556                is_backed_enum: en.scalar_type.is_some(),
557                enum_scalar_type: en.scalar_type.clone(),
558                location: en.location.clone(),
559                ..ClassNodeFields::for_enum(en.fqcn.clone())
560            });
561            if self.method_nodes.contains_key(en.fqcn.as_ref()) {
562                let mut method_keep: HashSet<&str> =
563                    en.own_methods.keys().map(|m| m.as_ref()).collect();
564                method_keep.insert("cases");
565                if en.scalar_type.is_some() {
566                    method_keep.insert("from");
567                    method_keep.insert("tryfrom");
568                }
569                self.prune_class_methods(&en.fqcn, &method_keep);
570            }
571            for method in en.own_methods.values() {
572                self.upsert_method_node(method.as_ref());
573            }
574            let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
575                fqcn: en.fqcn.clone(),
576                name: Arc::from(name),
577                params: Arc::from([].as_ref()),
578                return_type: Some(Arc::new(Union::mixed())),
579                inferred_return_type: None,
580                visibility: Visibility::Public,
581                is_static: true,
582                is_abstract: false,
583                is_constructor: false,
584                template_params: vec![],
585                assertions: vec![],
586                throws: vec![],
587                is_final: false,
588                is_internal: false,
589                is_pure: false,
590                deprecated: None,
591                location: None,
592                docstring: None,
593            };
594            let already = |name: &str| {
595                en.own_methods
596                    .keys()
597                    .any(|k| k.as_ref().eq_ignore_ascii_case(name))
598            };
599            if !already("cases") {
600                self.upsert_method_node(&synth_method("cases"));
601            }
602            if en.scalar_type.is_some() {
603                if !already("from") {
604                    self.upsert_method_node(&synth_method("from"));
605                }
606                if !already("tryFrom") {
607                    self.upsert_method_node(&synth_method("tryFrom"));
608                }
609            }
610            if self.class_constant_nodes.contains_key(en.fqcn.as_ref()) {
611                let mut const_keep: HashSet<&str> =
612                    en.own_constants.keys().map(|c| c.as_ref()).collect();
613                for case in en.cases.values() {
614                    const_keep.insert(case.name.as_ref());
615                }
616                self.prune_class_constants(&en.fqcn, &const_keep);
617            }
618            for constant in en.own_constants.values() {
619                self.upsert_class_constant_node(&en.fqcn, constant);
620            }
621            for case in en.cases.values() {
622                let case_const = ConstantStorage {
623                    name: case.name.clone(),
624                    ty: mir_types::Union::mixed(),
625                    visibility: None,
626                    is_final: false,
627                    location: case.location.clone(),
628                };
629                self.upsert_class_constant_node(&en.fqcn, &case_const);
630            }
631        }
632
633        for func in &slice.functions {
634            if let Some(file) = &slice_file {
635                Arc::make_mut(&mut self.symbol_to_file).insert(func.fqn.clone(), file.clone());
636            }
637            self.upsert_function_node(func);
638        }
639        for (fqn, ty) in &slice.constants {
640            self.upsert_global_constant_node(fqn.clone(), ty.clone());
641        }
642    }
643
644    /// Bulk-ingest many stub slices in one call.
645    ///
646    /// Why this exists: when an external `Arc<MirDb>` snapshot is alive (e.g.
647    /// an LSP server holds one for query serving), each `Arc::make_mut` inside
648    /// [`Self::ingest_stub_slice`] forces a copy-on-write clone of the
649    /// underlying `HashMap`. Calling `ingest_stub_slice` N times in sequence
650    /// with the snapshot alive between calls pays one clone *per call* —
651    /// asymptotically O(N × map_size), which becomes pathological at vendor
652    /// scale (~2k+ slices).
653    ///
654    /// Inside this bulk path the snapshot doesn't get refreshed between
655    /// slices, so the first slice's clone establishes a fresh inner `Arc` with
656    /// `strong_count == 1` and every subsequent insert in the batch is O(1).
657    /// Net cost: O(N + map_size) instead of O(N × map_size).
658    ///
659    /// Use this whenever you're about to ingest more than one slice in a row,
660    /// such as in:
661    /// - LSP warm-up over a `composer.lock` worth of vendor files
662    /// - Project-wide reindex
663    /// - Cache hydration on session restart
664    pub fn ingest_stub_slices<'a, I>(&mut self, slices: I)
665    where
666        I: IntoIterator<Item = &'a StubSlice>,
667    {
668        for slice in slices {
669            self.ingest_stub_slice(slice);
670        }
671    }
672
673    /// Create or update the `ClassNode` for `fqcn`.
674    ///
675    /// If a handle already exists, its fields are updated in-place so Salsa
676    /// can track the change.  A new handle is created only on first registration.
677    #[allow(clippy::too_many_arguments)]
678    pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
679        use salsa::Setter as _;
680        let ClassNodeFields {
681            fqcn,
682            is_interface,
683            is_trait,
684            is_enum,
685            is_abstract,
686            parent,
687            interfaces,
688            traits,
689            extends,
690            template_params,
691            require_extends,
692            require_implements,
693            is_backed_enum,
694            mixins,
695            deprecated,
696            enum_scalar_type,
697            is_final,
698            is_readonly,
699            location,
700            extends_type_args,
701            implements_type_args,
702        } = fields;
703        if let Some(&node) = self.class_nodes.get(&fqcn) {
704            // Fast-skip: an already-active node whose Salsa-tracked fields
705            // match the upsert input.  Bulk re-ingest paths
706            // (`ingest_stub_slice` / `lazy_load_*`) call this for every class
707            // on every iteration; without the skip each call fires 13
708            // setters, each acquiring the Salsa write lock.  Schema doesn't
709            // mutate after Pass 1 (Pass 2 only writes `inferred_return_type`),
710            // so an active node with matching fields is by construction up
711            // to date.
712            //
713            // Mutation paths (LSP re-analyze) call `deactivate_class_node`
714            // first; that flips `active=false`, defeating this guard so the
715            // setters run as before.
716            if node.active(self)
717                && node.is_interface(self) == is_interface
718                && node.is_trait(self) == is_trait
719                && node.is_enum(self) == is_enum
720                && node.is_abstract(self) == is_abstract
721                && node.is_backed_enum(self) == is_backed_enum
722                && node.parent(self) == parent
723                && *node.interfaces(self) == *interfaces
724                && *node.traits(self) == *traits
725                && *node.extends(self) == *extends
726                && *node.template_params(self) == *template_params
727                && *node.require_extends(self) == *require_extends
728                && *node.require_implements(self) == *require_implements
729                && *node.mixins(self) == *mixins
730                && node.deprecated(self) == deprecated
731                && node.enum_scalar_type(self) == enum_scalar_type
732                && node.is_final(self) == is_final
733                && node.is_readonly(self) == is_readonly
734                && node.location(self) == location
735                && *node.extends_type_args(self) == *extends_type_args
736                && *node.implements_type_args(self) == *implements_type_args
737            {
738                return node;
739            }
740            node.set_active(self).to(true);
741            node.set_is_interface(self).to(is_interface);
742            node.set_is_trait(self).to(is_trait);
743            node.set_is_enum(self).to(is_enum);
744            node.set_is_abstract(self).to(is_abstract);
745            node.set_parent(self).to(parent);
746            node.set_interfaces(self).to(interfaces);
747            node.set_traits(self).to(traits);
748            node.set_extends(self).to(extends);
749            node.set_template_params(self).to(template_params);
750            node.set_require_extends(self).to(require_extends);
751            node.set_require_implements(self).to(require_implements);
752            node.set_is_backed_enum(self).to(is_backed_enum);
753            node.set_mixins(self).to(mixins);
754            node.set_deprecated(self).to(deprecated);
755            node.set_enum_scalar_type(self).to(enum_scalar_type);
756            node.set_is_final(self).to(is_final);
757            node.set_is_readonly(self).to(is_readonly);
758            node.set_location(self).to(location);
759            node.set_extends_type_args(self).to(extends_type_args);
760            node.set_implements_type_args(self).to(implements_type_args);
761            node
762        } else {
763            let node = ClassNode::new(
764                self,
765                fqcn.clone(),
766                true,
767                is_interface,
768                is_trait,
769                is_enum,
770                is_abstract,
771                parent,
772                interfaces,
773                traits,
774                extends,
775                template_params,
776                require_extends,
777                require_implements,
778                is_backed_enum,
779                mixins,
780                deprecated,
781                enum_scalar_type,
782                is_final,
783                is_readonly,
784                location,
785                extends_type_args,
786                implements_type_args,
787            );
788            Arc::make_mut(&mut self.class_node_keys_lower)
789                .insert(fqcn.to_ascii_lowercase(), fqcn.clone());
790            Arc::make_mut(&mut self.class_nodes).insert(fqcn, node);
791            node
792        }
793    }
794
795    /// Mark the `ClassNode` for `fqcn` as inactive.
796    ///
797    /// Dependent `class_ancestors` queries will observe the change and re-run,
798    /// returning an empty list.
799    pub fn deactivate_class_node(&mut self, fqcn: &str) {
800        use salsa::Setter as _;
801        if let Some(&node) = self.class_nodes.get(fqcn) {
802            node.set_active(self).to(false);
803        }
804    }
805
806    /// Create or update the `FunctionNode` for the given `FunctionStorage`.
807    pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
808        use salsa::Setter as _;
809        let fqn = &storage.fqn;
810        if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
811            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
812            // `inferred_return_type` is intentionally NOT compared / written:
813            // it is owned by the priming sweep's serial commit phase
814            // (`commit_inferred_return_types`) and Pass-1 re-ingest must not
815            // clobber a previously-inferred value.
816            if node.active(self)
817                && node.short_name(self) == storage.short_name
818                && node.is_pure(self) == storage.is_pure
819                && node.deprecated(self) == storage.deprecated
820                && node.return_type(self).as_deref() == storage.return_type.as_deref()
821                && node.location(self) == storage.location
822                && *node.params(self) == *storage.params.as_ref()
823                && *node.template_params(self) == *storage.template_params
824                && *node.assertions(self) == *storage.assertions
825                && *node.throws(self) == *storage.throws
826            {
827                return node;
828            }
829            node.set_active(self).to(true);
830            node.set_short_name(self).to(storage.short_name.clone());
831            node.set_params(self).to(storage.params.clone());
832            node.set_return_type(self).to(storage.return_type.clone());
833            node.set_template_params(self)
834                .to(Arc::from(storage.template_params.as_slice()));
835            node.set_assertions(self)
836                .to(Arc::from(storage.assertions.as_slice()));
837            node.set_throws(self)
838                .to(Arc::from(storage.throws.as_slice()));
839            node.set_deprecated(self).to(storage.deprecated.clone());
840            node.set_docstring(self).to(storage.docstring.clone());
841            node.set_is_pure(self).to(storage.is_pure);
842            node.set_location(self).to(storage.location.clone());
843            node
844        } else {
845            let node = FunctionNode::new(
846                self,
847                fqn.clone(),
848                storage.short_name.clone(),
849                true,
850                storage.params.clone(),
851                storage.return_type.clone(),
852                storage
853                    .inferred_return_type
854                    .as_ref()
855                    .map(|t| Arc::new(t.clone())),
856                Arc::from(storage.template_params.as_slice()),
857                Arc::from(storage.assertions.as_slice()),
858                Arc::from(storage.throws.as_slice()),
859                storage.deprecated.clone(),
860                storage.docstring.clone(),
861                storage.is_pure,
862                storage.location.clone(),
863            );
864            Arc::make_mut(&mut self.function_node_keys_lower)
865                .insert(fqn.to_ascii_lowercase(), fqn.clone());
866            Arc::make_mut(&mut self.function_nodes).insert(fqn.clone(), node);
867            node
868        }
869    }
870
871    /// Commit a parallel-sweep-collected [`InferredReturnTypes`] buffer
872    /// into the Salsa db.  **Must be called serially**, after all rayon
873    /// workers from the priming sweep have dropped their db clones, so
874    /// that `Storage::cancel_others` sees strong-count==1 inside the
875    /// setter.  Calling this from inside a `for_each_with` / `map_with`
876    /// closure will deadlock.
877    ///
878    /// Skips writes whose value already matches the current Salsa-tracked
879    /// value (preserves PR21's fast-skip semantics).  Skips inactive
880    /// nodes — there's no point committing an inferred return for a node
881    /// that has been deactivated by a re-analyze.
882    /// Commit inferred return types collected during the priming sweep.
883    /// Takes ownership of the function and method inferred type vectors.
884    pub fn commit_inferred_return_types(
885        &mut self,
886        functions: Vec<(Arc<str>, mir_types::Union)>,
887        methods: Vec<(Arc<str>, Arc<str>, mir_types::Union)>,
888    ) {
889        use salsa::Setter as _;
890        for (fqn, inferred) in functions {
891            if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
892                if !node.active(self) {
893                    continue;
894                }
895                let new = Some(Arc::new(inferred));
896                if node.inferred_return_type(self) == new {
897                    continue;
898                }
899                node.set_inferred_return_type(self).to(new);
900            }
901        }
902        for (fqcn, name, inferred) in methods {
903            let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
904                name.clone()
905            } else {
906                Arc::from(name.to_lowercase().as_str())
907            };
908            let node = self
909                .method_nodes
910                .get(fqcn.as_ref())
911                .and_then(|m| m.get(&name_lower))
912                .copied();
913            if let Some(node) = node {
914                if !node.active(self) {
915                    continue;
916                }
917                let new = Some(Arc::new(inferred));
918                if node.inferred_return_type(self) == new {
919                    continue;
920                }
921                node.set_inferred_return_type(self).to(new);
922            }
923        }
924    }
925
926    /// Mark the `FunctionNode` for `fqn` as inactive.
927    pub fn deactivate_function_node(&mut self, fqn: &str) {
928        use salsa::Setter as _;
929        if let Some(&node) = self.function_nodes.get(fqn) {
930            node.set_active(self).to(false);
931        }
932    }
933
934    /// Create or update the `MethodNode` for `(storage.fqcn, storage.name.to_lowercase())`.
935    pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
936        use salsa::Setter as _;
937        let fqcn = &storage.fqcn;
938        let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
939        // Copy the existing handle out to release the immutable borrow before
940        // calling node.set_*(self), which needs &mut self.
941        let existing = self
942            .method_nodes
943            .get(fqcn.as_ref())
944            .and_then(|m| m.get(&name_lower))
945            .copied();
946        if let Some(node) = existing {
947            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
948            // `inferred_return_type` intentionally not compared / written here;
949            // ownership is in the priming-sweep commit phase.
950            if node.active(self)
951                && node.visibility(self) == storage.visibility
952                && node.is_static(self) == storage.is_static
953                && node.is_abstract(self) == storage.is_abstract
954                && node.is_final(self) == storage.is_final
955                && node.is_constructor(self) == storage.is_constructor
956                && node.is_pure(self) == storage.is_pure
957                && node.is_internal(self) == storage.is_internal
958                && node.deprecated(self) == storage.deprecated
959                && node.return_type(self).as_deref() == storage.return_type.as_deref()
960                && node.location(self) == storage.location
961                && *node.params(self) == *storage.params.as_ref()
962                && *node.template_params(self) == *storage.template_params
963                && *node.assertions(self) == *storage.assertions
964                && *node.throws(self) == *storage.throws
965            {
966                return node;
967            }
968            node.set_active(self).to(true);
969            node.set_params(self).to(storage.params.clone());
970            node.set_return_type(self).to(storage.return_type.clone());
971            node.set_template_params(self)
972                .to(Arc::from(storage.template_params.as_slice()));
973            node.set_assertions(self)
974                .to(Arc::from(storage.assertions.as_slice()));
975            node.set_throws(self)
976                .to(Arc::from(storage.throws.as_slice()));
977            node.set_deprecated(self).to(storage.deprecated.clone());
978            node.set_docstring(self).to(storage.docstring.clone());
979            node.set_is_internal(self).to(storage.is_internal);
980            node.set_visibility(self).to(storage.visibility);
981            node.set_is_static(self).to(storage.is_static);
982            node.set_is_abstract(self).to(storage.is_abstract);
983            node.set_is_final(self).to(storage.is_final);
984            node.set_is_constructor(self).to(storage.is_constructor);
985            node.set_is_pure(self).to(storage.is_pure);
986            node.set_location(self).to(storage.location.clone());
987            node
988        } else {
989            // MethodNode::new takes &mut self; insert after it returns.
990            let node = MethodNode::new(
991                self,
992                fqcn.clone(),
993                storage.name.clone(),
994                true,
995                storage.params.clone(),
996                storage.return_type.clone(),
997                storage
998                    .inferred_return_type
999                    .as_ref()
1000                    .map(|t| Arc::new(t.clone())),
1001                Arc::from(storage.template_params.as_slice()),
1002                Arc::from(storage.assertions.as_slice()),
1003                Arc::from(storage.throws.as_slice()),
1004                storage.deprecated.clone(),
1005                storage.docstring.clone(),
1006                storage.is_internal,
1007                storage.visibility,
1008                storage.is_static,
1009                storage.is_abstract,
1010                storage.is_final,
1011                storage.is_constructor,
1012                storage.is_pure,
1013                storage.location.clone(),
1014            );
1015            Arc::make_mut(&mut self.method_nodes)
1016                .entry(fqcn.clone())
1017                .or_default()
1018                .insert(name_lower, node);
1019            node
1020        }
1021    }
1022
1023    /// Mark all `MethodNode`s owned by `fqcn` as inactive.
1024    pub fn deactivate_class_methods(&mut self, fqcn: &str) {
1025        use salsa::Setter as _;
1026        let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
1027            Some(methods) => methods.values().copied().collect(),
1028            None => return,
1029        };
1030        for node in nodes {
1031            node.set_active(self).to(false);
1032        }
1033    }
1034
1035    /// Deactivate `MethodNode`s for `fqcn` whose lowercased name is not in
1036    /// `keep_lower`.  Used by `ingest_stub_slice` to prune stale stub methods
1037    /// when a user file shadows a bundled-stub class with a different method
1038    /// set.  Active-only check preserves PR21's fast-skip — already-inactive
1039    /// nodes don't fire a setter.
1040    pub fn prune_class_methods<T>(&mut self, fqcn: &str, keep_lower: &std::collections::HashSet<T>)
1041    where
1042        T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
1043    {
1044        use salsa::Setter as _;
1045        let candidates: Vec<MethodNode> = self
1046            .method_nodes
1047            .get(fqcn)
1048            .map(|m| {
1049                m.iter()
1050                    .filter(|(k, _)| !keep_lower.contains(k.as_ref()))
1051                    .map(|(_, n)| *n)
1052                    .collect()
1053            })
1054            .unwrap_or_default();
1055        for node in candidates {
1056            if node.active(self) {
1057                node.set_active(self).to(false);
1058            }
1059        }
1060    }
1061
1062    /// Deactivate `PropertyNode`s for `fqcn` whose name is not in `keep`.
1063    pub fn prune_class_properties<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
1064    where
1065        T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
1066    {
1067        use salsa::Setter as _;
1068        let candidates: Vec<PropertyNode> = self
1069            .property_nodes
1070            .get(fqcn)
1071            .map(|m| {
1072                m.iter()
1073                    .filter(|(k, _)| !keep.contains(k.as_ref()))
1074                    .map(|(_, n)| *n)
1075                    .collect()
1076            })
1077            .unwrap_or_default();
1078        for node in candidates {
1079            if node.active(self) {
1080                node.set_active(self).to(false);
1081            }
1082        }
1083    }
1084
1085    /// Deactivate `ClassConstantNode`s for `fqcn` whose name is not in `keep`.
1086    pub fn prune_class_constants<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
1087    where
1088        T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
1089    {
1090        use salsa::Setter as _;
1091        let candidates: Vec<ClassConstantNode> = self
1092            .class_constant_nodes
1093            .get(fqcn)
1094            .map(|m| {
1095                m.iter()
1096                    .filter(|(k, _)| !keep.contains(k.as_ref()))
1097                    .map(|(_, n)| *n)
1098                    .collect()
1099            })
1100            .unwrap_or_default();
1101        for node in candidates {
1102            if node.active(self) {
1103                node.set_active(self).to(false);
1104            }
1105        }
1106    }
1107
1108    /// Create or update the `PropertyNode` for `(storage.fqcn, storage.name)`.
1109    pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
1110        use salsa::Setter as _;
1111        let existing = self
1112            .property_nodes
1113            .get(fqcn.as_ref())
1114            .and_then(|m| m.get(storage.name.as_ref()))
1115            .copied();
1116        if let Some(node) = existing {
1117            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1118            if node.active(self)
1119                && node.visibility(self) == storage.visibility
1120                && node.is_static(self) == storage.is_static
1121                && node.is_readonly(self) == storage.is_readonly
1122                && node.ty(self) == storage.ty
1123                && node.location(self) == storage.location
1124            {
1125                return;
1126            }
1127            node.set_active(self).to(true);
1128            node.set_ty(self).to(storage.ty.clone());
1129            node.set_visibility(self).to(storage.visibility);
1130            node.set_is_static(self).to(storage.is_static);
1131            node.set_is_readonly(self).to(storage.is_readonly);
1132            node.set_location(self).to(storage.location.clone());
1133        } else {
1134            let node = PropertyNode::new(
1135                self,
1136                fqcn.clone(),
1137                storage.name.clone(),
1138                true,
1139                storage.ty.clone(),
1140                storage.visibility,
1141                storage.is_static,
1142                storage.is_readonly,
1143                storage.location.clone(),
1144            );
1145            Arc::make_mut(&mut self.property_nodes)
1146                .entry(fqcn.clone())
1147                .or_default()
1148                .insert(storage.name.clone(), node);
1149        }
1150    }
1151
1152    /// Mark all `PropertyNode`s owned by `fqcn` as inactive.
1153    pub fn deactivate_class_properties(&mut self, fqcn: &str) {
1154        use salsa::Setter as _;
1155        let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
1156            Some(props) => props.values().copied().collect(),
1157            None => return,
1158        };
1159        for node in nodes {
1160            node.set_active(self).to(false);
1161        }
1162    }
1163
1164    /// Create or update the `ClassConstantNode` for `(fqcn, storage.name)`.
1165    pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
1166        use salsa::Setter as _;
1167        let existing = self
1168            .class_constant_nodes
1169            .get(fqcn.as_ref())
1170            .and_then(|m| m.get(storage.name.as_ref()))
1171            .copied();
1172        if let Some(node) = existing {
1173            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1174            if node.active(self)
1175                && node.visibility(self) == storage.visibility
1176                && node.is_final(self) == storage.is_final
1177                && node.ty(self) == storage.ty
1178                && node.location(self) == storage.location
1179            {
1180                return;
1181            }
1182            node.set_active(self).to(true);
1183            node.set_ty(self).to(storage.ty.clone());
1184            node.set_visibility(self).to(storage.visibility);
1185            node.set_is_final(self).to(storage.is_final);
1186            node.set_location(self).to(storage.location.clone());
1187        } else {
1188            let node = ClassConstantNode::new(
1189                self,
1190                fqcn.clone(),
1191                storage.name.clone(),
1192                true,
1193                storage.ty.clone(),
1194                storage.visibility,
1195                storage.is_final,
1196                storage.location.clone(),
1197            );
1198            Arc::make_mut(&mut self.class_constant_nodes)
1199                .entry(fqcn.clone())
1200                .or_default()
1201                .insert(storage.name.clone(), node);
1202        }
1203    }
1204
1205    /// Create or update the `GlobalConstantNode` for `fqn`.
1206    pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
1207        use salsa::Setter as _;
1208        if let Some(&node) = self.global_constant_nodes.get(&fqn) {
1209            // Fast-skip identical re-ingest — see `upsert_class_node` for rationale.
1210            if node.active(self) && node.ty(self) == ty {
1211                return node;
1212            }
1213            node.set_active(self).to(true);
1214            node.set_ty(self).to(ty);
1215            node
1216        } else {
1217            let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
1218            Arc::make_mut(&mut self.global_constant_nodes).insert(fqn, node);
1219            node
1220        }
1221    }
1222
1223    /// Mark the `GlobalConstantNode` for `fqn` as inactive.
1224    pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
1225        use salsa::Setter as _;
1226        if let Some(&node) = self.global_constant_nodes.get(fqn) {
1227            node.set_active(self).to(false);
1228        }
1229    }
1230
1231    /// Mark all `ClassConstantNode`s owned by `fqcn` as inactive.
1232    pub fn deactivate_class_constants(&mut self, fqcn: &str) {
1233        use salsa::Setter as _;
1234        let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
1235            Some(consts) => consts.values().copied().collect(),
1236            None => return,
1237        };
1238        for node in nodes {
1239            node.set_active(self).to(false);
1240        }
1241    }
1242}