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