mir_analyzer/db/nodes.rs
1use std::sync::Arc;
2
3use mir_codebase::storage::{Assertion, FnParam, Location, TemplateParam, Visibility};
4use mir_codebase::StubSlice;
5use mir_issues::Issue;
6use mir_types::Union;
7
8// SourceFile input (S1)
9
10/// Source file registered as a Salsa input.
11/// Setting `text` on an existing `SourceFile` is the single write that drives
12/// all downstream query invalidation.
13#[salsa::input]
14pub struct SourceFile {
15 pub path: Arc<str>,
16 pub text: Arc<str>,
17}
18
19// FileDefinitions (S1)
20
21/// Result of the `collect_file_definitions` tracked query.
22#[derive(Clone, Debug)]
23pub struct FileDefinitions {
24 pub slice: Arc<StubSlice>,
25 pub issues: Arc<Vec<Issue>>,
26}
27
28impl PartialEq for FileDefinitions {
29 fn eq(&self, other: &Self) -> bool {
30 Arc::ptr_eq(&self.slice, &other.slice) && Arc::ptr_eq(&self.issues, &other.issues)
31 }
32}
33
34// SAFETY: FileDefinitions contains Arc pointers and Vec, which are Move-safe.
35// The pointer passed to maybe_update is provided by Salsa and points to
36// properly aligned and initialized memory. We have exclusive write access
37// through the mutable pointer (Salsa guarantees this). The in-place update
38// is safe because we own both the old and new values.
39unsafe impl salsa::Update for FileDefinitions {
40 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
41 unsafe { *old_pointer = new_value };
42 true
43 }
44}
45
46// ClassNode input (S2)
47
48/// `(interface_fqcn, type_args)` pairs from `@implements Iface<T1, T2>`
49/// docblocks. Stored on `ClassNode` for classes only.
50pub type ImplementsTypeArgs = Arc<[(Arc<str>, Arc<[Union]>)]>;
51
52/// Salsa input representing a single class or interface in the inheritance
53/// graph. Fields are kept minimal — only what `class_ancestors` needs.
54///
55/// Invariant: every FQCN in the codebase that is known to the Salsa DB has
56/// exactly one `ClassNode` handle, stored in `MirDb::class_nodes`. When a
57/// class is removed (file deleted or re-indexed), its node is marked
58/// `active = false` rather than dropped, so dependent `class_ancestors` queries
59/// can still observe the change and re-run.
60#[salsa::input]
61pub struct ClassNode {
62 pub fqcn: Arc<str>,
63 /// `false` when the class has been removed from the codebase. Dependent
64 /// queries observe this change and re-run, returning empty ancestors.
65 pub active: bool,
66 pub is_interface: bool,
67 /// `true` for trait nodes. Traits don't currently participate in the
68 /// `class_ancestors` query (it returns empty for traits), but registering
69 /// them as `ClassNode`s lets callers answer `type_exists`-style questions
70 /// through the db.
71 pub is_trait: bool,
72 /// `true` for enum nodes. See note on `is_trait`.
73 pub is_enum: bool,
74 /// `true` if the class is declared `abstract`. Always `false` for
75 /// interfaces, traits, and enums.
76 pub is_abstract: bool,
77 /// Direct parent class (classes only; `None` for interfaces).
78 pub parent: Option<Arc<str>>,
79 /// Directly implemented interfaces (classes only).
80 pub interfaces: Arc<[Arc<str>]>,
81 /// Used traits (classes only). Traits are added to the ancestor list but
82 /// their own ancestors are not recursed into, matching PHP semantics.
83 pub traits: Arc<[Arc<str>]>,
84 /// Directly extended interfaces (interfaces only).
85 pub extends: Arc<[Arc<str>]>,
86 /// Declared `@template` parameters from the class/interface/trait
87 /// docblock. Empty for classes without templates.
88 pub template_params: Arc<[TemplateParam]>,
89 /// `@psalm-require-extends` / `@phpstan-require-extends` — FQCNs that
90 /// using classes must extend. Populated for trait nodes only; empty for
91 /// classes/interfaces/enums.
92 pub require_extends: Arc<[Arc<str>]>,
93 /// `@psalm-require-implements` / `@phpstan-require-implements` — FQCNs
94 /// that using classes must implement. Populated for trait nodes only;
95 /// empty for classes/interfaces/enums.
96 pub require_implements: Arc<[Arc<str>]>,
97 /// `true` if this is a *backed* enum (declared with a scalar type).
98 /// Always `false` for non-enum nodes and pure (unbacked) enums. Used by
99 /// `extends_or_implements_via_db` to answer the implicit `BackedEnum`
100 /// interface check.
101 pub is_backed_enum: bool,
102 /// `@mixin` / `@psalm-mixin` FQCNs declared on the class docblock.
103 /// Used by `lookup_method_in_chain` for delegated magic-method lookup.
104 /// Empty for interfaces, traits, and enums (mixin is a class-only
105 /// docblock concept).
106 pub mixins: Arc<[Arc<str>]>,
107 /// `@deprecated` message from the class docblock, if any. Mirrors
108 /// `ClassStorage::deprecated`. Empty / `None` for interfaces, traits,
109 /// and enums (S5-PR42 only mirrors the class-level field — those storages
110 /// don't carry a deprecated message).
111 pub deprecated: Option<Arc<str>>,
112 /// For backed-enum nodes: the declared scalar type (`int`/`string`).
113 /// Mirrors `EnumStorage::scalar_type`. `None` for non-enum nodes and
114 /// for unbacked (pure) enums. Used by the `Enum->value` property read
115 /// in `expr.rs` to return the backed scalar type instead of `mixed`.
116 pub enum_scalar_type: Option<Union>,
117 /// `true` if the class is declared `final`. Always `false` for
118 /// interfaces, traits, and enums (PHP enums are implicitly final but the
119 /// codebase doesn't currently track that on `EnumStorage`).
120 pub is_final: bool,
121 /// `true` if the class is declared `readonly`. Always `false` for
122 /// non-class kinds.
123 pub is_readonly: bool,
124 /// Source location of the class declaration. Mirrors
125 /// `ClassStorage::location` (and `InterfaceStorage::location`,
126 /// `TraitStorage::location`, `EnumStorage::location`). Used by
127 /// `ClassAnalyzer` to attribute issues to the right span.
128 pub location: Option<Location>,
129 /// Type arguments from `@extends Parent<T1, T2>` — populated for
130 /// classes only. Mirrors `ClassStorage::extends_type_args`.
131 pub extends_type_args: Arc<[Union]>,
132 /// Type arguments from `@implements Iface<T1, T2>` — populated for
133 /// classes only. Mirrors `ClassStorage::implements_type_args`.
134 pub implements_type_args: ImplementsTypeArgs,
135}
136
137// FunctionNode input (S5-PR2)
138
139/// Salsa input representing a single global function.
140///
141/// `inferred_return_type` is the Pass-2-derived return type, populated
142/// per-function by the priming sweep. It is committed to Salsa serially
143/// after the parallel sweep returns (so worker db clones have dropped
144/// and `Storage::cancel_others` sees strong-count==1). The buffer-and-
145/// commit pattern lives in [`InferredReturnTypes`] and
146/// [`MirDb::commit_inferred_return_types`].
147///
148/// Invariant: every FQN known to the Salsa DB has exactly one `FunctionNode`
149/// handle in `MirDb::function_nodes`. Removed functions are marked
150/// `active = false` rather than dropped.
151#[salsa::input]
152pub struct FunctionNode {
153 pub fqn: Arc<str>,
154 pub short_name: Arc<str>,
155 pub active: bool,
156 pub params: Arc<[FnParam]>,
157 pub return_type: Option<Arc<Union>>,
158 pub inferred_return_type: Option<Arc<Union>>,
159 pub template_params: Arc<[TemplateParam]>,
160 pub assertions: Arc<[Assertion]>,
161 pub throws: Arc<[Arc<str>]>,
162 pub deprecated: Option<Arc<str>>,
163 pub is_pure: bool,
164 /// Source location of the declaration. `None` for functions registered
165 /// without a known origin (e.g. some legacy test fixtures).
166 pub location: Option<Location>,
167}
168
169// MethodNode input (S5-PR3)
170
171/// Salsa input representing a single method or interface/trait method.
172///
173/// `inferred_return_type` is the Pass-2-derived return type, populated per
174/// method by the priming sweep. Committed to Salsa serially after the
175/// parallel sweep returns; see [`FunctionNode`] for the buffer-and-commit
176/// pattern that resolves the historical "S3 deadlock".
177///
178/// The node is keyed by `(fqcn, method_name_lower)` where `fqcn` is the
179/// FQCN of the **owning** class/interface/trait and `method_name_lower` is
180/// the PHP-normalised (lowercased) method name. Nodes for classes that are
181/// removed from the codebase are marked `active = false` via
182/// `deactivate_class_methods` rather than being dropped.
183#[salsa::input]
184pub struct MethodNode {
185 pub fqcn: Arc<str>,
186 pub name: Arc<str>,
187 pub active: bool,
188 pub params: Arc<[FnParam]>,
189 pub return_type: Option<Arc<Union>>,
190 pub inferred_return_type: Option<Arc<Union>>,
191 pub template_params: Arc<[TemplateParam]>,
192 pub assertions: Arc<[Assertion]>,
193 pub throws: Arc<[Arc<str>]>,
194 pub deprecated: Option<Arc<str>>,
195 pub is_internal: bool,
196 pub visibility: Visibility,
197 pub is_static: bool,
198 pub is_abstract: bool,
199 pub is_final: bool,
200 pub is_constructor: bool,
201 pub is_pure: bool,
202 /// Source location of the declaration. `None` for synthesized methods
203 /// (e.g. enum implicit `cases`/`from`/`tryFrom`).
204 pub location: Option<Location>,
205}
206
207// PropertyNode input (S5-PR4)
208
209/// Salsa input representing a single class/trait property.
210///
211/// `inferred_ty` is intentionally absent — it stays in `PropertyStorage` until
212/// a future S3-style tracked query promotes it.
213///
214/// Keyed by `(owner fqcn, prop_name)` — property names are case-sensitive.
215#[salsa::input]
216pub struct PropertyNode {
217 pub fqcn: Arc<str>,
218 pub name: Arc<str>,
219 pub active: bool,
220 pub ty: Option<Union>,
221 pub visibility: Visibility,
222 pub is_static: bool,
223 pub is_readonly: bool,
224 pub location: Option<Location>,
225}
226
227// ClassConstantNode input (S5-PR4)
228
229/// Salsa input representing a single class/interface/enum constant.
230///
231/// Keyed by `(owner fqcn, const_name)` — constant names are case-sensitive.
232#[salsa::input]
233pub struct ClassConstantNode {
234 pub fqcn: Arc<str>,
235 pub name: Arc<str>,
236 pub active: bool,
237 pub ty: Union,
238 pub visibility: Option<Visibility>,
239 pub is_final: bool,
240 /// Source location of the declaration. Mirrors `ConstantStorage::location`
241 /// for class/interface/trait constants, and `EnumCaseStorage::location` for
242 /// enum cases. `None` for nodes registered without a source span.
243 pub location: Option<Location>,
244}
245
246// GlobalConstantNode input (S5-PR47)
247
248/// Salsa input representing a global PHP constant (e.g. `PHP_EOL`).
249/// Mirrors `Codebase::constants`.
250#[salsa::input]
251pub struct GlobalConstantNode {
252 pub fqn: Arc<str>,
253 pub active: bool,
254 pub ty: Union,
255}
256
257// Ancestors return type (S2)
258
259/// The computed ancestor list for a class or interface.
260///
261/// Uses content equality so Salsa's cycle-convergence check can detect
262/// fixpoints correctly (two empty lists from different iterations are equal).
263#[derive(Clone, Debug, Default)]
264pub struct Ancestors(pub Vec<Arc<str>>);
265
266impl PartialEq for Ancestors {
267 fn eq(&self, other: &Self) -> bool {
268 self.0.len() == other.0.len()
269 && self
270 .0
271 .iter()
272 .zip(&other.0)
273 .all(|(a, b)| a.as_ref() == b.as_ref())
274 }
275}
276
277// SAFETY: Ancestors contains Arc pointers, which are Move-safe.
278// The pointer passed to maybe_update is provided by Salsa and points to
279// properly aligned and initialized memory. We dereference it to check equality
280// and conditionally update. Salsa guarantees exclusive write access through
281// the mutable pointer. The comparison is safe because we're comparing valid
282// initialized values.
283unsafe impl salsa::Update for Ancestors {
284 unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
285 let old = unsafe { &mut *old_ptr };
286 if *old == new_val {
287 return false;
288 }
289 *old = new_val;
290 true
291 }
292}