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