Skip to main content

mir_analyzer/session/
stubs.rs

1use super::*;
2
3impl AnalysisSession {
4    /// Deprecated — stub loading is now fully lazy per-AST.
5    ///
6    /// This is an alias for [`Self::ensure_all_stubs`] kept for API
7    /// compatibility. Internal analysis paths use [`Self::prepare_ast_for_analysis`]
8    /// which loads only the stubs referenced by the file under analysis.
9    #[deprecated(note = "use ensure_all_stubs() or ensure_stubs_for_ast() instead")]
10    pub fn ensure_essential_stubs(&mut self) {
11        self.ensure_all_stubs();
12    }
13
14    /// Load every embedded PHP stub plus any configured user stubs.
15    /// Use for batch tools (CLI, full project analysis) where comprehensive
16    /// symbol coverage matters more than cold-start latency.
17    pub fn ensure_all_stubs(&mut self) {
18        let paths: Vec<&'static str> = crate::stubs::stub_files().iter().map(|&(p, _)| p).collect();
19        self.db.ingest_stub_paths(&paths, self.php_version);
20        self.ensure_user_stubs_loaded();
21    }
22
23    /// Ensure the embedded stub that defines `name` (a function) is ingested.
24    /// Returns `true` when a matching stub exists (whether or not it was
25    /// already loaded), `false` when `name` isn't a known PHP built-in.
26    ///
27    /// Most callers should use [`Self::ensure_stubs_for_ast`] instead —
28    /// it auto-discovers needed stubs from a parsed file.
29    #[doc(hidden)]
30    pub fn ensure_stub_for_function(&mut self, name: &str) -> bool {
31        match crate::stubs::stub_path_for_function(name) {
32            Some(path) => {
33                self.db.ingest_stub_paths(&[path], self.php_version);
34                true
35            }
36            None => false,
37        }
38    }
39
40    /// Ensure the embedded stub that defines `fqcn` (a class / interface /
41    /// trait / enum) is ingested. Case-insensitive lookup with optional
42    /// leading backslash.
43    ///
44    /// Most callers should use [`Self::ensure_stubs_for_ast`] instead.
45    #[doc(hidden)]
46    pub fn ensure_stub_for_class(&mut self, fqcn: &str) -> bool {
47        match crate::stubs::stub_path_for_class(fqcn) {
48            Some(path) => {
49                self.db.ingest_stub_paths(&[path], self.php_version);
50                true
51            }
52            None => false,
53        }
54    }
55
56    /// Ensure the embedded stub that defines `name` (a constant) is ingested.
57    ///
58    /// Most callers should use [`Self::ensure_stubs_for_ast`] instead.
59    #[doc(hidden)]
60    pub fn ensure_stub_for_constant(&mut self, name: &str) -> bool {
61        match crate::stubs::stub_path_for_constant(name) {
62            Some(path) => {
63                self.db.ingest_stub_paths(&[path], self.php_version);
64                true
65            }
66            None => false,
67        }
68    }
69
70    /// Number of distinct embedded stubs currently ingested into the session.
71    /// Useful for diagnostics and bench reporting.
72    pub fn loaded_stub_count(&self) -> usize {
73        self.db.loaded_stubs.lock().len()
74    }
75
76    /// Auto-discover and ingest the embedded stubs needed to cover every
77    /// built-in PHP function / class / constant referenced by `source`.
78    ///
79    /// Used by the open-file analysis paths
80    /// ([`crate::FileAnalyzer::analyze`] and
81    /// [`crate::FileAnalyzer::analyze_diagnostics_only`]) to keep
82    /// essentials-only mode correct without forcing callers to enumerate which
83    /// stubs they need.
84    /// Idempotent — already-loaded stubs are skipped via [`Self::loaded_stubs`].
85    ///
86    /// The discovery scan is a coarse identifier sweep (see
87    /// [`crate::stubs::collect_referenced_builtin_paths`]) — it may pull in
88    /// a slightly larger set than the file strictly needs, but never misses
89    /// a referenced built-in. Cost is sub-millisecond per file.
90    ///
91    /// Fast path: if every embedded stub is already loaded (e.g. after a
92    /// batch tool called [`Self::ensure_all_stubs`]), the source scan
93    /// is skipped entirely.
94    pub fn ensure_stubs_for_source(&mut self, source: &str) {
95        // Cheap check first: skip the scan entirely when we already know we
96        // have everything. Avoids a ~50-500µs source walk on every analyze
97        // call in batch / warm-session scenarios.
98        {
99            let loaded = self.db.loaded_stubs.lock();
100            if loaded.len() >= crate::stubs::stub_files().len() {
101                return;
102            }
103        }
104        let paths = crate::stubs::collect_referenced_builtin_paths(source);
105        if paths.is_empty() {
106            return;
107        }
108        self.db.ingest_stub_paths(&paths, self.php_version);
109    }
110
111    /// Discover and ingest stubs by walking the parsed AST of a PHP file.
112    ///
113    /// Similar to [`Self::ensure_stubs_for_source`], but takes an already-parsed
114    /// AST instead of raw source text. Produces zero false positives since it
115    /// only extracts identifiers from actual AST nodes (not from strings or
116    /// comments). Preferred over `ensure_stubs_for_source` when the AST is
117    /// already available (e.g., in [`crate::FileAnalyzer`]).
118    ///
119    /// Idempotent and skips the scan if all stubs are already loaded.
120    pub fn ensure_stubs_for_ast(&mut self, program: &php_ast::owned::Program) {
121        {
122            let loaded = self.db.loaded_stubs.lock();
123            if loaded.len() >= crate::stubs::stub_files().len() {
124                return;
125            }
126        }
127        let paths = crate::stubs::collect_referenced_builtin_paths_from_ast(program);
128        if paths.is_empty() {
129            return;
130        }
131        self.db.ingest_stub_paths(&paths, self.php_version);
132    }
133
134    /// Returns true if this session has a configured class resolver
135    /// (typically a PSR-4 / classmap autoloader chained with the stub
136    /// resolver). Used by `FileAnalyzer` to skip the AST-scan preload
137    /// when no resolver is wired up.
138    pub fn has_resolver(&self) -> bool {
139        self.resolver.is_some()
140    }
141
142    /// Index vendor `autoload.files` entries the first time analysis runs.
143    ///
144    /// Composer's `autoload.files` lists files that define global functions and
145    /// constants (e.g. Laravel helpers). These are invisible to the PSR-4 class
146    /// resolver — there is no function-name → file-path mapping without
147    /// parsing them first.  Rather than per-function lazy resolution, this
148    /// loads all pending vendor eager files at once on the first
149    /// [`Self::prepare_ast_for_analysis`] call.
150    ///
151    /// Subsequent calls see `None` and return immediately (O(1)). Files are
152    /// read via the session's [`crate::SourceProvider`], so LSP VFS overrides
153    /// are respected.
154    pub(crate) fn ensure_vendor_eager_functions(&mut self) {
155        let files = match self.pending_eager_function_files.lock().take() {
156            None => return,
157            Some(f) if f.is_empty() => return,
158            Some(f) => f,
159        };
160        let sources: Vec<(std::sync::Arc<str>, std::sync::Arc<str>)> = files
161            .iter()
162            .filter_map(|p| {
163                let text = self.source_provider.read(p.to_string_lossy().as_ref())?;
164                Some((std::sync::Arc::from(p.to_string_lossy().as_ref()), text))
165            })
166            .collect();
167        if !sources.is_empty() {
168            let cancel = crate::IndexCancel::new();
169            // Rayon: when this is the first `index_batch` call it seeds the
170            // symbol index from every registered file, not just this chunk.
171            self.index_batch(&sources, crate::IndexParallelism::Rayon, &cancel);
172        }
173    }
174
175    /// Run all pre-passes (builtin-stub loading, vendor-eager-file loading,
176    /// and PSR-4 class preloading) before body analysis of a single file.
177    ///
178    /// Replaces the two separate `ensure_stubs_for_ast` /
179    /// `preload_psr4_classes_for_ast` calls at every open-file analysis site.
180    pub fn prepare_ast_for_analysis(&mut self, program: &php_ast::owned::Program, file: &str) {
181        self.ensure_stubs_for_ast(program);
182        self.ensure_vendor_eager_functions();
183        self.priority_index_for_ast(program, file);
184    }
185
186    /// Run `file`'s Phase-1 warm-up — resolve its direct class references and
187    /// lazy-load any not yet indexed — and record it as prepared against its
188    /// current text. No-op when the file is unknown or already prepared.
189    ///
190    /// This is the write-path home of the warm-up: hosts call it (via
191    /// [`Self::ingest_file_prepared`]) when text lands, so read paths
192    /// (`indexed_references_to`, `reanalyze_files_cancellable`) find every
193    /// candidate prepared and stay pure. Loading mutates salsa inputs, so the
194    /// parse snapshot is scoped and dropped before the warm-up runs — callers
195    /// must not hold a live snapshot across this call.
196    pub fn prepare_file_for_analysis(&mut self, path: &std::sync::Arc<str>) {
197        let _ = self.prepare_file_for_analysis_cancellable(path, &|| false);
198    }
199
200    /// Cancellable variant of [`Self::prepare_file_for_analysis`].
201    ///
202    /// The scoped parse snapshot is acquired cooperatively so an obsolete
203    /// background reanalysis cannot remain queued behind a writer which is
204    /// itself waiting for another Salsa snapshot to end.
205    pub(crate) fn prepare_file_for_analysis_cancellable(
206        &mut self,
207        path: &std::sync::Arc<str>,
208        should_cancel: &(dyn Fn() -> bool + Sync),
209    ) -> bool {
210        if should_cancel() {
211            return false;
212        }
213        let generation = self.prepare_generation_snapshot();
214        let (parsed, text) = {
215            if should_cancel() {
216                return false;
217            }
218            let db = self.snapshot_db();
219            let Some(sf) = db.lookup_source_file(path.as_ref()) else {
220                return true;
221            };
222            let text = sf.text(&db as &dyn crate::db::MirDatabase).clone();
223            if self.is_prepared_for_analysis(path.as_ref(), &text, generation) {
224                return true;
225            }
226            (
227                crate::db::parse_file(&db as &dyn crate::db::MirDatabase, sf)
228                    .0
229                    .clone(),
230                text,
231            )
232        };
233        // Nests under pass-level scopes, which coalesce the whole pass.
234        let mut session = self.defer_revision_bumps();
235        session.prepare_ast_for_analysis(&parsed.program, path.as_ref());
236        session.mark_prepared_for_analysis(path, text, generation);
237        true
238    }
239
240    /// Current warm-up generation; capture before a prepare + mark pair so a
241    /// concurrent [`Self::bump_prepare_generation`] invalidates the mark.
242    pub(crate) fn prepare_generation_snapshot(&self) -> u64 {
243        self.prepare_generation
244            .load(std::sync::atomic::Ordering::Acquire)
245    }
246
247    /// Whether `file`'s warm-up already ran against `current_text` in
248    /// `generation`. See the `prepared_files` field docs for the invalidation
249    /// rules.
250    pub(crate) fn is_prepared_for_analysis(
251        &self,
252        file: &str,
253        current_text: &std::sync::Arc<str>,
254        generation: u64,
255    ) -> bool {
256        self.prepared_files
257            .read()
258            .get(file)
259            .is_some_and(|(text, prepared_gen)| {
260                *prepared_gen == generation && std::sync::Arc::ptr_eq(text, current_text)
261            })
262    }
263
264    /// Record that `file`'s warm-up ran against `text`. `generation` must be
265    /// the [`Self::prepare_generation_snapshot`] taken *before* the warm-up —
266    /// a bump in between leaves the entry stale, which is the safe direction.
267    pub(crate) fn mark_prepared_for_analysis(
268        &self,
269        file: &std::sync::Arc<str>,
270        text: std::sync::Arc<str>,
271        generation: u64,
272    ) {
273        self.prepared_files
274            .write()
275            .insert(file.clone(), (text, generation));
276    }
277
278    /// Drop `file`'s warm-up skip entry (its loaded state is being removed).
279    pub(crate) fn forget_prepared(&self, file: &str) {
280        self.prepared_files.write().remove(file);
281    }
282
283    /// Invalidate every warm-up skip entry. Call when previously loaded
284    /// declarations may have been removed outside [`Self::ingest_file`] /
285    /// [`Self::invalidate_file`] — e.g. a host that writes file text straight
286    /// into the salsa layer and detects a declaration-level change. A prepared
287    /// file might then need its warm-up re-run to lazy-load a replacement
288    /// (a vendor class shadowed by a since-deleted project class).
289    pub fn bump_prepare_generation(&self) {
290        self.clear_transient_batch_replay();
291        self.prepare_generation
292            .fetch_add(1, std::sync::atomic::Ordering::Release);
293    }
294
295    /// Priority-index the classes directly referenced by `file`'s AST.
296    ///
297    /// In the eager-static-input model the background indexer
298    /// ([`Self::index_batch`]) walks the whole vendor tree, but it may not have
299    /// reached every file the open buffer references yet. To avoid a transient
300    /// false `UndefinedClass` during the warm-up window, this **reorders** that
301    /// static work: it resolves the buffer's *direct* class references and
302    /// loads any not-yet-indexed ones immediately, jumping them to the front of
303    /// the queue.
304    ///
305    /// This is bounded by the number of distinct direct references in **one**
306    /// file — no transitive BFS, no depth/total budget, no pinning. Inheritance
307    /// ancestors and signature types of those classes are picked up by the
308    /// background walk (or, for navigation, by [`Self::hover`] /
309    /// [`Self::definition_of`]). Because `bump_workspace_revision` no longer
310    /// nulls the workspace index singleton, each [`Self::load_class`] here costs
311    /// only a resolver lookup + parse (or cache hit) + one tier-aware merge,
312    /// invalidating just the actively-analyzed file's memo once — not the whole
313    /// cache. Once background indexing completes this is a no-op (every
314    /// reference already resolves).
315    pub fn priority_index_for_ast(&mut self, program: &php_ast::owned::Program, file: &str) {
316        if self.resolver.is_none() {
317            return;
318        }
319        let refs = collect_class_refs_from_ast(program);
320        if refs.is_empty() {
321            return;
322        }
323        // Resolve names against the file's namespace/imports up front, then
324        // drop the snapshot before loading (which mutates inputs).
325        let resolved: Vec<String> = {
326            let db = self.snapshot_db();
327            refs.into_iter()
328                .map(|raw| crate::db::resolve_name(&db, file, &raw))
329                .collect()
330        };
331        for fqcn in resolved {
332            // load_class is a no-op when the class is already indexed (the
333            // common case once the background walk has passed this file).
334            self.load_class(&fqcn);
335        }
336    }
337
338    fn ensure_user_stubs_loaded(&mut self) {
339        self.db
340            .ingest_user_stubs(&self.user_stub_files, &self.user_stub_dirs);
341    }
342}