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