Skip to main content

mir_codebase/
storage.rs

1use std::sync::Arc;
2
3use indexmap::IndexMap;
4use mir_types::{Location, Name, Type};
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7
8// ---------------------------------------------------------------------------
9// Interned common types for deduplication
10// ---------------------------------------------------------------------------
11
12/// Interned Type types for common parameter/property types.
13/// Deduplicates allocations when thousands of parameters share types like `string`, `int`, etc.
14mod interned_types {
15    use super::*;
16    use std::sync::OnceLock;
17
18    fn intern_string() -> Arc<Type> {
19        Arc::new(Type::string())
20    }
21
22    fn intern_int() -> Arc<Type> {
23        Arc::new(Type::int())
24    }
25
26    fn intern_float() -> Arc<Type> {
27        Arc::new(Type::float())
28    }
29
30    fn intern_bool() -> Arc<Type> {
31        Arc::new(Type::bool())
32    }
33
34    fn intern_mixed() -> Arc<Type> {
35        Arc::new(Type::mixed())
36    }
37
38    fn intern_null() -> Arc<Type> {
39        Arc::new(Type::null())
40    }
41
42    fn intern_void() -> Arc<Type> {
43        Arc::new(Type::void())
44    }
45
46    static STRING: OnceLock<Arc<Type>> = OnceLock::new();
47    static INT: OnceLock<Arc<Type>> = OnceLock::new();
48    static FLOAT: OnceLock<Arc<Type>> = OnceLock::new();
49    static BOOL: OnceLock<Arc<Type>> = OnceLock::new();
50    static MIXED: OnceLock<Arc<Type>> = OnceLock::new();
51    static NULL: OnceLock<Arc<Type>> = OnceLock::new();
52    static VOID: OnceLock<Arc<Type>> = OnceLock::new();
53
54    pub fn string() -> Arc<Type> {
55        STRING.get_or_init(intern_string).clone()
56    }
57
58    pub fn int() -> Arc<Type> {
59        INT.get_or_init(intern_int).clone()
60    }
61
62    pub fn float() -> Arc<Type> {
63        FLOAT.get_or_init(intern_float).clone()
64    }
65
66    pub fn bool() -> Arc<Type> {
67        BOOL.get_or_init(intern_bool).clone()
68    }
69
70    pub fn mixed() -> Arc<Type> {
71        MIXED.get_or_init(intern_mixed).clone()
72    }
73
74    pub fn null() -> Arc<Type> {
75        NULL.get_or_init(intern_null).clone()
76    }
77
78    pub fn void() -> Arc<Type> {
79        VOID.get_or_init(intern_void).clone()
80    }
81
82    /// Global content-keyed `Arc<Type>` interner. Any structurally-identical
83    /// Type is shared as a single Arc across the session.
84    ///
85    /// Why: PHP codebases re-declare a small set of type shapes thousands of
86    /// times — `string|null` return types, `int` params, `array<string, mixed>`
87    /// property types. Without interning, each declaration allocates its own
88    /// `Arc<Type>` plus the inline `SmallVec<[Atomic; 2]>` and any boxed
89    /// `Atomic` payloads. With interning, only the first occurrence allocates.
90    ///
91    /// Trade-off: every `intern_or_wrap` call hashes + does one DashMap lookup.
92    /// Hashing a `Type` is cheap (SmallVec, small atomics) — measured cost is
93    /// well below the alloc-savings benefit on real workloads.
94    static GLOBAL_UNION_INTERN: std::sync::OnceLock<dashmap::DashMap<Type, Arc<Type>>> =
95        std::sync::OnceLock::new();
96
97    fn global_intern_table() -> &'static dashmap::DashMap<Type, Arc<Type>> {
98        GLOBAL_UNION_INTERN.get_or_init(dashmap::DashMap::default)
99    }
100
101    /// Try to intern a Type if it matches a common type, otherwise wrap in Arc.
102    pub fn intern_or_wrap(union: Type) -> Arc<Type> {
103        // Fast path 1: single-atomic scalar — covered by `OnceLock` constants.
104        // Avoids any DashMap traffic for the most common case.
105        if union.types.len() == 1 && !union.possibly_undefined && !union.from_docblock {
106            match &union.types[0] {
107                mir_types::Atomic::TString => return string(),
108                mir_types::Atomic::TInt => return int(),
109                mir_types::Atomic::TFloat => return float(),
110                mir_types::Atomic::TBool => return bool(),
111                mir_types::Atomic::TMixed => return mixed(),
112                mir_types::Atomic::TNull => return null(),
113                mir_types::Atomic::TVoid => return void(),
114                _ => {}
115            }
116        }
117        // Fast path 2: empty Type — also a common case (e.g. unresolved
118        // return type). Don't pollute the intern table with these.
119        if union.types.is_empty() {
120            return Arc::new(union);
121        }
122        // Global path: dedup against any previously-seen identical Type.
123        let table = global_intern_table();
124        if let Some(existing) = table.get(&union) {
125            return Arc::clone(existing.value());
126        }
127        let arc = Arc::new(union.clone());
128        // `insert` semantics: if a parallel thread beat us, its Arc wins.
129        // The lookup-before-insert race is benign — both Arcs are content-
130        // equal — but we still want to share the canonical one going forward.
131        match table.entry(union) {
132            dashmap::mapref::entry::Entry::Occupied(o) => Arc::clone(o.get()),
133            dashmap::mapref::entry::Entry::Vacant(v) => {
134                v.insert(Arc::clone(&arc));
135                arc
136            }
137        }
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Shared primitives
143// ---------------------------------------------------------------------------
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
146pub enum Visibility {
147    Public,
148    Protected,
149    Private,
150}
151
152impl Visibility {
153    pub fn is_at_least(&self, required: Visibility) -> bool {
154        *self <= required
155    }
156}
157
158impl std::fmt::Display for Visibility {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            Visibility::Public => write!(f, "public"),
162            Visibility::Protected => write!(f, "protected"),
163            Visibility::Private => write!(f, "private"),
164        }
165    }
166}
167
168fn serialize_template_bound<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
169where
170    S: serde::Serializer,
171{
172    value.as_deref().serialize(serializer)
173}
174
175fn deserialize_template_bound<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
176where
177    D: serde::Deserializer<'de>,
178{
179    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct TemplateParam {
184    pub name: Name,
185    /// Declared upper bound, e.g. `@template T of Traversable`.
186    /// Stored as `Option<Arc<Type>>` so common bounds (e.g. `object`, `mixed`)
187    /// are deduplicated across all template params via the global intern table.
188    #[serde(
189        serialize_with = "serialize_template_bound",
190        deserialize_with = "deserialize_template_bound"
191    )]
192    pub bound: Option<Arc<Type>>,
193    /// The entity (class or function FQN) that declared this template param.
194    pub defining_entity: Name,
195    pub variance: mir_types::Variance,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct FnParam {
200    pub name: Name,
201    /// Parameter type. Stored as `Option<Arc<Type>>` to enable deduplication of
202    /// common types across parameters. Many parameters share types like `string`,
203    /// `int`, `bool`, etc., so interning via Arc saves allocations.
204    #[serde(
205        deserialize_with = "deserialize_param_type",
206        serialize_with = "serialize_param_type"
207    )]
208    pub ty: Option<Arc<Type>>,
209    /// Out-type declared via `@param-out` / `@psalm-param-out`. When set, this
210    /// type is written back to the caller's argument variable after the call
211    /// instead of (or in addition to) the declared in-type.
212    #[serde(
213        default,
214        deserialize_with = "deserialize_param_type",
215        serialize_with = "serialize_param_type"
216    )]
217    pub out_ty: Option<Arc<Type>>,
218    /// Whether this parameter has a default value. During analysis, defaults are
219    /// never used for their value — only for marking parameters as optional.
220    pub has_default: bool,
221    pub is_variadic: bool,
222    pub is_byref: bool,
223    pub is_optional: bool,
224}
225
226impl std::hash::Hash for FnParam {
227    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
228        self.name.hash(state);
229        self.has_default.hash(state);
230        self.is_variadic.hash(state);
231        self.is_byref.hash(state);
232        self.is_optional.hash(state);
233        // Hash the type value (not the Arc pointer) so that two FnParams with
234        // equal types (PartialEq) always produce the same hash, even when they
235        // are backed by different Arc allocations.
236        self.ty.as_deref().hash(state);
237        self.out_ty.as_deref().hash(state);
238    }
239}
240
241// Serde helpers to transparently convert between Option<Type> and Option<Arc<Type>>
242fn deserialize_param_type<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
243where
244    D: serde::Deserializer<'de>,
245{
246    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
247}
248
249fn serialize_param_type<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
250where
251    S: serde::Serializer,
252{
253    let opt = value.as_ref().map(|arc| (**arc).clone());
254    opt.serialize(serializer)
255}
256
257fn deserialize_return_type<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
258where
259    D: serde::Deserializer<'de>,
260{
261    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
262}
263
264fn serialize_return_type<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
265where
266    S: serde::Serializer,
267{
268    let opt = value.as_ref().map(|arc| (**arc).clone());
269    opt.serialize(serializer)
270}
271
272fn deserialize_params<'de, D>(deserializer: D) -> Result<Arc<[FnParam]>, D::Error>
273where
274    D: serde::Deserializer<'de>,
275{
276    Vec::<FnParam>::deserialize(deserializer).map(|v| Arc::from(v.into_boxed_slice()))
277}
278
279fn default_imports() -> Arc<FxHashMap<Name, Name>> {
280    Arc::new(FxHashMap::default())
281}
282
283/// Deserialize imports map. Supports both new (Name-keyed) and legacy
284/// (String-keyed) on-disk formats — older `cache.bin` files have plain
285/// `HashMap<String, String>`. Either way, we intern at load time so the
286/// in-memory representation is always `Arc<FxHashMap<Name, Name>>`.
287fn deserialize_imports<'de, D>(deserializer: D) -> Result<Arc<FxHashMap<Name, Name>>, D::Error>
288where
289    D: serde::Deserializer<'de>,
290{
291    let raw = FxHashMap::<String, String>::deserialize(deserializer)?;
292    let mut out: FxHashMap<Name, Name> =
293        FxHashMap::with_capacity_and_hasher(raw.len(), Default::default());
294    for (k, v) in raw {
295        out.insert(Name::new(&k), Name::new(&v));
296    }
297    Ok(Arc::new(out))
298}
299
300/// Serialize imports as the legacy `HashMap<String, String>` shape so disk
301/// caches written by this version remain compatible with readers that haven't
302/// been recompiled yet (and vice-versa).
303fn serialize_imports<S>(
304    value: &Arc<FxHashMap<Name, Name>>,
305    serializer: S,
306) -> Result<S::Ok, S::Error>
307where
308    S: serde::Serializer,
309{
310    use serde::ser::SerializeMap;
311    let mut map = serializer.serialize_map(Some(value.len()))?;
312    for (k, v) in value.iter() {
313        map.serialize_entry(k.as_str(), v.as_str())?;
314    }
315    map.end()
316}
317
318fn serialize_params<S>(value: &Arc<[FnParam]>, serializer: S) -> Result<S::Ok, S::Error>
319where
320    S: serde::Serializer,
321{
322    value.as_ref().serialize(serializer)
323}
324
325/// Helper to wrap Option<Type> in interned Arc<Type>.
326pub fn wrap_param_type(ty: Option<Type>) -> Option<Arc<Type>> {
327    ty.map(interned_types::intern_or_wrap)
328}
329
330/// Helper to wrap return type Option<Type> in interned Arc<Type>.
331pub fn wrap_return_type(ty: Option<Type>) -> Option<Arc<Type>> {
332    ty.map(interned_types::intern_or_wrap)
333}
334
335/// Helper to wrap a `PropertyDef` type field (`ty`/`inferred_ty`/`default`) in
336/// an interned `Arc<Type>`, deduplicating common property types via the global
337/// pool. See [`PropertyDef`].
338pub fn wrap_property_type(ty: Option<Type>) -> Option<Arc<Type>> {
339    ty.map(interned_types::intern_or_wrap)
340}
341
342/// Helper to wrap a `TemplateParam.bound` in an interned `Arc<Type>`.
343pub fn wrap_template_bound(ty: Option<Type>) -> Option<Arc<Type>> {
344    ty.map(interned_types::intern_or_wrap)
345}
346
347/// Wrap a variable type in an interned `Arc<Type>`. Use instead of
348/// `Arc::new(ty)` at `FlowState::set_var` and parameter-init sites so that
349/// common scalars (string, int, bool, null, mixed) share a static Arc rather
350/// than allocating a fresh one per assignment.
351pub fn wrap_var_type(ty: Type) -> Arc<Type> {
352    interned_types::intern_or_wrap(ty)
353}
354
355// ---------------------------------------------------------------------------
356// Assertion — `@psalm-assert`, `@psalm-assert-if-true`, etc.
357// ---------------------------------------------------------------------------
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360pub enum AssertionKind {
361    Assert,
362    AssertIfTrue,
363    AssertIfFalse,
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
367pub struct Assertion {
368    pub kind: AssertionKind,
369    pub param: Arc<str>,
370    pub ty: Type,
371}
372
373// ---------------------------------------------------------------------------
374// MethodDef
375// ---------------------------------------------------------------------------
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
378pub struct MethodDef {
379    pub name: Arc<str>,
380    pub fqcn: Arc<str>,
381    #[serde(
382        deserialize_with = "deserialize_params",
383        serialize_with = "serialize_params"
384    )]
385    pub params: Arc<[FnParam]>,
386    /// Type from annotation (`@return` / native type hint). `None` means unannotated.
387    /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types
388    /// (e.g., `void`, `string`, `mixed`, `bool`) across thousands of methods.
389    #[serde(
390        deserialize_with = "deserialize_return_type",
391        serialize_with = "serialize_return_type"
392    )]
393    pub return_type: Option<Arc<Type>>,
394    /// Type inferred from body analysis. Stored as `Option<Arc<Type>>` (8 B) rather
395    /// than inline `Option<Type>` (176 B, no niche) — inference is now demand-driven
396    /// via salsa (`inferred_*_return_type_demand`), so this field is a rarely/never
397    /// populated fallback; shrinking it saves ~168 B on every MethodDef.
398    #[serde(
399        deserialize_with = "deserialize_return_type",
400        serialize_with = "serialize_return_type"
401    )]
402    pub inferred_return_type: Option<Arc<Type>>,
403    pub visibility: Visibility,
404    pub is_static: bool,
405    pub is_abstract: bool,
406    pub is_final: bool,
407    pub is_constructor: bool,
408    pub template_params: Vec<TemplateParam>,
409    pub assertions: Vec<Assertion>,
410    pub throws: Vec<Arc<str>>,
411    pub deprecated: Option<Arc<str>>,
412    pub is_internal: bool,
413    pub is_pure: bool,
414    /// `@no-named-arguments` — callers must not use named argument syntax.
415    #[serde(default)]
416    pub no_named_arguments: bool,
417    /// True when the method has the `#[Override]` PHP attribute.
418    #[serde(default)]
419    pub is_override: bool,
420    pub location: Option<Location>,
421    /// Plain-text description from the docblock (text before `@tag` lines).
422    /// Used for hover info.
423    #[serde(default)]
424    pub docstring: Option<Arc<str>>,
425    /// True for methods added via `@method` docblock annotations. Virtual
426    /// methods must not be required as concrete interface implementations.
427    #[serde(default)]
428    pub is_virtual: bool,
429    /// Parameters declared as taint sinks via `@taint-sink <kind> $param`.
430    /// Each entry is `(param_name_without_dollar, sink_kind_string)`.
431    #[serde(default)]
432    pub taint_sink_params: Vec<(Arc<str>, Arc<str>)>,
433    /// `@if-this-is Type` — the resolved constraint a receiver's type must
434    /// satisfy for this method to be callable. `None` when absent.
435    #[serde(default)]
436    pub if_this_is: Option<Arc<Type>>,
437    /// True when the method has `@inheritDoc` / `{@inheritDoc}` in its docblock.
438    /// The analyzer inherits the parent's return type, param types, throws, and
439    /// template params when this method has none of its own.
440    #[serde(default)]
441    pub is_inherit_doc: bool,
442    /// `@psalm-mutation-free` / `@phpstan-mutation-free` — this method must not
443    /// assign to `$this` properties (same constraint as `@psalm-immutable` on the
444    /// class, but scoped to this single method).
445    #[serde(default)]
446    pub is_mutation_free: bool,
447    /// `@psalm-external-mutation-free` — this method must not mutate any objects
448    /// passed as arguments, but is allowed to modify `$this`.
449    #[serde(default)]
450    pub is_external_mutation_free: bool,
451}
452
453impl MethodDef {
454    pub fn effective_return_type(&self) -> Option<&Type> {
455        self.return_type
456            .as_deref()
457            .or(self.inferred_return_type.as_deref())
458    }
459}
460
461// ---------------------------------------------------------------------------
462// PropertyDef
463// ---------------------------------------------------------------------------
464
465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
466pub struct PropertyDef {
467    pub name: Arc<str>,
468    /// Declared/inferred/default types. Stored as `Option<Arc<Type>>` (8 B)
469    /// rather than inline `Option<Type>` (176 B, no niche) and interned via the
470    /// global pool on construction/deserialization — common property types
471    /// (`string`, `int`, a shared class type) dedup to one allocation. Mirrors
472    /// `FnParam::ty`. On-disk format is unchanged (the serde helpers (de)serialize
473    /// the inner `Type` transparently).
474    #[serde(
475        deserialize_with = "deserialize_param_type",
476        serialize_with = "serialize_param_type"
477    )]
478    pub ty: Option<Arc<Type>>,
479    #[serde(
480        deserialize_with = "deserialize_param_type",
481        serialize_with = "serialize_param_type"
482    )]
483    pub inferred_ty: Option<Arc<Type>>,
484    pub visibility: Visibility,
485    pub is_static: bool,
486    pub is_readonly: bool,
487    #[serde(
488        deserialize_with = "deserialize_param_type",
489        serialize_with = "serialize_param_type"
490    )]
491    pub default: Option<Arc<Type>>,
492    pub location: Option<Location>,
493    /// `@deprecated` docblock annotation, if present.
494    #[serde(default)]
495    pub deprecated: Option<Arc<str>>,
496    /// True when the property declares a PHP native type hint (`public int $x`).
497    /// A property typed only via a `@var` docblock (or untyped entirely) is
498    /// `false`: PHP gives such a property an implicit `null` default, so it is
499    /// never "uninitialized" (no MissingConstructor) and accepts `null` on
500    /// assignment regardless of the advisory docblock type.
501    #[serde(default)]
502    pub has_native_type: bool,
503    /// True when this entry was synthesised from a `@property` / `@property-read` /
504    /// `@property-write` docblock tag rather than a real PHP property declaration.
505    /// Such entries describe magic properties accessible via `__get`/`__set` and
506    /// do **not** participate in PHP's inheritance visibility rules.
507    #[serde(default)]
508    pub from_docblock: bool,
509    /// True when `readonly` comes from a native PHP keyword (`readonly` modifier or
510    /// `readonly class`). False when only a `@readonly` docblock annotation is present.
511    /// Distinguishes PHP-enforced read-only from advisory documentation.
512    #[serde(default)]
513    pub has_native_readonly: bool,
514}
515
516// ---------------------------------------------------------------------------
517// ConstantDef
518// ---------------------------------------------------------------------------
519
520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
521pub struct ConstantDef {
522    pub name: Arc<str>,
523    pub ty: Type,
524    pub visibility: Option<Visibility>,
525    #[serde(default)]
526    pub is_final: bool,
527    pub location: Option<Location>,
528    /// `@deprecated` docblock annotation, if present.
529    #[serde(default)]
530    pub deprecated: Option<Arc<str>>,
531}
532
533// ---------------------------------------------------------------------------
534// ClassDef
535// ---------------------------------------------------------------------------
536
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538pub struct ClassDef {
539    pub fqcn: Arc<str>,
540    pub short_name: Arc<str>,
541    pub parent: Option<Arc<str>>,
542    pub interfaces: Vec<Arc<str>>,
543    pub traits: Vec<Arc<str>>,
544    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
545    pub own_properties: IndexMap<Arc<str>, PropertyDef>,
546    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
547    #[serde(default)]
548    pub mixins: Vec<Arc<str>>,
549    pub template_params: Vec<TemplateParam>,
550    /// Type arguments from `@extends ParentClass<T1, T2>` — maps parent's template params to concrete types.
551    pub extends_type_args: Vec<Type>,
552    /// Type arguments from `@implements Interface<T1, T2>`.
553    #[serde(default)]
554    pub implements_type_args: Vec<(Arc<str>, Vec<Type>)>,
555    pub is_abstract: bool,
556    pub is_final: bool,
557    pub is_readonly: bool,
558    pub deprecated: Option<Arc<str>>,
559    pub is_internal: bool,
560    /// Set when the class carries `@psalm-immutable` — non-constructor methods must not
561    /// assign to `$this` properties.
562    #[serde(default)]
563    pub is_immutable: bool,
564    /// Attribute target flags if this class has `#[Attribute]` annotation.
565    /// `None` = not an attribute class. The value is a bitmask of PHP's
566    /// `Attribute::TARGET_*` constants (e.g. `Attribute::TARGET_CLASS = 1`).
567    #[serde(default)]
568    pub attribute_flags: Option<i64>,
569    pub location: Option<Location>,
570    /// Per-`use` statement locations for each used trait: `(fqcn, location)` in
571    /// declaration order, parallel to `traits`.  Absent from older serialized
572    /// slices; defaults to empty.
573    #[serde(default)]
574    pub trait_use_locations: Vec<(Arc<str>, Location)>,
575    /// Type aliases declared on this class via `@psalm-type` / `@phpstan-type`.
576    #[serde(default)]
577    pub type_aliases: FxHashMap<Arc<str>, Type>,
578    /// Raw import-type declarations (`(local_name, original_name, from_class)`) — resolved during finalization.
579    #[serde(default)]
580    pub pending_import_types: Vec<(Arc<str>, Arc<str>, Arc<str>)>,
581    /// Trait precedence exclusions from `insteadof` declarations in this class's `use` blocks.
582    /// Maps method_name_lowercase → list of trait FQCNs whose version of the method is excluded.
583    /// E.g. `use A, B { B::hello insteadof A; }` stores `"hello" → ["A"]`.
584    #[serde(default)]
585    pub trait_insteadof: IndexMap<Arc<str>, Vec<Arc<str>>>,
586    /// Trait method aliases from `as` declarations in this class's `use` blocks.
587    /// Maps new_name_lowercase → (optional_trait_fqcn, original_method_name_lowercase, visibility_override, alias_cased).
588    /// `alias_cased` is the alias name preserving the original PHP casing (for error messages / case checks).
589    /// Visibility is `None` when the `as` clause only renames without changing visibility.
590    /// E.g. `use Base { __construct as __constructBase; }` stores
591    ///   `"__constructbase" → (None, "__construct", None, "__constructBase")`.
592    /// E.g. `use T { foo as private traitFoo; }` stores
593    ///   `"traitfoo" → (None, "foo", Some(Private), "traitFoo")`.
594    #[serde(default)]
595    #[allow(clippy::type_complexity)]
596    pub trait_aliases:
597        FxHashMap<Arc<str>, (Option<Arc<str>>, Arc<str>, Option<Visibility>, Arc<str>)>,
598}
599
600impl ClassDef {
601    pub fn get_method(&self, name: &str) -> Option<&MethodDef> {
602        // PHP method names are case-insensitive; caller should pass lowercase name.
603        // Only searches own_methods — inherited method resolution is done by
604        // `db::lookup_method_in_chain`.
605        self.own_methods.get(name).map(Arc::as_ref).or_else(|| {
606            self.own_methods
607                .iter()
608                .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
609                .map(|(_, v)| v.as_ref())
610        })
611    }
612
613    pub fn get_property(&self, name: &str) -> Option<&PropertyDef> {
614        self.own_properties.get(name)
615    }
616}
617
618// ---------------------------------------------------------------------------
619// InterfaceDef
620// ---------------------------------------------------------------------------
621
622#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
623pub struct InterfaceDef {
624    pub fqcn: Arc<str>,
625    pub short_name: Arc<str>,
626    pub extends: Vec<Arc<str>>,
627    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
628    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
629    pub template_params: Vec<TemplateParam>,
630    pub location: Option<Location>,
631    /// `@deprecated` docblock annotation, if present.
632    #[serde(default)]
633    pub deprecated: Option<Arc<str>>,
634    /// Properties declared via `@property*` docblock annotations on the interface.
635    #[serde(default)]
636    pub own_properties: IndexMap<Arc<str>, PropertyDef>,
637    /// `@seal-properties` / `@psalm-seal-properties` — disallows undeclared property access.
638    #[serde(default)]
639    pub seal_properties: bool,
640}
641
642// ---------------------------------------------------------------------------
643// TraitDef
644// ---------------------------------------------------------------------------
645
646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
647pub struct TraitDef {
648    pub fqcn: Arc<str>,
649    pub short_name: Arc<str>,
650    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
651    pub own_properties: IndexMap<Arc<str>, PropertyDef>,
652    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
653    pub template_params: Vec<TemplateParam>,
654    /// Traits used by this trait (`use OtherTrait;` inside a trait body).
655    pub traits: Vec<Arc<str>>,
656    pub location: Option<Location>,
657    /// `@psalm-require-extends` / `@phpstan-require-extends` — FQCNs that using classes must extend.
658    #[serde(default)]
659    pub require_extends: Vec<Arc<str>>,
660    /// `@psalm-require-implements` / `@phpstan-require-implements` — FQCNs that using classes must implement.
661    #[serde(default)]
662    pub require_implements: Vec<Arc<str>>,
663    /// `@deprecated` docblock annotation, if present.
664    #[serde(default)]
665    pub deprecated: Option<Arc<str>>,
666}
667
668// ---------------------------------------------------------------------------
669// EnumDef
670// ---------------------------------------------------------------------------
671
672#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
673pub struct EnumCaseDef {
674    pub name: Arc<str>,
675    pub value: Option<Type>,
676    pub location: Option<Location>,
677    /// `@deprecated` docblock annotation, if present.
678    #[serde(default)]
679    pub deprecated: Option<Arc<str>>,
680}
681
682#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
683pub struct EnumDef {
684    pub fqcn: Arc<str>,
685    pub short_name: Arc<str>,
686    pub scalar_type: Option<Type>,
687    pub interfaces: Vec<Arc<str>>,
688    pub cases: IndexMap<Arc<str>, EnumCaseDef>,
689    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
690    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
691    pub location: Option<Location>,
692}
693
694// ---------------------------------------------------------------------------
695// FunctionDef
696// ---------------------------------------------------------------------------
697
698#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
699pub struct FunctionDef {
700    pub fqn: Arc<str>,
701    pub short_name: Arc<str>,
702    #[serde(
703        deserialize_with = "deserialize_params",
704        serialize_with = "serialize_params"
705    )]
706    pub params: Arc<[FnParam]>,
707    /// Type from annotation (`@return` / native type hint). `None` means unannotated.
708    /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types.
709    #[serde(
710        deserialize_with = "deserialize_return_type",
711        serialize_with = "serialize_return_type"
712    )]
713    pub return_type: Option<Arc<Type>>,
714    /// See `MethodDef::inferred_return_type` — `Option<Arc<Type>>` (8 B) for the
715    /// same demand-driven-inference reason.
716    #[serde(
717        deserialize_with = "deserialize_return_type",
718        serialize_with = "serialize_return_type"
719    )]
720    pub inferred_return_type: Option<Arc<Type>>,
721    pub template_params: Vec<TemplateParam>,
722    pub assertions: Vec<Assertion>,
723    pub throws: Vec<Arc<str>>,
724    pub deprecated: Option<Arc<str>>,
725    pub is_pure: bool,
726    /// `@no-named-arguments` — callers must not use named argument syntax.
727    #[serde(default)]
728    pub no_named_arguments: bool,
729    pub location: Option<Location>,
730    /// Plain-text description from the docblock (text before `@tag` lines).
731    /// Used for hover info.
732    #[serde(default)]
733    pub docstring: Option<Arc<str>>,
734    /// Parameters declared as taint sinks via `@taint-sink <kind> $param`.
735    /// Each entry is `(param_name_without_dollar, sink_kind_string)`.
736    #[serde(default)]
737    pub taint_sink_params: Vec<(Arc<str>, Arc<str>)>,
738}
739
740impl FunctionDef {
741    pub fn effective_return_type(&self) -> Option<&Type> {
742        self.return_type
743            .as_deref()
744            .or(self.inferred_return_type.as_deref())
745    }
746}
747
748// ---------------------------------------------------------------------------
749// StubSlice — serializable bundle of definitions from one extension's stubs
750// ---------------------------------------------------------------------------
751
752/// A snapshot of all PHP definitions contributed by a single stub file set.
753///
754/// Produced by `mir-stubs-gen` at code-generation time and deserialized at
755/// runtime to ingest definitions into the salsa db via
756/// `MirDatabase::ingest_stub_slice`.
757#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
758pub struct StubSlice {
759    pub classes: Vec<Arc<ClassDef>>,
760    pub interfaces: Vec<Arc<InterfaceDef>>,
761    pub traits: Vec<Arc<TraitDef>>,
762    pub enums: Vec<Arc<EnumDef>>,
763    pub functions: Vec<Arc<FunctionDef>>,
764    #[serde(default)]
765    pub constants: Vec<(Arc<str>, Type)>,
766    /// Source file this slice was collected from. `None` for bundled stub slices
767    /// that were pre-computed and are not tied to a specific on-disk file.
768    #[serde(default)]
769    pub file: Option<Arc<str>>,
770    /// Types of `@var`-annotated global variables collected from this file.
771    /// Populated by `DefinitionCollector`; ingested into the salsa db's
772    /// `global_vars` table by `ingest_stub_slice` when `file` is `Some`.
773    #[serde(default)]
774    pub global_vars: Vec<(Arc<str>, Type)>,
775    /// The first namespace declared in this file (e.g. `"App\\Service"`).
776    /// Populated by `DefinitionCollector`; ingested into the salsa db's
777    /// `file_namespaces` table by `ingest_stub_slice` when `file` is `Some`.
778    #[serde(default)]
779    pub namespace: Option<Arc<str>>,
780    /// `use` alias map for this file: alias → FQCN.
781    ///
782    /// Stored as `Arc<FxHashMap<Name, Name>>` so that `file_imports()`
783    /// returns a cheap Arc clone instead of deep-cloning the map on every
784    /// `resolve_name` call (which fires once per symbol reference in
785    /// Pass 2). `Name` keys/values shrink each entry from ~108 bytes
786    /// (two `String` headers + two heap allocs averaging ~30 chars) to
787    /// 16 bytes (two `Ustr` u64 handles); the global ustr interner holds
788    /// one copy of each unique alias / FQCN string for the whole session.
789    #[serde(
790        deserialize_with = "deserialize_imports",
791        serialize_with = "serialize_imports"
792    )]
793    #[serde(default = "default_imports")]
794    pub imports: Arc<FxHashMap<Name, Name>>,
795    /// Set to `true` after `deduplicate_params_in_slice` has run on this slice.
796    /// `ingest_stub_slice` skips the clone+re-dedup when this flag is set.
797    #[serde(skip)]
798    pub is_deduped: bool,
799}
800
801// ---------------------------------------------------------------------------
802// Param list deduplication
803// ---------------------------------------------------------------------------
804
805use std::sync::Mutex;
806
807type ParamCache = Mutex<FxHashMap<Vec<FnParam>, Arc<[FnParam]>>>;
808
809/// Global cache of canonical Arc<[FnParam]> instances for deduplication.
810/// Shared across all StubSlices to deduplicate vendor code with millions of
811/// methods that often have identical parameter lists.
812static PARAM_DEDUP_CACHE: std::sync::OnceLock<ParamCache> = std::sync::OnceLock::new();
813
814/// Deduplicate parameter lists across all methods and functions in a StubSlice.
815/// Many PHP framework methods share identical parameter lists (e.g., thousands
816/// of `(string $arg, array $opts)` signatures). This function groups identical
817/// param lists globally (across all slices processed so far) and replaces them
818/// with Arc<[FnParam]> pointers to shared allocations.
819///
820/// Expected memory savings: 100–150 MiB on cold start (vendor collection).
821pub fn deduplicate_params_in_slice(slice: &mut StubSlice) {
822    let cache: &ParamCache = PARAM_DEDUP_CACHE.get_or_init(|| Mutex::new(FxHashMap::default()));
823    let mut canonical_params = cache.lock().unwrap();
824
825    let mut deduplicate = |params: &mut Arc<[FnParam]>| {
826        if let Some(existing) = canonical_params.get(params.as_ref()) {
827            *params = existing.clone();
828        } else {
829            canonical_params.insert(params.as_ref().to_vec(), params.clone());
830        }
831    };
832
833    // Deduplicate method params in all classes
834    for cls in &mut slice.classes {
835        for method in Arc::make_mut(cls).own_methods.values_mut() {
836            deduplicate(&mut Arc::make_mut(method).params);
837        }
838    }
839
840    // Deduplicate method params in all interfaces
841    for iface in &mut slice.interfaces {
842        for method in Arc::make_mut(iface).own_methods.values_mut() {
843            deduplicate(&mut Arc::make_mut(method).params);
844        }
845    }
846
847    // Deduplicate method params in all traits
848    for tr in &mut slice.traits {
849        for method in Arc::make_mut(tr).own_methods.values_mut() {
850            deduplicate(&mut Arc::make_mut(method).params);
851        }
852    }
853
854    // Deduplicate method params in all enums
855    for en in &mut slice.enums {
856        for method in Arc::make_mut(en).own_methods.values_mut() {
857            deduplicate(&mut Arc::make_mut(method).params);
858        }
859    }
860
861    // Deduplicate function params
862    for func in &mut slice.functions {
863        deduplicate(&mut Arc::make_mut(func).params);
864    }
865    slice.is_deduped = true;
866}