Skip to main content

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}