Skip to main content

mir_analyzer/session/
stubs.rs

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