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}
389
390// ---------------------------------------------------------------------------
391// MethodDef
392// ---------------------------------------------------------------------------
393
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395pub struct MethodDef {
396 pub name: Arc<str>,
397 pub fqcn: Arc<str>,
398 #[serde(
399 deserialize_with = "deserialize_params",
400 serialize_with = "serialize_params"
401 )]
402 pub params: Arc<[DeclaredParam]>,
403 /// Type from annotation (`@return` / native type hint). `None` means unannotated.
404 /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types
405 /// (e.g., `void`, `string`, `mixed`, `bool`) across thousands of methods.
406 #[serde(
407 deserialize_with = "deserialize_return_type",
408 serialize_with = "serialize_return_type"
409 )]
410 pub return_type: Option<Arc<Type>>,
411 /// Type inferred from body analysis. Stored as `Option<Arc<Type>>` (8 B) rather
412 /// than inline `Option<Type>` (176 B, no niche) — inference is now demand-driven
413 /// via salsa (`inferred_*_return_type_demand`), so this field is a rarely/never
414 /// populated fallback; shrinking it saves ~168 B on every MethodDef.
415 #[serde(
416 deserialize_with = "deserialize_return_type",
417 serialize_with = "serialize_return_type"
418 )]
419 pub inferred_return_type: Option<Arc<Type>>,
420 pub visibility: Visibility,
421 pub is_static: bool,
422 pub is_abstract: bool,
423 pub is_final: bool,
424 pub is_constructor: bool,
425 pub template_params: Vec<TemplateParam>,
426 pub assertions: Vec<Assertion>,
427 pub throws: Vec<Arc<str>>,
428 pub deprecated: Option<Arc<str>>,
429 pub is_internal: bool,
430 pub is_pure: bool,
431 /// `@no-named-arguments` — callers must not use named argument syntax.
432 #[serde(default)]
433 pub no_named_arguments: bool,
434 /// True when the method has the `#[Override]` PHP attribute.
435 #[serde(default)]
436 pub is_override: bool,
437 pub location: Option<Location>,
438 /// Plain-text description from the docblock (text before `@tag` lines).
439 /// Used for hover info.
440 #[serde(default)]
441 pub docstring: Option<Arc<str>>,
442 /// True for methods added via `@method` docblock annotations. Virtual
443 /// methods must not be required as concrete interface implementations.
444 #[serde(default)]
445 pub is_virtual: bool,
446 /// Parameters declared as taint sinks via `@taint-sink <kind> $param`.
447 /// Each entry is `(param_name_without_dollar, sink_kind_string)`.
448 #[serde(default)]
449 pub taint_sink_params: Vec<(Arc<str>, Arc<str>)>,
450 /// `@if-this-is Type` — the resolved constraint a receiver's type must
451 /// satisfy for this method to be callable. `None` when absent.
452 #[serde(default)]
453 pub if_this_is: Option<Arc<Type>>,
454 /// `@psalm-self-out Type` / `@phpstan-self-out Type` — the receiver's type
455 /// after this call returns (e.g. a fluent builder that narrows `$this` as
456 /// it's configured). `None` when absent.
457 #[serde(default)]
458 pub self_out: Option<Arc<Type>>,
459 /// True when the method has `@inheritDoc` / `{@inheritDoc}` in its docblock.
460 /// The analyzer inherits the parent's return type, param types, throws, and
461 /// template params when this method has none of its own.
462 #[serde(default)]
463 pub is_inherit_doc: bool,
464 /// `@psalm-mutation-free` / `@phpstan-mutation-free` — this method must not
465 /// assign to `$this` properties (same constraint as `@psalm-immutable` on the
466 /// class, but scoped to this single method).
467 #[serde(default)]
468 pub is_mutation_free: bool,
469 /// `@psalm-external-mutation-free` — this method must not mutate any objects
470 /// passed as arguments, but is allowed to modify `$this`.
471 #[serde(default)]
472 pub is_external_mutation_free: bool,
473 /// Method names referenced via `@dataProvider name` / `#[DataProvider('name')]`
474 /// (PHPUnit) — treated as used by dead-code analysis, since PHPUnit invokes
475 /// them by name through reflection rather than a direct call site.
476 #[serde(default)]
477 pub data_provider_targets: Vec<Arc<str>>,
478}
479
480impl MethodDef {
481 pub fn effective_return_type(&self) -> Option<&Type> {
482 self.return_type
483 .as_deref()
484 .or(self.inferred_return_type.as_deref())
485 }
486}
487
488// ---------------------------------------------------------------------------
489// PropertyDef
490// ---------------------------------------------------------------------------
491
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493pub struct PropertyDef {
494 pub name: Arc<str>,
495 /// Declared/inferred/default types. Stored as `Option<Arc<Type>>` (8 B)
496 /// rather than inline `Option<Type>` (176 B, no niche) and interned via the
497 /// global pool on construction/deserialization — common property types
498 /// (`string`, `int`, a shared class type) dedup to one allocation. Mirrors
499 /// `DeclaredParam::ty`. On-disk format is unchanged (the serde helpers (de)serialize
500 /// the inner `Type` transparently).
501 #[serde(
502 deserialize_with = "deserialize_param_type",
503 serialize_with = "serialize_param_type"
504 )]
505 pub ty: Option<Arc<Type>>,
506 #[serde(
507 deserialize_with = "deserialize_param_type",
508 serialize_with = "serialize_param_type"
509 )]
510 pub inferred_ty: Option<Arc<Type>>,
511 pub visibility: Visibility,
512 pub is_static: bool,
513 pub is_readonly: bool,
514 #[serde(
515 deserialize_with = "deserialize_param_type",
516 serialize_with = "serialize_param_type"
517 )]
518 pub default: Option<Arc<Type>>,
519 pub location: Option<Location>,
520 /// `@deprecated` docblock annotation, if present.
521 #[serde(default)]
522 pub deprecated: Option<Arc<str>>,
523 /// True when the property declares a PHP native type hint (`public int $x`).
524 /// A property typed only via a `@var` docblock (or untyped entirely) is
525 /// `false`: PHP gives such a property an implicit `null` default, so it is
526 /// never "uninitialized" (no MissingConstructor) and accepts `null` on
527 /// assignment regardless of the advisory docblock type.
528 #[serde(default)]
529 pub has_native_type: bool,
530 /// True when this entry was synthesised from a `@property` / `@property-read` /
531 /// `@property-write` docblock tag rather than a real PHP property declaration.
532 /// Such entries describe magic properties accessible via `__get`/`__set` and
533 /// do **not** participate in PHP's inheritance visibility rules.
534 #[serde(default)]
535 pub from_docblock: bool,
536 /// True when `readonly` comes from a native PHP keyword (`readonly` modifier or
537 /// `readonly class`). False when only a `@readonly` docblock annotation is present.
538 /// Distinguishes PHP-enforced read-only from advisory documentation.
539 #[serde(default)]
540 pub has_native_readonly: bool,
541 /// The PHP native type hint alone, with any `@var` docblock refinement stripped —
542 /// `None` when `has_native_type` is false. `ty` mixes in the docblock type when
543 /// present, which makes it unsuitable for checking PHP's redeclared-property
544 /// type invariance rule: that rule is enforced by the runtime purely on the
545 /// native hint, never on the (unenforced) docblock annotation.
546 #[serde(default)]
547 #[serde(
548 deserialize_with = "deserialize_param_type",
549 serialize_with = "serialize_param_type"
550 )]
551 pub native_ty: Option<Arc<Type>>,
552}
553
554// ---------------------------------------------------------------------------
555// ConstantDef
556// ---------------------------------------------------------------------------
557
558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
559pub struct ConstantDef {
560 pub name: Arc<str>,
561 pub ty: Type,
562 pub visibility: Option<Visibility>,
563 #[serde(default)]
564 pub is_final: bool,
565 pub location: Option<Location>,
566 /// `@deprecated` docblock annotation, if present.
567 #[serde(default)]
568 pub deprecated: Option<Arc<str>>,
569}
570
571// ---------------------------------------------------------------------------
572// ClassDef
573// ---------------------------------------------------------------------------
574
575#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
576pub struct ClassDef {
577 pub fqcn: Arc<str>,
578 pub short_name: Arc<str>,
579 pub parent: Option<Arc<str>>,
580 pub interfaces: Vec<Arc<str>>,
581 pub traits: Vec<Arc<str>>,
582 pub own_methods: MemberMap<Arc<MethodDef>>,
583 pub own_properties: MemberMap<PropertyDef>,
584 pub own_constants: MemberMap<ConstantDef>,
585 #[serde(default)]
586 pub mixins: Vec<Arc<str>>,
587 pub template_params: Vec<TemplateParam>,
588 /// Type arguments from `@extends ParentClass<T1, T2>` — maps parent's template params to concrete types.
589 pub extends_type_args: Vec<Type>,
590 /// Type arguments from `@implements Interface<T1, T2>`.
591 #[serde(default)]
592 pub implements_type_args: Vec<(Arc<str>, Vec<Type>)>,
593 /// Type arguments from `@use TraitName<T1, T2>`, keyed by the used
594 /// trait's FQCN — a class's `use` clause (unlike `@extends`) may name
595 /// several traits at once.
596 #[serde(default)]
597 pub trait_use_type_args: Vec<(Arc<str>, Vec<Type>)>,
598 pub is_abstract: bool,
599 pub is_final: bool,
600 pub is_readonly: bool,
601 pub deprecated: Option<Arc<str>>,
602 pub is_internal: bool,
603 /// Set when the class carries `@psalm-immutable` — non-constructor methods must not
604 /// assign to `$this` properties.
605 #[serde(default)]
606 pub is_immutable: bool,
607 /// Attribute target flags if this class has `#[Attribute]` annotation.
608 /// `None` = not an attribute class. The value is a bitmask of PHP's
609 /// `Attribute::TARGET_*` constants (e.g. `Attribute::TARGET_CLASS = 1`).
610 #[serde(default)]
611 pub attribute_flags: Option<i64>,
612 pub location: Option<Location>,
613 /// Per-`use` statement locations for each used trait: `(fqcn, location)` in
614 /// declaration order, parallel to `traits`. Absent from older serialized
615 /// slices; defaults to empty.
616 #[serde(default)]
617 pub trait_use_locations: Vec<(Arc<str>, Location)>,
618 /// Type aliases declared on this class via `@psalm-type` / `@phpstan-type`.
619 #[serde(default)]
620 pub type_aliases: FxHashMap<Arc<str>, Type>,
621 /// Raw import-type declarations (`(local_name, original_name, from_class)`) — resolved during finalization.
622 #[serde(default)]
623 pub pending_import_types: Vec<(Arc<str>, Arc<str>, Arc<str>)>,
624 /// Trait precedence exclusions from `insteadof` declarations in this class's `use` blocks.
625 /// Maps method_name_lowercase → list of trait FQCNs whose version of the method is excluded.
626 /// E.g. `use A, B { B::hello insteadof A; }` stores `"hello" → ["A"]`.
627 #[serde(default)]
628 pub trait_insteadof: MemberMap<Vec<Arc<str>>>,
629 /// Trait method aliases from `as` declarations in this class's `use` blocks.
630 /// Maps new_name_lowercase → (optional_trait_fqcn, original_method_name_lowercase, visibility_override, alias_cased).
631 /// `alias_cased` is the alias name preserving the original PHP casing (for error messages / case checks).
632 /// Visibility is `None` when the `as` clause only renames without changing visibility.
633 /// E.g. `use Base { __construct as __constructBase; }` stores
634 /// `"__constructbase" → (None, "__construct", None, "__constructBase")`.
635 /// E.g. `use T { foo as private traitFoo; }` stores
636 /// `"traitfoo" → (None, "foo", Some(Private), "traitFoo")`.
637 #[serde(default)]
638 #[allow(clippy::type_complexity)]
639 pub trait_aliases:
640 FxHashMap<Arc<str>, (Option<Arc<str>>, Arc<str>, Option<Visibility>, Arc<str>)>,
641}
642
643impl ClassDef {
644 pub fn get_method(&self, name: &str) -> Option<&MethodDef> {
645 // PHP method names are case-insensitive; caller should pass lowercase name.
646 // Only searches own_methods — inherited method resolution is done by
647 // `db::lookup_method_in_chain`.
648 self.own_methods.get(name).map(Arc::as_ref).or_else(|| {
649 self.own_methods
650 .iter()
651 .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
652 .map(|(_, v)| v.as_ref())
653 })
654 }
655
656 pub fn get_property(&self, name: &str) -> Option<&PropertyDef> {
657 self.own_properties.get(name)
658 }
659}
660
661// ---------------------------------------------------------------------------
662// InterfaceDef
663// ---------------------------------------------------------------------------
664
665#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
666pub struct InterfaceDef {
667 pub fqcn: Arc<str>,
668 pub short_name: Arc<str>,
669 pub extends: Vec<Arc<str>>,
670 pub own_methods: MemberMap<Arc<MethodDef>>,
671 pub own_constants: MemberMap<ConstantDef>,
672 pub template_params: Vec<TemplateParam>,
673 pub location: Option<Location>,
674 /// `@deprecated` docblock annotation, if present.
675 #[serde(default)]
676 pub deprecated: Option<Arc<str>>,
677 /// Properties declared via `@property*` docblock annotations on the interface.
678 #[serde(default)]
679 pub own_properties: MemberMap<PropertyDef>,
680 /// `@seal-properties` / `@psalm-seal-properties` — disallows undeclared property access.
681 #[serde(default)]
682 pub seal_properties: bool,
683 /// Type arguments from `@extends BaseIface<T1, T2>` docblock lines, keyed by the
684 /// extended interface's FQCN — an interface's native `extends` list (unlike a
685 /// class's single parent) may name several base interfaces at once.
686 #[serde(default)]
687 pub extends_type_args: Vec<(Arc<str>, Vec<Type>)>,
688 /// Type aliases declared on this interface via `@psalm-type` / `@phpstan-type`.
689 #[serde(default)]
690 pub type_aliases: FxHashMap<Arc<str>, Type>,
691}
692
693// ---------------------------------------------------------------------------
694// TraitDef
695// ---------------------------------------------------------------------------
696
697#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
698pub struct TraitDef {
699 pub fqcn: Arc<str>,
700 pub short_name: Arc<str>,
701 pub own_methods: MemberMap<Arc<MethodDef>>,
702 pub own_properties: MemberMap<PropertyDef>,
703 pub own_constants: MemberMap<ConstantDef>,
704 pub template_params: Vec<TemplateParam>,
705 /// Traits used by this trait (`use OtherTrait;` inside a trait body).
706 pub traits: Vec<Arc<str>>,
707 pub location: Option<Location>,
708 /// Per-`use` statement locations for each used trait: `(fqcn, location)` in
709 /// declaration order, parallel to `traits`. Mirrors `ClassDef`/`EnumDef`'s
710 /// field of the same name. Absent from older serialized slices; defaults
711 /// to empty.
712 #[serde(default)]
713 pub trait_use_locations: Vec<(Arc<str>, Location)>,
714 /// Type arguments from `@use OtherTrait<T1, T2>` (a trait may itself
715 /// `use` a generic trait).
716 #[serde(default)]
717 pub trait_use_type_args: Vec<(Arc<str>, Vec<Type>)>,
718 /// `@psalm-require-extends` / `@phpstan-require-extends` — FQCNs that using classes must extend.
719 #[serde(default)]
720 pub require_extends: Vec<Arc<str>>,
721 /// `@psalm-require-implements` / `@phpstan-require-implements` — FQCNs that using classes must implement.
722 #[serde(default)]
723 pub require_implements: Vec<Arc<str>>,
724 /// `@deprecated` docblock annotation, if present.
725 #[serde(default)]
726 pub deprecated: Option<Arc<str>>,
727 /// Type aliases declared on this trait via `@psalm-type` / `@phpstan-type`.
728 #[serde(default)]
729 pub type_aliases: FxHashMap<Arc<str>, Type>,
730}
731
732// ---------------------------------------------------------------------------
733// EnumDef
734// ---------------------------------------------------------------------------
735
736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
737pub struct EnumCaseDef {
738 pub name: Arc<str>,
739 pub value: Option<Type>,
740 pub location: Option<Location>,
741 /// `@deprecated` docblock annotation, if present.
742 #[serde(default)]
743 pub deprecated: Option<Arc<str>>,
744}
745
746#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
747pub struct EnumDef {
748 pub fqcn: Arc<str>,
749 pub short_name: Arc<str>,
750 pub scalar_type: Option<Type>,
751 pub interfaces: Vec<Arc<str>>,
752 /// Type arguments from `@implements Interface<T1, T2>`.
753 #[serde(default)]
754 pub implements_type_args: Vec<(Arc<str>, Vec<Type>)>,
755 pub cases: MemberMap<EnumCaseDef>,
756 pub own_methods: MemberMap<Arc<MethodDef>>,
757 pub own_constants: MemberMap<ConstantDef>,
758 /// `use SomeTrait;` declarations. PHP enums may use traits (for methods),
759 /// just never carry instance properties from them.
760 #[serde(default)]
761 pub traits: Vec<Arc<str>>,
762 #[serde(default)]
763 pub trait_use_locations: Vec<(Arc<str>, Location)>,
764 /// Type arguments from `@use SomeTrait<T1, T2>`.
765 #[serde(default)]
766 pub trait_use_type_args: Vec<(Arc<str>, Vec<Type>)>,
767 pub location: Option<Location>,
768 /// `@deprecated` docblock annotation (or `#[Deprecated]` attribute), if present.
769 #[serde(default)]
770 pub deprecated: Option<Arc<str>>,
771 /// Type aliases declared on this enum via `@psalm-type` / `@phpstan-type`.
772 #[serde(default)]
773 pub type_aliases: FxHashMap<Arc<str>, Type>,
774 /// Properties declared via `@property*` docblock annotations on the enum.
775 #[serde(default)]
776 pub own_properties: MemberMap<PropertyDef>,
777}
778
779// ---------------------------------------------------------------------------
780// FunctionDef
781// ---------------------------------------------------------------------------
782
783#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
784pub struct FunctionDef {
785 pub fqn: Arc<str>,
786 pub short_name: Arc<str>,
787 #[serde(
788 deserialize_with = "deserialize_params",
789 serialize_with = "serialize_params"
790 )]
791 pub params: Arc<[DeclaredParam]>,
792 /// Type from annotation (`@return` / native type hint). `None` means unannotated.
793 /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types.
794 #[serde(
795 deserialize_with = "deserialize_return_type",
796 serialize_with = "serialize_return_type"
797 )]
798 pub return_type: Option<Arc<Type>>,
799 /// See `MethodDef::inferred_return_type` — `Option<Arc<Type>>` (8 B) for the
800 /// same demand-driven-inference reason.
801 #[serde(
802 deserialize_with = "deserialize_return_type",
803 serialize_with = "serialize_return_type"
804 )]
805 pub inferred_return_type: Option<Arc<Type>>,
806 pub template_params: Vec<TemplateParam>,
807 pub assertions: Vec<Assertion>,
808 pub throws: Vec<Arc<str>>,
809 pub deprecated: Option<Arc<str>>,
810 pub is_pure: bool,
811 /// `@no-named-arguments` — callers must not use named argument syntax.
812 #[serde(default)]
813 pub no_named_arguments: bool,
814 pub location: Option<Location>,
815 /// Plain-text description from the docblock (text before `@tag` lines).
816 /// Used for hover info.
817 #[serde(default)]
818 pub docstring: Option<Arc<str>>,
819 /// Parameters declared as taint sinks via `@taint-sink <kind> $param`.
820 /// Each entry is `(param_name_without_dollar, sink_kind_string)`.
821 #[serde(default)]
822 pub taint_sink_params: Vec<(Arc<str>, Arc<str>)>,
823 /// Type aliases declared on this function via `@psalm-type` / `@phpstan-type`.
824 #[serde(default)]
825 pub type_aliases: FxHashMap<Arc<str>, Type>,
826}
827
828impl FunctionDef {
829 pub fn effective_return_type(&self) -> Option<&Type> {
830 self.return_type
831 .as_deref()
832 .or(self.inferred_return_type.as_deref())
833 }
834}
835
836// ---------------------------------------------------------------------------
837// StubSlice — serializable bundle of definitions from one extension's stubs
838// ---------------------------------------------------------------------------
839
840/// A snapshot of all PHP definitions contributed by a single stub file set.
841///
842/// Produced by `mir-stubs-gen` at code-generation time and deserialized at
843/// runtime to ingest definitions into the salsa db via
844/// `MirDatabase::ingest_stub_slice`.
845#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
846pub struct StubSlice {
847 pub classes: Vec<Arc<ClassDef>>,
848 pub interfaces: Vec<Arc<InterfaceDef>>,
849 pub traits: Vec<Arc<TraitDef>>,
850 pub enums: Vec<Arc<EnumDef>>,
851 pub functions: Vec<Arc<FunctionDef>>,
852 #[serde(default)]
853 pub constants: Vec<(Arc<str>, Type)>,
854 /// Source file this slice was collected from. `None` for bundled stub slices
855 /// that were pre-computed and are not tied to a specific on-disk file.
856 #[serde(default)]
857 pub file: Option<Arc<str>>,
858 /// Types of `@var`-annotated global variables collected from this file.
859 /// Populated by `DefinitionCollector`; ingested into the salsa db's
860 /// `global_vars` table by `ingest_stub_slice` when `file` is `Some`.
861 #[serde(default)]
862 pub global_vars: Vec<(Arc<str>, Type)>,
863 /// The first namespace declared in this file (e.g. `"App\\Service"`).
864 /// Populated by `DefinitionCollector`; ingested into the salsa db's
865 /// `file_namespaces` table by `ingest_stub_slice` when `file` is `Some`.
866 #[serde(default)]
867 pub namespace: Option<Arc<str>>,
868 /// `use` alias map for this file: alias → FQCN.
869 ///
870 /// Stored as `Arc<FxHashMap<Name, Name>>` so that `file_imports()`
871 /// returns a cheap Arc clone instead of deep-cloning the map on every
872 /// `resolve_name` call (which fires once per symbol reference in
873 /// Pass 2). `Name` keys/values shrink each entry from ~108 bytes
874 /// (two `String` headers + two heap allocs averaging ~30 chars) to
875 /// 16 bytes (two `Ustr` u64 handles); the global ustr interner holds
876 /// one copy of each unique alias / FQCN string for the whole session.
877 #[serde(
878 deserialize_with = "deserialize_imports",
879 serialize_with = "serialize_imports"
880 )]
881 #[serde(default = "default_imports")]
882 pub imports: Arc<FxHashMap<Name, Name>>,
883 /// Subset of `imports` containing only `use` items that import a
884 /// class/interface/trait/enum (`UseKind::Normal`) — excludes `use
885 /// function`/`use const` aliases. Class-name resolution consults this
886 /// instead of `imports` so a function/constant import can't shadow a
887 /// same-named class reference (`use function Foo\bar;` must not make an
888 /// unrelated `bar` type hint resolve to `Foo\bar`).
889 #[serde(
890 deserialize_with = "deserialize_imports",
891 serialize_with = "serialize_imports"
892 )]
893 #[serde(default = "default_imports")]
894 pub class_imports: Arc<FxHashMap<Name, Name>>,
895 /// Set to `true` after `deduplicate_params_in_slice` has run on this slice.
896 /// `ingest_stub_slice` skips the clone+re-dedup when this flag is set.
897 #[serde(skip)]
898 pub is_deduped: bool,
899}
900
901// ---------------------------------------------------------------------------
902// Param list deduplication
903// ---------------------------------------------------------------------------
904
905use std::sync::Mutex;
906
907type ParamCache = Mutex<FxHashMap<Vec<DeclaredParam>, Arc<[DeclaredParam]>>>;
908
909/// Global cache of canonical Arc<[DeclaredParam]> instances for deduplication.
910/// Shared across all StubSlices to deduplicate vendor code with millions of
911/// methods that often have identical parameter lists.
912static PARAM_DEDUP_CACHE: std::sync::OnceLock<ParamCache> = std::sync::OnceLock::new();
913
914/// Deduplicate parameter lists across all methods and functions in a StubSlice.
915/// Many PHP framework methods share identical parameter lists (e.g., thousands
916/// of `(string $arg, array $opts)` signatures). This function groups identical
917/// param lists globally (across all slices processed so far) and replaces them
918/// with Arc<[DeclaredParam]> pointers to shared allocations.
919///
920/// Expected memory savings: 100–150 MiB on cold start (vendor collection).
921pub fn deduplicate_params_in_slice(slice: &mut StubSlice) {
922 let cache: &ParamCache = PARAM_DEDUP_CACHE.get_or_init(|| Mutex::new(FxHashMap::default()));
923 let mut canonical_params = cache.lock().unwrap();
924
925 let mut deduplicate = |params: &mut Arc<[DeclaredParam]>| {
926 if let Some(existing) = canonical_params.get(params.as_ref()) {
927 *params = existing.clone();
928 } else {
929 canonical_params.insert(params.as_ref().to_vec(), params.clone());
930 }
931 };
932
933 // Deduplicate method params in all classes
934 for cls in &mut slice.classes {
935 for method in Arc::make_mut(cls).own_methods.values_mut() {
936 deduplicate(&mut Arc::make_mut(method).params);
937 }
938 }
939
940 // Deduplicate method params in all interfaces
941 for iface in &mut slice.interfaces {
942 for method in Arc::make_mut(iface).own_methods.values_mut() {
943 deduplicate(&mut Arc::make_mut(method).params);
944 }
945 }
946
947 // Deduplicate method params in all traits
948 for tr in &mut slice.traits {
949 for method in Arc::make_mut(tr).own_methods.values_mut() {
950 deduplicate(&mut Arc::make_mut(method).params);
951 }
952 }
953
954 // Deduplicate method params in all enums
955 for en in &mut slice.enums {
956 for method in Arc::make_mut(en).own_methods.values_mut() {
957 deduplicate(&mut Arc::make_mut(method).params);
958 }
959 }
960
961 // Deduplicate function params
962 for func in &mut slice.functions {
963 deduplicate(&mut Arc::make_mut(func).params);
964 }
965 slice.is_deduped = true;
966}