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