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