Skip to main content

mir_codebase/
definitions.rs

1use std::sync::Arc;
2
3/// Insertion-ordered member map keyed by lowercased member name.
4/// FxHash instead of SipHash: member lookup is one of the hottest analyzer
5/// operations and the keys are short trusted identifiers.
6pub type MemberMap<V> = indexmap::IndexMap<std::sync::Arc<str>, V, rustc_hash::FxBuildHasher>;
7use mir_types::{Location, Name, Type};
8use rustc_hash::FxHashMap;
9use serde::{Deserialize, Serialize};
10
11// ---------------------------------------------------------------------------
12// Interned common types for deduplication
13// ---------------------------------------------------------------------------
14
15/// Interned Type types for common parameter/property types.
16/// Deduplicates allocations when thousands of parameters share types like `string`, `int`, etc.
17mod interned_types {
18    use super::*;
19    use std::sync::OnceLock;
20
21    fn intern_string() -> Arc<Type> {
22        Arc::new(Type::string())
23    }
24
25    fn intern_int() -> Arc<Type> {
26        Arc::new(Type::int())
27    }
28
29    fn intern_float() -> Arc<Type> {
30        Arc::new(Type::float())
31    }
32
33    fn intern_bool() -> Arc<Type> {
34        Arc::new(Type::bool())
35    }
36
37    fn intern_mixed() -> Arc<Type> {
38        Arc::new(Type::mixed())
39    }
40
41    fn intern_null() -> Arc<Type> {
42        Arc::new(Type::null())
43    }
44
45    fn intern_void() -> Arc<Type> {
46        Arc::new(Type::void())
47    }
48
49    static STRING: OnceLock<Arc<Type>> = OnceLock::new();
50    static INT: OnceLock<Arc<Type>> = OnceLock::new();
51    static FLOAT: OnceLock<Arc<Type>> = OnceLock::new();
52    static BOOL: OnceLock<Arc<Type>> = OnceLock::new();
53    static MIXED: OnceLock<Arc<Type>> = OnceLock::new();
54    static NULL: OnceLock<Arc<Type>> = OnceLock::new();
55    static VOID: OnceLock<Arc<Type>> = OnceLock::new();
56
57    pub fn string() -> Arc<Type> {
58        STRING.get_or_init(intern_string).clone()
59    }
60
61    pub fn int() -> Arc<Type> {
62        INT.get_or_init(intern_int).clone()
63    }
64
65    pub fn float() -> Arc<Type> {
66        FLOAT.get_or_init(intern_float).clone()
67    }
68
69    pub fn bool() -> Arc<Type> {
70        BOOL.get_or_init(intern_bool).clone()
71    }
72
73    pub fn mixed() -> Arc<Type> {
74        MIXED.get_or_init(intern_mixed).clone()
75    }
76
77    pub fn null() -> Arc<Type> {
78        NULL.get_or_init(intern_null).clone()
79    }
80
81    pub fn void() -> Arc<Type> {
82        VOID.get_or_init(intern_void).clone()
83    }
84
85    /// Global content-keyed `Arc<Type>` interner. Any structurally-identical
86    /// Type is shared as a single Arc across the session.
87    ///
88    /// Why: PHP codebases re-declare a small set of type shapes thousands of
89    /// times — `string|null` return types, `int` params, `array<string, mixed>`
90    /// property types. Without interning, each declaration allocates its own
91    /// `Arc<Type>` plus the inline `SmallVec<[Atomic; 2]>` and any boxed
92    /// `Atomic` payloads. With interning, only the first occurrence allocates.
93    ///
94    /// Trade-off: every `intern_or_wrap` call hashes + does one DashMap lookup.
95    /// Hashing a `Type` is cheap (SmallVec, small atomics) — measured cost is
96    /// well below the alloc-savings benefit on real workloads.
97    type InternTable = dashmap::DashMap<Type, Arc<Type>, rustc_hash::FxBuildHasher>;
98
99    static GLOBAL_UNION_INTERN: std::sync::OnceLock<InternTable> = std::sync::OnceLock::new();
100
101    fn global_intern_table() -> &'static InternTable {
102        GLOBAL_UNION_INTERN.get_or_init(|| dashmap::DashMap::with_hasher(Default::default()))
103    }
104
105    /// The shared `Arc` of a single-atomic scalar `union` without flags.
106    fn scalar_singleton(union: &Type) -> Option<Arc<Type>> {
107        // Excludes `falsy_stripped`/`possibly_absent_offset` types (e.g.
108        // `preg_replace_callback`'s narrowed `string`, or a generic
109        // array-offset read's value type) — the canonical singleton never
110        // carries either flag, so returning it here would silently drop it.
111        if union.types.len() != 1
112            || union.possibly_undefined
113            || union.from_docblock
114            || union.falsy_stripped
115            || union.possibly_absent_offset
116        {
117            return None;
118        }
119        match &union.types[0] {
120            mir_types::Atomic::TString => Some(string()),
121            mir_types::Atomic::TInt => Some(int()),
122            mir_types::Atomic::TFloat => Some(float()),
123            mir_types::Atomic::TBool => Some(bool()),
124            mir_types::Atomic::TMixed => Some(mixed()),
125            mir_types::Atomic::TNull => Some(null()),
126            mir_types::Atomic::TVoid => Some(void()),
127            _ => None,
128        }
129    }
130
131    /// Try to intern a Type if it matches a common type, otherwise wrap in Arc.
132    pub fn intern_or_wrap(union: Type) -> Arc<Type> {
133        if let Some(scalar) = scalar_singleton(&union) {
134            return scalar;
135        }
136        // Empty Type — also a common case (e.g. unresolved return type).
137        // Don't pollute the intern table with these.
138        if union.types.is_empty() {
139            return Arc::new(union);
140        }
141        let hash = {
142            use std::hash::BuildHasher;
143            rustc_hash::FxBuildHasher.hash_one(&union)
144        };
145        let slot = (hash as usize) & (LOCAL_SLOTS - 1);
146        let hit = LOCAL_CACHE.with(|cache| {
147            cache.borrow()[slot]
148                .as_ref()
149                .filter(|(h, arc)| *h == hash && **arc == union)
150                .map(|(_, arc)| Arc::clone(arc))
151        });
152        if let Some(hit) = hit {
153            return hit;
154        }
155        let interned = intern_global(union);
156        LOCAL_CACHE.with(|cache| cache.borrow_mut()[slot] = Some((hash, Arc::clone(&interned))));
157        interned
158    }
159
160    const LOCAL_SLOTS: usize = 1024;
161
162    type LocalSlots = std::cell::RefCell<Box<[Option<(u64, Arc<Type>)>]>>;
163
164    thread_local! {
165        /// Direct-mapped front for the global table. It holds only Arcs taken
166        /// from that table: equal types that differ in key order (equality
167        /// ignores it) must keep resolving to the table's one representative.
168        static LOCAL_CACHE: LocalSlots =
169            std::cell::RefCell::new(vec![None; LOCAL_SLOTS].into_boxed_slice());
170    }
171
172    fn intern_global(union: Type) -> Arc<Type> {
173        let table = global_intern_table();
174        if let Some(existing) = table.get(&union) {
175            return Arc::clone(existing.value());
176        }
177        let arc = Arc::new(union.clone());
178        // `insert` semantics: if a parallel thread beat us, its Arc wins.
179        // The lookup-before-insert race is benign — both Arcs are content-
180        // equal — but we still want to share the canonical one going forward.
181        match table.entry(union) {
182            dashmap::mapref::entry::Entry::Occupied(o) => Arc::clone(o.get()),
183            dashmap::mapref::entry::Entry::Vacant(v) => {
184                v.insert(Arc::clone(&arc));
185                arc
186            }
187        }
188    }
189}
190
191// ---------------------------------------------------------------------------
192// Shared primitives
193// ---------------------------------------------------------------------------
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
196pub enum Visibility {
197    Public,
198    Protected,
199    Private,
200}
201
202impl Visibility {
203    pub fn is_at_least(&self, required: Visibility) -> bool {
204        *self <= required
205    }
206}
207
208impl std::fmt::Display for Visibility {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            Visibility::Public => write!(f, "public"),
212            Visibility::Protected => write!(f, "protected"),
213            Visibility::Private => write!(f, "private"),
214        }
215    }
216}
217
218fn serialize_template_bound<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
219where
220    S: serde::Serializer,
221{
222    value.as_deref().serialize(serializer)
223}
224
225fn deserialize_template_bound<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
226where
227    D: serde::Deserializer<'de>,
228{
229    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct TemplateParam {
234    pub name: Name,
235    /// Declared upper bound, e.g. `@template T of Traversable`.
236    /// Stored as `Option<Arc<Type>>` so common bounds (e.g. `object`, `mixed`)
237    /// are deduplicated across all template params via the global intern table.
238    #[serde(
239        serialize_with = "serialize_template_bound",
240        deserialize_with = "deserialize_template_bound"
241    )]
242    pub bound: Option<Arc<Type>>,
243    /// Default type used when nothing binds this template param, e.g.
244    /// `@template T = string`. Falls back to `mixed` when absent, same as
245    /// before this field existed.
246    #[serde(
247        default,
248        serialize_with = "serialize_template_bound",
249        deserialize_with = "deserialize_template_bound"
250    )]
251    pub default: Option<Arc<Type>>,
252    /// The entity (class or function FQN) that declared this template param.
253    pub defining_entity: Name,
254    pub variance: mir_types::Variance,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct DeclaredParam {
259    pub name: Name,
260    /// Parameter type. Stored as `Option<Arc<Type>>` to enable deduplication of
261    /// common types across parameters. Many parameters share types like `string`,
262    /// `int`, `bool`, etc., so interning via Arc saves allocations.
263    #[serde(
264        deserialize_with = "deserialize_param_type",
265        serialize_with = "serialize_param_type"
266    )]
267    pub ty: Option<Arc<Type>>,
268    /// Out-type declared via `@param-out` / `@psalm-param-out`. When set, this
269    /// type is written back to the caller's argument variable after the call
270    /// instead of (or in addition to) the declared in-type.
271    #[serde(
272        default,
273        deserialize_with = "deserialize_param_type",
274        serialize_with = "serialize_param_type"
275    )]
276    pub out_ty: Option<Arc<Type>>,
277    /// `@param-closure-this Type`: the `$this` type of a closure literal passed for this parameter.
278    #[serde(
279        default,
280        deserialize_with = "deserialize_param_type",
281        serialize_with = "serialize_param_type"
282    )]
283    pub closure_this: Option<Arc<Type>>,
284    /// Raw `@param` type spelling when `ty` came from a docblock rather than a
285    /// native hint. Preserved so later analysis can resolve ambiguous
286    /// pseudo-type aliases (e.g. `integer`) against same-named classes once the
287    /// full symbol table is available.
288    #[serde(default)]
289    pub doc_type_raw: Option<Arc<str>>,
290    /// Declaring file for `doc_type_raw`. Needed because inherited signatures
291    /// may be consumed from a different file/namespace than the original
292    /// docblock declaration.
293    #[serde(default)]
294    pub doc_type_file: Option<Arc<str>>,
295    /// Whether this parameter has a default value. Analysis uses it only to mark
296    /// parameters optional; the value itself is kept as text in `default_text`.
297    pub has_default: bool,
298    /// Source text of the default value expression (`10`, `null`, `self::X`).
299    /// `None` when there is no default or its source text is unavailable.
300    #[serde(default)]
301    pub default_text: Option<Arc<str>>,
302    pub is_variadic: bool,
303    pub is_byref: bool,
304    pub is_optional: bool,
305}
306
307impl std::hash::Hash for DeclaredParam {
308    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
309        self.name.hash(state);
310        self.has_default.hash(state);
311        self.default_text.hash(state);
312        self.is_variadic.hash(state);
313        self.is_byref.hash(state);
314        self.is_optional.hash(state);
315        self.doc_type_raw.hash(state);
316        self.doc_type_file.hash(state);
317        // Hash the type value (not the Arc pointer) so that two FnParams with
318        // equal types (PartialEq) always produce the same hash, even when they
319        // are backed by different Arc allocations.
320        self.ty.as_deref().hash(state);
321        self.out_ty.as_deref().hash(state);
322        self.closure_this.as_deref().hash(state);
323    }
324}
325
326// Serde helpers to transparently convert between Option<Type> and Option<Arc<Type>>
327fn deserialize_param_type<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
328where
329    D: serde::Deserializer<'de>,
330{
331    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
332}
333
334fn serialize_param_type<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
335where
336    S: serde::Serializer,
337{
338    let opt = value.as_ref().map(|arc| (**arc).clone());
339    opt.serialize(serializer)
340}
341
342fn deserialize_return_type<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
343where
344    D: serde::Deserializer<'de>,
345{
346    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
347}
348
349fn serialize_return_type<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
350where
351    S: serde::Serializer,
352{
353    let opt = value.as_ref().map(|arc| (**arc).clone());
354    opt.serialize(serializer)
355}
356
357fn deserialize_params<'de, D>(deserializer: D) -> Result<Arc<[DeclaredParam]>, D::Error>
358where
359    D: serde::Deserializer<'de>,
360{
361    Vec::<DeclaredParam>::deserialize(deserializer).map(|v| Arc::from(v.into_boxed_slice()))
362}
363
364fn default_imports() -> Arc<FxHashMap<Name, Name>> {
365    Arc::new(FxHashMap::default())
366}
367
368/// Deserialize imports map. Supports both new (Name-keyed) and legacy
369/// (String-keyed) on-disk formats — older `cache.bin` files have plain
370/// `HashMap<String, String>`. Either way, we intern at load time so the
371/// in-memory representation is always `Arc<FxHashMap<Name, Name>>`.
372fn deserialize_imports<'de, D>(deserializer: D) -> Result<Arc<FxHashMap<Name, Name>>, D::Error>
373where
374    D: serde::Deserializer<'de>,
375{
376    let raw = FxHashMap::<String, String>::deserialize(deserializer)?;
377    let mut out: FxHashMap<Name, Name> =
378        FxHashMap::with_capacity_and_hasher(raw.len(), Default::default());
379    for (k, v) in raw {
380        out.insert(Name::new(&k), Name::new(&v));
381    }
382    Ok(Arc::new(out))
383}
384
385/// Serialize imports as the legacy `HashMap<String, String>` shape so disk
386/// caches written by this version remain compatible with readers that haven't
387/// been recompiled yet (and vice-versa).
388fn serialize_imports<S>(
389    value: &Arc<FxHashMap<Name, Name>>,
390    serializer: S,
391) -> Result<S::Ok, S::Error>
392where
393    S: serde::Serializer,
394{
395    use serde::ser::SerializeMap;
396    let mut map = serializer.serialize_map(Some(value.len()))?;
397    for (k, v) in value.iter() {
398        map.serialize_entry(k.as_str(), v.as_str())?;
399    }
400    map.end()
401}
402
403fn serialize_params<S>(value: &Arc<[DeclaredParam]>, serializer: S) -> Result<S::Ok, S::Error>
404where
405    S: serde::Serializer,
406{
407    value.as_ref().serialize(serializer)
408}
409
410/// Helper to wrap `Option<Type>` in interned `Arc<Type>`.
411pub fn wrap_param_type(ty: Option<Type>) -> Option<Arc<Type>> {
412    ty.map(interned_types::intern_or_wrap)
413}
414
415/// Helper to wrap return type `Option<Type>` in interned `Arc<Type>`.
416pub fn wrap_return_type(ty: Option<Type>) -> Option<Arc<Type>> {
417    ty.map(interned_types::intern_or_wrap)
418}
419
420/// Helper to wrap a `PropertyDef` type field (`ty`/`inferred_ty`/`default`) in
421/// an interned `Arc<Type>`, deduplicating common property types via the global
422/// pool. See [`PropertyDef`].
423pub fn wrap_property_type(ty: Option<Type>) -> Option<Arc<Type>> {
424    ty.map(interned_types::intern_or_wrap)
425}
426
427/// Helper to wrap a `TemplateParam.bound` in an interned `Arc<Type>`.
428pub fn wrap_template_bound(ty: Option<Type>) -> Option<Arc<Type>> {
429    ty.map(interned_types::intern_or_wrap)
430}
431
432/// Wrap a variable type in an interned `Arc<Type>`. Use instead of
433/// `Arc::new(ty)` at `FlowState::set_var` and parameter-init sites so that
434/// common scalars (string, int, bool, null, mixed) share a static Arc rather
435/// than allocating a fresh one per assignment.
436pub fn wrap_var_type(ty: Type) -> Arc<Type> {
437    interned_types::intern_or_wrap(ty)
438}
439
440// ---------------------------------------------------------------------------
441// Assertion — `@psalm-assert`, `@psalm-assert-if-true`, etc.
442// ---------------------------------------------------------------------------
443
444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
445pub enum AssertionKind {
446    Assert,
447    AssertIfTrue,
448    AssertIfFalse,
449}
450
451#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
452pub struct Assertion {
453    pub kind: AssertionKind,
454    pub param: Arc<str>,
455    pub ty: Type,
456    /// True for the `!Type` negated form (`@psalm-assert !null $x`): the
457    /// parameter is asserted to NOT be `ty`, rather than to BE it.
458    #[serde(default)]
459    pub negated: bool,
460    /// Set for an assertion targeting a specific (possibly nested) array key
461    /// path of `param` rather than the whole parameter (`@psalm-assert-if-true
462    /// string $arr['a']['b']` — `param` is `"arr"`, `param_key` is
463    /// `[String("a"), String("b")]`). Empty means "targets the whole parameter".
464    #[serde(default)]
465    pub param_key: Vec<mir_types::atomic::ArrayKey>,
466}
467
468// ---------------------------------------------------------------------------
469// MethodDef
470// ---------------------------------------------------------------------------
471
472/// Prose following the type and name of `@param`, `@return` and `@throws`
473/// tags. Tags without prose are omitted.
474#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
475pub struct TagDescriptions {
476    /// `(parameter name without $, description)`.
477    pub params: Vec<(Arc<str>, Arc<str>)>,
478    pub returns: Option<Arc<str>>,
479    /// `(resolved exception FQCN, description)`.
480    pub throws: Vec<(Arc<str>, Arc<str>)>,
481}
482
483/// `@taint-source`, `@taint-escape`, `@taint-unescape` and `@psalm-flow` on a function or method.
484#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
485pub struct TaintAnnotations {
486    /// Kinds of an `@taint-source`; none means every kind.
487    pub source: Vec<Arc<str>>,
488    /// Sink kinds the return value no longer carries.
489    pub escape: Vec<Arc<str>>,
490    /// Sink kinds the return value regains from tainted arguments.
491    pub unescape: Vec<Arc<str>>,
492    /// Parameters whose taint flows into the return value.
493    pub flow_params: Vec<Arc<str>>,
494}
495
496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
497pub struct MethodDef {
498    pub name: Arc<str>,
499    pub fqcn: Arc<str>,
500    #[serde(
501        deserialize_with = "deserialize_params",
502        serialize_with = "serialize_params"
503    )]
504    pub params: Arc<[DeclaredParam]>,
505    /// Type from annotation (`@return` / native type hint). `None` means unannotated.
506    /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types
507    /// (e.g., `void`, `string`, `mixed`, `bool`) across thousands of methods.
508    #[serde(
509        deserialize_with = "deserialize_return_type",
510        serialize_with = "serialize_return_type"
511    )]
512    pub return_type: Option<Arc<Type>>,
513    /// Type inferred from body analysis. Stored as `Option<Arc<Type>>` (8 B) rather
514    /// than inline `Option<Type>` (176 B, no niche) — inference is now demand-driven
515    /// via salsa (`inferred_*_return_type_demand`), so this field is a rarely/never
516    /// populated fallback; shrinking it saves ~168 B on every MethodDef.
517    #[serde(
518        deserialize_with = "deserialize_return_type",
519        serialize_with = "serialize_return_type"
520    )]
521    pub inferred_return_type: Option<Arc<Type>>,
522    /// Native return hint kept beside a `@return` that names a template, to bound it when unbound.
523    #[serde(default)]
524    pub native_return: Option<Box<Type>>,
525    pub visibility: Visibility,
526    pub is_static: bool,
527    pub is_abstract: bool,
528    pub is_final: bool,
529    pub is_constructor: bool,
530    pub template_params: Vec<TemplateParam>,
531    pub assertions: Vec<Assertion>,
532    pub throws: Vec<Arc<str>>,
533    pub deprecated: Option<Arc<str>>,
534    pub is_internal: bool,
535    pub is_pure: bool,
536    /// An explicit `@impure`/`@psalm-impure`/`@phpstan-impure` declaration.
537    #[serde(default)]
538    pub is_impure: bool,
539    /// `@no-named-arguments` — callers must not use named argument syntax.
540    #[serde(default)]
541    pub no_named_arguments: bool,
542    /// True when the method has the `#[Override]` PHP attribute.
543    #[serde(default)]
544    pub is_override: bool,
545    pub location: Option<Location>,
546    /// Plain-text description from the docblock (text before `@tag` lines).
547    /// Used for hover info.
548    #[serde(default)]
549    pub docstring: Option<Arc<str>>,
550    /// Prose attached to `@param`, `@return` and `@throws` tags. `None` when
551    /// no tag carries a description.
552    #[serde(default)]
553    pub tag_descriptions: Option<Arc<TagDescriptions>>,
554    /// True for methods added via `@method` docblock annotations. Virtual
555    /// methods must not be required as concrete interface implementations.
556    #[serde(default)]
557    pub is_virtual: bool,
558    /// Parameters declared as taint sinks via `@taint-sink <kind> $param`.
559    /// Each entry is `(param_name_without_dollar, sink_kind_string)`.
560    #[serde(default)]
561    pub taint_sink_params: Vec<(Arc<str>, Arc<str>)>,
562    /// `@taint-source` — this method's return value is treated as tainted
563    /// (attacker-controlled) at every call site, mirroring `@taint-sink`'s
564    /// mechanism but marking the source side instead.
565    #[serde(default)]
566    pub is_taint_source: bool,
567    #[serde(default)]
568    pub taint: Option<Arc<TaintAnnotations>>,
569    /// `@if-this-is Type` — the resolved constraint a receiver's type must
570    /// satisfy for this method to be callable. `None` when absent.
571    #[serde(default)]
572    pub if_this_is: Option<Arc<Type>>,
573    /// `@psalm-self-out Type` / `@phpstan-self-out Type` — the receiver's type
574    /// after this call returns (e.g. a fluent builder that narrows `$this` as
575    /// it's configured). `None` when absent.
576    #[serde(default)]
577    pub self_out: Option<Arc<Type>>,
578    /// True when the method has `@inheritDoc` / `{@inheritDoc}` in its docblock.
579    /// The analyzer inherits the parent's return type, param types, throws, and
580    /// template params when this method has none of its own.
581    #[serde(default)]
582    pub is_inherit_doc: bool,
583    /// `@psalm-mutation-free` / `@phpstan-mutation-free` — this method must not
584    /// assign to `$this` properties (same constraint as `@psalm-immutable` on the
585    /// class, but scoped to this single method).
586    #[serde(default)]
587    pub is_mutation_free: bool,
588    /// `@psalm-external-mutation-free` — this method must not mutate any objects
589    /// passed as arguments, but is allowed to modify `$this`.
590    #[serde(default)]
591    pub is_external_mutation_free: bool,
592    /// The body provably never writes a property, so a call leaves the
593    /// receiver's property narrowing intact. Derived from the body, unlike
594    /// `is_mutation_free`, which comes from the annotation.
595    #[serde(default)]
596    pub inferred_mutation_free: bool,
597    /// Method names referenced via `@dataProvider name` / `#[DataProvider('name')]`
598    /// (PHPUnit) — treated as used by dead-code analysis, since PHPUnit invokes
599    /// them by name through reflection rather than a direct call site.
600    #[serde(default)]
601    pub data_provider_targets: Vec<Arc<str>>,
602}
603
604impl MethodDef {
605    pub fn effective_return_type(&self) -> Option<&Type> {
606        self.return_type
607            .as_deref()
608            .or(self.inferred_return_type.as_deref())
609    }
610}
611
612// ---------------------------------------------------------------------------
613// PropertyDef
614// ---------------------------------------------------------------------------
615
616#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
617pub struct PropertyDef {
618    pub name: Arc<str>,
619    /// Declared/inferred/default types. Stored as `Option<Arc<Type>>` (8 B)
620    /// rather than inline `Option<Type>` (176 B, no niche) and interned via the
621    /// global pool on construction/deserialization — common property types
622    /// (`string`, `int`, a shared class type) dedup to one allocation. Mirrors
623    /// `DeclaredParam::ty`. On-disk format is unchanged (the serde helpers (de)serialize
624    /// the inner `Type` transparently).
625    #[serde(
626        deserialize_with = "deserialize_param_type",
627        serialize_with = "serialize_param_type"
628    )]
629    pub ty: Option<Arc<Type>>,
630    #[serde(
631        deserialize_with = "deserialize_param_type",
632        serialize_with = "serialize_param_type"
633    )]
634    pub inferred_ty: Option<Arc<Type>>,
635    pub visibility: Visibility,
636    /// Asymmetric write visibility (`private(set)`); `None` follows `visibility`.
637    pub set_visibility: Option<Visibility>,
638    pub is_static: bool,
639    pub is_readonly: bool,
640    #[serde(
641        deserialize_with = "deserialize_param_type",
642        serialize_with = "serialize_param_type"
643    )]
644    pub default: Option<Arc<Type>>,
645    pub location: Option<Location>,
646    /// `@deprecated` docblock annotation, if present.
647    #[serde(default)]
648    pub deprecated: Option<Arc<str>>,
649    /// True when the property declares a PHP native type hint (`public int $x`).
650    /// A property typed only via a `@var` docblock (or untyped entirely) is
651    /// `false`: PHP gives such a property an implicit `null` default, so it is
652    /// never "uninitialized" (no MissingConstructor) and accepts `null` on
653    /// assignment regardless of the advisory docblock type.
654    #[serde(default)]
655    pub has_native_type: bool,
656    /// True when this entry was synthesised from a `@property` / `@property-read` /
657    /// `@property-write` docblock tag rather than a real PHP property declaration.
658    /// Such entries describe magic properties accessible via `__get`/`__set` and
659    /// do **not** participate in PHP's inheritance visibility rules.
660    #[serde(default)]
661    pub from_docblock: bool,
662    /// True when the property declares a `get` hook with no accompanying `set`
663    /// hook. Such a property is virtual: it has no backing storage, computes
664    /// its value on every access, and can never be assigned to at all — so it
665    /// is never "left uninitialized" by the constructor. A property that also
666    /// declares (or only declares) a `set` hook is backed and can still be
667    /// left uninitialized, so it does not count here.
668    #[serde(default)]
669    pub get_only_hook: bool,
670    /// Hooks declared without a body (interface or `abstract` property): bit 0 `get`, bit 1 `set`.
671    #[serde(default)]
672    pub abstract_hooks: u8,
673    /// For a virtual property (hooks, none touching `$this->name`), its declared hooks as in
674    /// `abstract_hooks`; 0 for a backed one.
675    #[serde(default)]
676    pub virtual_hooks: u8,
677    /// True when `readonly` comes from a native PHP keyword (`readonly` modifier or
678    /// `readonly class`). False when only a `@readonly` docblock annotation is present.
679    /// Distinguishes PHP-enforced read-only from advisory documentation.
680    #[serde(default)]
681    pub has_native_readonly: bool,
682    /// Declared with `#[Override]` (PHP 8.5 checks properties too).
683    #[serde(default)]
684    pub is_override: bool,
685    /// The PHP native type hint alone, with any `@var` docblock refinement stripped —
686    /// `None` when `has_native_type` is false. `ty` mixes in the docblock type when
687    /// present, which makes it unsuitable for checking PHP's redeclared-property
688    /// type invariance rule: that rule is enforced by the runtime purely on the
689    /// native hint, never on the (unenforced) docblock annotation.
690    #[serde(default)]
691    #[serde(
692        deserialize_with = "deserialize_param_type",
693        serialize_with = "serialize_param_type"
694    )]
695    pub native_ty: Option<Arc<Type>>,
696}
697
698// ---------------------------------------------------------------------------
699// ConstantDef
700// ---------------------------------------------------------------------------
701
702#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
703pub struct ConstantDef {
704    pub name: Arc<str>,
705    pub ty: Type,
706    /// Literal initializer when a `@var` makes `ty` wider than the value.
707    #[serde(default)]
708    pub literal_value: Option<Type>,
709    pub visibility: Option<Visibility>,
710    #[serde(default)]
711    pub is_final: bool,
712    pub location: Option<Location>,
713    /// `@deprecated` docblock annotation, if present.
714    #[serde(default)]
715    pub deprecated: Option<Arc<str>>,
716}
717
718// ---------------------------------------------------------------------------
719// ClassDef
720// ---------------------------------------------------------------------------
721
722#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
723pub struct ClassDef {
724    pub fqcn: Arc<str>,
725    pub short_name: Arc<str>,
726    /// Marked `@api` / `@psalm-api`: used from outside the analyzed code.
727    #[serde(default)]
728    pub is_api: bool,
729    pub parent: Option<Arc<str>>,
730    pub interfaces: Vec<Arc<str>>,
731    pub traits: Vec<Arc<str>>,
732    pub own_methods: MemberMap<Arc<MethodDef>>,
733    pub own_properties: MemberMap<PropertyDef>,
734    pub own_constants: MemberMap<ConstantDef>,
735    #[serde(default)]
736    pub mixins: Vec<Arc<str>>,
737    /// Source of each `mixins` tag, same order; empty when unknown.
738    #[serde(default)]
739    pub mixin_locations: Vec<Location>,
740    pub template_params: Vec<TemplateParam>,
741    /// Type arguments from `@extends ParentClass<T1, T2>` — maps parent's template params to concrete types.
742    pub extends_type_args: Vec<Type>,
743    /// Type arguments from `@implements Interface<T1, T2>`.
744    #[serde(default)]
745    pub implements_type_args: Vec<(Arc<str>, Vec<Type>)>,
746    /// Type arguments from `@use TraitName<T1, T2>`, keyed by the used
747    /// trait's FQCN — a class's `use` clause (unlike `@extends`) may name
748    /// several traits at once.
749    #[serde(default)]
750    pub trait_use_type_args: Vec<(Arc<str>, Vec<Type>)>,
751    pub is_abstract: bool,
752    pub is_final: bool,
753    pub is_readonly: bool,
754    pub deprecated: Option<Arc<str>>,
755    pub is_internal: bool,
756    /// Set when the class carries `@psalm-immutable` — non-constructor methods must not
757    /// assign to `$this` properties.
758    #[serde(default)]
759    pub is_immutable: bool,
760    /// Attribute target flags if this class has `#[Attribute]` annotation.
761    /// `None` = not an attribute class. The value is a bitmask of PHP's
762    /// `Attribute::TARGET_*` constants (e.g. `Attribute::TARGET_CLASS = 1`).
763    #[serde(default)]
764    pub attribute_flags: Option<i64>,
765    /// Declared with `#[AllowDynamicProperties]`; subclasses inherit it.
766    #[serde(default)]
767    pub allows_dynamic_properties: bool,
768    pub location: Option<Location>,
769    /// Per-`use` statement locations for each used trait: `(fqcn, location)` in
770    /// declaration order, parallel to `traits`.  Absent from older serialized
771    /// slices; defaults to empty.
772    #[serde(default)]
773    pub trait_use_locations: Vec<(Arc<str>, Location)>,
774    /// Type aliases declared on this class via `@psalm-type` / `@phpstan-type`.
775    #[serde(default)]
776    pub type_aliases: FxHashMap<Arc<str>, Type>,
777    /// Raw import-type declarations (`(local_name, original_name, from_class)`) — resolved during finalization.
778    #[serde(default)]
779    pub pending_import_types: Vec<(Arc<str>, Arc<str>, Arc<str>)>,
780    /// Trait precedence exclusions from `insteadof` declarations in this class's `use` blocks.
781    /// Maps method_name_lowercase → list of trait FQCNs whose version of the method is excluded.
782    /// E.g. `use A, B { B::hello insteadof A; }` stores `"hello" → ["A"]`.
783    #[serde(default)]
784    pub trait_insteadof: MemberMap<Vec<Arc<str>>>,
785    /// Trait method aliases from `as` declarations in this class's `use` blocks.
786    /// Maps new_name_lowercase → (optional_trait_fqcn, original_method_name_lowercase, visibility_override, alias_cased).
787    /// `alias_cased` is the alias name preserving the original PHP casing (for error messages / case checks).
788    /// Visibility is `None` when the `as` clause only renames without changing visibility.
789    /// E.g. `use Base { __construct as __constructBase; }` stores
790    ///   `"__constructbase" → (None, "__construct", None, "__constructBase")`.
791    /// E.g. `use T { foo as private traitFoo; }` stores
792    ///   `"traitfoo" → (None, "foo", Some(Private), "traitFoo")`.
793    #[serde(default)]
794    #[allow(clippy::type_complexity)]
795    pub trait_aliases:
796        FxHashMap<Arc<str>, (Option<Arc<str>>, Arc<str>, Option<Visibility>, Arc<str>)>,
797    /// Source span of each `trait_aliases` adaptation, keyed the same way.
798    #[serde(default)]
799    pub trait_alias_locations: FxHashMap<Arc<str>, Location>,
800}
801
802impl ClassDef {
803    pub fn get_method(&self, name: &str) -> Option<&MethodDef> {
804        // PHP method names are case-insensitive; caller should pass lowercase name.
805        // Only searches own_methods — inherited method resolution is done by
806        // `db::lookup_method_in_chain`.
807        self.own_methods.get(name).map(Arc::as_ref).or_else(|| {
808            self.own_methods
809                .iter()
810                .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
811                .map(|(_, v)| v.as_ref())
812        })
813    }
814
815    pub fn get_property(&self, name: &str) -> Option<&PropertyDef> {
816        self.own_properties.get(name)
817    }
818}
819
820// ---------------------------------------------------------------------------
821// InterfaceDef
822// ---------------------------------------------------------------------------
823
824#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
825pub struct InterfaceDef {
826    pub fqcn: Arc<str>,
827    pub short_name: Arc<str>,
828    /// Marked `@api` / `@psalm-api`: used from outside the analyzed code.
829    #[serde(default)]
830    pub is_api: bool,
831    pub extends: Vec<Arc<str>>,
832    pub own_methods: MemberMap<Arc<MethodDef>>,
833    pub own_constants: MemberMap<ConstantDef>,
834    pub template_params: Vec<TemplateParam>,
835    pub location: Option<Location>,
836    /// `@deprecated` docblock annotation, if present.
837    #[serde(default)]
838    pub deprecated: Option<Arc<str>>,
839    /// Properties declared via `@property*` docblock annotations on the interface.
840    #[serde(default)]
841    pub own_properties: MemberMap<PropertyDef>,
842    /// `@seal-properties` / `@psalm-seal-properties` — disallows undeclared property access.
843    #[serde(default)]
844    pub seal_properties: bool,
845    /// Type arguments from `@extends BaseIface<T1, T2>` docblock lines, keyed by the
846    /// extended interface's FQCN — an interface's native `extends` list (unlike a
847    /// class's single parent) may name several base interfaces at once.
848    #[serde(default)]
849    pub extends_type_args: Vec<(Arc<str>, Vec<Type>)>,
850    /// Type aliases declared on this interface via `@psalm-type` / `@phpstan-type`.
851    #[serde(default)]
852    pub type_aliases: FxHashMap<Arc<str>, Type>,
853}
854
855// ---------------------------------------------------------------------------
856// TraitDef
857// ---------------------------------------------------------------------------
858
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
860pub struct TraitDef {
861    pub fqcn: Arc<str>,
862    pub short_name: Arc<str>,
863    /// Marked `@api` / `@psalm-api`: used from outside the analyzed code.
864    #[serde(default)]
865    pub is_api: bool,
866    pub own_methods: MemberMap<Arc<MethodDef>>,
867    pub own_properties: MemberMap<PropertyDef>,
868    pub own_constants: MemberMap<ConstantDef>,
869    pub template_params: Vec<TemplateParam>,
870    /// Traits used by this trait (`use OtherTrait;` inside a trait body).
871    pub traits: Vec<Arc<str>>,
872    pub location: Option<Location>,
873    /// Per-`use` statement locations for each used trait: `(fqcn, location)` in
874    /// declaration order, parallel to `traits`. Mirrors `ClassDef`/`EnumDef`'s
875    /// field of the same name. Absent from older serialized slices; defaults
876    /// to empty.
877    #[serde(default)]
878    pub trait_use_locations: Vec<(Arc<str>, Location)>,
879    /// Type arguments from `@use OtherTrait<T1, T2>` (a trait may itself
880    /// `use` a generic trait).
881    #[serde(default)]
882    pub trait_use_type_args: Vec<(Arc<str>, Vec<Type>)>,
883    /// Set when the trait carries `@psalm-immutable`.
884    #[serde(default)]
885    pub is_immutable: bool,
886    /// `@psalm-require-extends` / `@phpstan-require-extends` — FQCNs that using classes must extend.
887    #[serde(default)]
888    pub require_extends: Vec<Arc<str>>,
889    /// `@psalm-require-implements` / `@phpstan-require-implements` — FQCNs that using classes must implement.
890    #[serde(default)]
891    pub require_implements: Vec<Arc<str>>,
892    /// `@deprecated` docblock annotation, if present.
893    #[serde(default)]
894    pub deprecated: Option<Arc<str>>,
895    /// Type aliases declared on this trait via `@psalm-type` / `@phpstan-type`.
896    #[serde(default)]
897    pub type_aliases: FxHashMap<Arc<str>, Type>,
898}
899
900// ---------------------------------------------------------------------------
901// EnumDef
902// ---------------------------------------------------------------------------
903
904#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
905pub struct EnumCaseDef {
906    pub name: Arc<str>,
907    pub value: Option<Type>,
908    pub location: Option<Location>,
909    /// `@deprecated` docblock annotation, if present.
910    #[serde(default)]
911    pub deprecated: Option<Arc<str>>,
912}
913
914#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
915pub struct EnumDef {
916    pub fqcn: Arc<str>,
917    pub short_name: Arc<str>,
918    /// Marked `@api` / `@psalm-api`: used from outside the analyzed code.
919    #[serde(default)]
920    pub is_api: bool,
921    pub scalar_type: Option<Type>,
922    pub interfaces: Vec<Arc<str>>,
923    /// Type arguments from `@implements Interface<T1, T2>`.
924    #[serde(default)]
925    pub implements_type_args: Vec<(Arc<str>, Vec<Type>)>,
926    pub cases: MemberMap<EnumCaseDef>,
927    pub own_methods: MemberMap<Arc<MethodDef>>,
928    pub own_constants: MemberMap<ConstantDef>,
929    /// `use SomeTrait;` declarations. PHP enums may use traits (for methods),
930    /// just never carry instance properties from them.
931    #[serde(default)]
932    pub traits: Vec<Arc<str>>,
933    #[serde(default)]
934    pub trait_use_locations: Vec<(Arc<str>, Location)>,
935    /// Type arguments from `@use SomeTrait<T1, T2>`.
936    #[serde(default)]
937    pub trait_use_type_args: Vec<(Arc<str>, Vec<Type>)>,
938    pub location: Option<Location>,
939    /// `@deprecated` docblock annotation (or `#[Deprecated]` attribute), if present.
940    #[serde(default)]
941    pub deprecated: Option<Arc<str>>,
942    /// Type aliases declared on this enum via `@psalm-type` / `@phpstan-type`.
943    #[serde(default)]
944    pub type_aliases: FxHashMap<Arc<str>, Type>,
945    /// Properties declared via `@property*` docblock annotations on the enum.
946    #[serde(default)]
947    pub own_properties: MemberMap<PropertyDef>,
948}
949
950// ---------------------------------------------------------------------------
951// FunctionDef
952// ---------------------------------------------------------------------------
953
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955pub struct FunctionDef {
956    pub fqn: Arc<str>,
957    pub short_name: Arc<str>,
958    #[serde(
959        deserialize_with = "deserialize_params",
960        serialize_with = "serialize_params"
961    )]
962    pub params: Arc<[DeclaredParam]>,
963    /// Type from annotation (`@return` / native type hint). `None` means unannotated.
964    /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types.
965    #[serde(
966        deserialize_with = "deserialize_return_type",
967        serialize_with = "serialize_return_type"
968    )]
969    pub return_type: Option<Arc<Type>>,
970    /// See `MethodDef::inferred_return_type` — `Option<Arc<Type>>` (8 B) for the
971    /// same demand-driven-inference reason.
972    #[serde(
973        deserialize_with = "deserialize_return_type",
974        serialize_with = "serialize_return_type"
975    )]
976    pub inferred_return_type: Option<Arc<Type>>,
977    pub template_params: Vec<TemplateParam>,
978    pub assertions: Vec<Assertion>,
979    pub throws: Vec<Arc<str>>,
980    pub deprecated: Option<Arc<str>>,
981    pub is_pure: bool,
982    /// `@psalm-mutation-free` / `@phpstan-mutation-free` on a free function.
983    /// A free function has no `$this`, so this is behaviorally equivalent to
984    /// `is_external_mutation_free` here (both forbid mutating a parameter);
985    /// see `MethodDef::is_mutation_free` for the method-level (has-`$this`)
986    /// distinction this mirrors.
987    #[serde(default)]
988    pub is_mutation_free: bool,
989    /// `@psalm-external-mutation-free` on a free function — must not mutate
990    /// any object passed as an argument.
991    #[serde(default)]
992    pub is_external_mutation_free: bool,
993    /// `@no-named-arguments` — callers must not use named argument syntax.
994    #[serde(default)]
995    pub no_named_arguments: bool,
996    pub location: Option<Location>,
997    /// Plain-text description from the docblock (text before `@tag` lines).
998    /// Used for hover info.
999    #[serde(default)]
1000    pub docstring: Option<Arc<str>>,
1001    /// Prose attached to `@param`, `@return` and `@throws` tags. `None` when
1002    /// no tag carries a description.
1003    #[serde(default)]
1004    pub tag_descriptions: Option<Arc<TagDescriptions>>,
1005    /// Parameters declared as taint sinks via `@taint-sink <kind> $param`.
1006    /// Each entry is `(param_name_without_dollar, sink_kind_string)`.
1007    #[serde(default)]
1008    pub taint_sink_params: Vec<(Arc<str>, Arc<str>)>,
1009    /// `@taint-source` — this function's return value is treated as tainted
1010    /// (attacker-controlled) at every call site, mirroring `@taint-sink`'s
1011    /// mechanism but marking the source side instead.
1012    #[serde(default)]
1013    pub is_taint_source: bool,
1014    #[serde(default)]
1015    pub taint: Option<Arc<TaintAnnotations>>,
1016    /// Type aliases declared on this function via `@psalm-type` / `@phpstan-type`.
1017    #[serde(default)]
1018    pub type_aliases: FxHashMap<Arc<str>, Type>,
1019}
1020
1021impl FunctionDef {
1022    pub fn effective_return_type(&self) -> Option<&Type> {
1023        self.return_type
1024            .as_deref()
1025            .or(self.inferred_return_type.as_deref())
1026    }
1027}
1028
1029// ---------------------------------------------------------------------------
1030// StubSlice — serializable bundle of definitions from one extension's stubs
1031// ---------------------------------------------------------------------------
1032
1033/// A `namespace` declaration's byte range with the names it resolves against.
1034#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1035pub struct NamespaceBlock {
1036    pub start: u32,
1037    pub end: u32,
1038    pub namespace: Option<Arc<str>>,
1039    #[serde(
1040        deserialize_with = "deserialize_imports",
1041        serialize_with = "serialize_imports"
1042    )]
1043    pub class_imports: Arc<FxHashMap<Name, Name>>,
1044    #[serde(
1045        deserialize_with = "deserialize_imports",
1046        serialize_with = "serialize_imports"
1047    )]
1048    pub function_imports: Arc<FxHashMap<Name, Name>>,
1049    #[serde(
1050        deserialize_with = "deserialize_imports",
1051        serialize_with = "serialize_imports"
1052    )]
1053    pub const_imports: Arc<FxHashMap<Name, Name>>,
1054}
1055
1056/// A snapshot of all PHP definitions contributed by a single stub file set.
1057///
1058/// Produced by `mir-stubs-gen` at code-generation time and deserialized at
1059/// runtime to ingest definitions into the salsa db via
1060/// `MirDatabase::ingest_stub_slice`.
1061#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1062pub struct StubSlice {
1063    pub classes: Vec<Arc<ClassDef>>,
1064    pub interfaces: Vec<Arc<InterfaceDef>>,
1065    pub traits: Vec<Arc<TraitDef>>,
1066    pub enums: Vec<Arc<EnumDef>>,
1067    pub functions: Vec<Arc<FunctionDef>>,
1068    #[serde(default)]
1069    pub constants: Vec<(Arc<str>, Type)>,
1070    /// Source file this slice was collected from. `None` for bundled stub slices
1071    /// that were pre-computed and are not tied to a specific on-disk file.
1072    #[serde(default)]
1073    pub file: Option<Arc<str>>,
1074    /// Types of `@var`-annotated global variables collected from this file.
1075    /// Populated by `DefinitionCollector`; ingested into the salsa db's
1076    /// `global_vars` table by `ingest_stub_slice` when `file` is `Some`.
1077    #[serde(default)]
1078    pub global_vars: Vec<(Arc<str>, Type)>,
1079    /// The first namespace declared in this file (e.g. `"App\\Service"`).
1080    /// Populated by `DefinitionCollector`; ingested into the salsa db's
1081    /// `file_namespaces` table by `ingest_stub_slice` when `file` is `Some`.
1082    #[serde(default)]
1083    pub namespace: Option<Arc<str>>,
1084    /// `use` class-like aliases: alias → FQCN. Each `use` kind has its own map, as in PHP.
1085    #[serde(
1086        deserialize_with = "deserialize_imports",
1087        serialize_with = "serialize_imports"
1088    )]
1089    #[serde(default = "default_imports")]
1090    pub class_imports: Arc<FxHashMap<Name, Name>>,
1091    /// `use function` aliases: alias → function FQN.
1092    #[serde(
1093        deserialize_with = "deserialize_imports",
1094        serialize_with = "serialize_imports"
1095    )]
1096    #[serde(default = "default_imports")]
1097    pub function_imports: Arc<FxHashMap<Name, Name>>,
1098    /// `use const` aliases: alias → constant FQN.
1099    #[serde(
1100        deserialize_with = "deserialize_imports",
1101        serialize_with = "serialize_imports"
1102    )]
1103    #[serde(default = "default_imports")]
1104    pub const_imports: Arc<FxHashMap<Name, Name>>,
1105    /// Per-block namespace and imports; populated only when the file declares
1106    /// two or more `namespace` blocks, otherwise empty.
1107    #[serde(default)]
1108    pub namespace_blocks: Arc<[NamespaceBlock]>,
1109    /// Set to `true` after `deduplicate_params_in_slice` has run on this slice.
1110    /// `ingest_stub_slice` skips the clone+re-dedup when this flag is set.
1111    #[serde(skip)]
1112    pub is_deduped: bool,
1113}
1114
1115// ---------------------------------------------------------------------------
1116// Param list deduplication
1117// ---------------------------------------------------------------------------
1118
1119/// Sharded so slices collected in parallel don't serialize on one lock.
1120type ParamCache =
1121    dashmap::DashMap<Vec<DeclaredParam>, Arc<[DeclaredParam]>, rustc_hash::FxBuildHasher>;
1122
1123/// Global cache of canonical Arc<[DeclaredParam]> instances for deduplication.
1124/// Shared across all StubSlices to deduplicate vendor code with millions of
1125/// methods that often have identical parameter lists.
1126static PARAM_DEDUP_CACHE: std::sync::OnceLock<ParamCache> = std::sync::OnceLock::new();
1127
1128/// Deduplicate parameter lists across all methods and functions in a StubSlice.
1129/// Many PHP framework methods share identical parameter lists (e.g., thousands
1130/// of `(string $arg, array $opts)` signatures). This function groups identical
1131/// param lists globally (across all slices processed so far) and replaces them
1132/// with Arc<[DeclaredParam]> pointers to shared allocations.
1133///
1134/// Expected memory savings: 100–150 MiB on cold start (vendor collection).
1135pub fn deduplicate_params_in_slice(slice: &mut StubSlice) {
1136    let cache: &ParamCache =
1137        PARAM_DEDUP_CACHE.get_or_init(|| dashmap::DashMap::with_hasher(Default::default()));
1138
1139    let deduplicate = |params: &mut Arc<[DeclaredParam]>| {
1140        if let Some(existing) = cache.get(params.as_ref()) {
1141            *params = existing.clone();
1142            return;
1143        }
1144        let canonical = cache
1145            .entry(params.as_ref().to_vec())
1146            .or_insert_with(|| params.clone())
1147            .clone();
1148        *params = canonical;
1149    };
1150
1151    // Deduplicate method params in all classes
1152    for cls in &mut slice.classes {
1153        for method in Arc::make_mut(cls).own_methods.values_mut() {
1154            deduplicate(&mut Arc::make_mut(method).params);
1155        }
1156    }
1157
1158    // Deduplicate method params in all interfaces
1159    for iface in &mut slice.interfaces {
1160        for method in Arc::make_mut(iface).own_methods.values_mut() {
1161            deduplicate(&mut Arc::make_mut(method).params);
1162        }
1163    }
1164
1165    // Deduplicate method params in all traits
1166    for tr in &mut slice.traits {
1167        for method in Arc::make_mut(tr).own_methods.values_mut() {
1168            deduplicate(&mut Arc::make_mut(method).params);
1169        }
1170    }
1171
1172    // Deduplicate method params in all enums
1173    for en in &mut slice.enums {
1174        for method in Arc::make_mut(en).own_methods.values_mut() {
1175            deduplicate(&mut Arc::make_mut(method).params);
1176        }
1177    }
1178
1179    // Deduplicate function params
1180    for func in &mut slice.functions {
1181        deduplicate(&mut Arc::make_mut(func).params);
1182    }
1183    slice.is_deduped = true;
1184}