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