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