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