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(&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(&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(&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(&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(&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(&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(&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 /// The mutex is held for the duration of the load, so concurrent callers
152 /// block here until the files are indexed. Subsequent calls see `None`
153 /// and return immediately (O(1)). Files are read via the session's
154 /// [`crate::SourceProvider`], so LSP VFS overrides are respected.
155 pub(crate) fn ensure_vendor_eager_functions(&self) {
156 let mut guard = self.pending_eager_function_files.lock();
157 let files = match guard.take() {
158 None => return,
159 Some(f) if f.is_empty() => return,
160 Some(f) => f,
161 };
162 // Guard remains held (now `None`) — concurrent callers block here
163 // until `index_batch` returns and all functions are indexed.
164 let sources: Vec<(std::sync::Arc<str>, std::sync::Arc<str>)> = files
165 .iter()
166 .filter_map(|p| {
167 let text = self.source_provider.read(p.to_string_lossy().as_ref())?;
168 Some((std::sync::Arc::from(p.to_string_lossy().as_ref()), text))
169 })
170 .collect();
171 if !sources.is_empty() {
172 let cancel = crate::IndexCancel::new();
173 // Rayon: when this is the first `index_batch` call it seeds the
174 // symbol index from every registered file, not just this chunk.
175 self.index_batch(&sources, crate::IndexParallelism::Rayon, &cancel);
176 }
177 }
178
179 /// Run all pre-passes (builtin-stub loading, vendor-eager-file loading,
180 /// and PSR-4 class preloading) before body analysis of a single file.
181 ///
182 /// Replaces the two separate `ensure_stubs_for_ast` /
183 /// `preload_psr4_classes_for_ast` calls at every open-file analysis site.
184 pub fn prepare_ast_for_analysis(&self, program: &php_ast::owned::Program, file: &str) {
185 self.ensure_stubs_for_ast(program);
186 self.ensure_vendor_eager_functions();
187 self.priority_index_for_ast(program, file);
188 }
189
190 /// Run `file`'s Phase-1 warm-up — resolve its direct class references and
191 /// lazy-load any not yet indexed — and record it as prepared against its
192 /// current text. No-op when the file is unknown or already prepared.
193 ///
194 /// This is the write-path home of the warm-up: hosts call it (via
195 /// [`Self::ingest_file_prepared`]) when text lands, so read paths
196 /// (`indexed_references_to`, `reanalyze_files_cancellable`) find every
197 /// candidate prepared and stay pure. Loading mutates salsa inputs, so the
198 /// parse snapshot is scoped and dropped before the warm-up runs — callers
199 /// must not hold a live snapshot across this call.
200 pub fn prepare_file_for_analysis(&self, path: &std::sync::Arc<str>) {
201 let generation = self.prepare_generation_snapshot();
202 let (parsed, text) = {
203 let db = self.snapshot_db();
204 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
205 return;
206 };
207 let text = sf.text(&db as &dyn crate::db::MirDatabase).clone();
208 if self.is_prepared_for_analysis(path.as_ref(), &text, generation) {
209 return;
210 }
211 (
212 crate::db::parse_file(&db as &dyn crate::db::MirDatabase, sf)
213 .0
214 .clone(),
215 text,
216 )
217 };
218 // One revision bump per prepared file instead of one per loaded
219 // class; nests under a pass-level scope (references Phase 1, the
220 // reanalyze sweep), which then coalesces the whole pass into one.
221 let _deferred_bumps = self.defer_revision_bumps();
222 self.prepare_ast_for_analysis(&parsed.program, path.as_ref());
223 self.mark_prepared_for_analysis(path, text, generation);
224 }
225
226 /// Current warm-up generation; capture before a prepare + mark pair so a
227 /// concurrent [`Self::bump_prepare_generation`] invalidates the mark.
228 pub(crate) fn prepare_generation_snapshot(&self) -> u64 {
229 self.prepare_generation
230 .load(std::sync::atomic::Ordering::Acquire)
231 }
232
233 /// Whether `file`'s warm-up already ran against `current_text` in
234 /// `generation`. See the `prepared_files` field docs for the invalidation
235 /// rules.
236 pub(crate) fn is_prepared_for_analysis(
237 &self,
238 file: &str,
239 current_text: &std::sync::Arc<str>,
240 generation: u64,
241 ) -> bool {
242 self.prepared_files
243 .read()
244 .get(file)
245 .is_some_and(|(text, prepared_gen)| {
246 *prepared_gen == generation && std::sync::Arc::ptr_eq(text, current_text)
247 })
248 }
249
250 /// Record that `file`'s warm-up ran against `text`. `generation` must be
251 /// the [`Self::prepare_generation_snapshot`] taken *before* the warm-up —
252 /// a bump in between leaves the entry stale, which is the safe direction.
253 pub(crate) fn mark_prepared_for_analysis(
254 &self,
255 file: &std::sync::Arc<str>,
256 text: std::sync::Arc<str>,
257 generation: u64,
258 ) {
259 self.prepared_files
260 .write()
261 .insert(file.clone(), (text, generation));
262 }
263
264 /// Drop `file`'s warm-up skip entry (its loaded state is being removed).
265 pub(crate) fn forget_prepared(&self, file: &str) {
266 self.prepared_files.write().remove(file);
267 }
268
269 /// Invalidate every warm-up skip entry. Call when previously loaded
270 /// declarations may have been removed outside [`Self::ingest_file`] /
271 /// [`Self::invalidate_file`] — e.g. a host that writes file text straight
272 /// into the salsa layer and detects a declaration-level change. A prepared
273 /// file might then need its warm-up re-run to lazy-load a replacement
274 /// (a vendor class shadowed by a since-deleted project class).
275 pub fn bump_prepare_generation(&self) {
276 self.clear_transient_batch_replay();
277 self.prepare_generation
278 .fetch_add(1, std::sync::atomic::Ordering::Release);
279 }
280
281 /// Priority-index the classes directly referenced by `file`'s AST.
282 ///
283 /// In the eager-static-input model the background indexer
284 /// ([`Self::index_batch`]) walks the whole vendor tree, but it may not have
285 /// reached every file the open buffer references yet. To avoid a transient
286 /// false `UndefinedClass` during the warm-up window, this **reorders** that
287 /// static work: it resolves the buffer's *direct* class references and
288 /// loads any not-yet-indexed ones immediately, jumping them to the front of
289 /// the queue.
290 ///
291 /// This is bounded by the number of distinct direct references in **one**
292 /// file — no transitive BFS, no depth/total budget, no pinning. Inheritance
293 /// ancestors and signature types of those classes are picked up by the
294 /// background walk (or, for navigation, by [`Self::hover`] /
295 /// [`Self::definition_of`]). Because `bump_workspace_revision` no longer
296 /// nulls the workspace index singleton, each [`Self::load_class`] here costs
297 /// only a resolver lookup + parse (or cache hit) + one tier-aware merge,
298 /// invalidating just the actively-analyzed file's memo once — not the whole
299 /// cache. Once background indexing completes this is a no-op (every
300 /// reference already resolves).
301 pub fn priority_index_for_ast(&self, program: &php_ast::owned::Program, file: &str) {
302 if self.resolver.is_none() {
303 return;
304 }
305 let refs = collect_class_refs_from_ast(program);
306 if refs.is_empty() {
307 return;
308 }
309 // Resolve names against the file's namespace/imports up front, then
310 // drop the snapshot before loading (which mutates inputs).
311 let resolved: Vec<String> = {
312 let db = self.snapshot_db();
313 refs.into_iter()
314 .map(|raw| crate::db::resolve_name(&db, file, &raw))
315 .collect()
316 };
317 for fqcn in resolved {
318 // load_class is a no-op when the class is already indexed (the
319 // common case once the background walk has passed this file).
320 self.load_class(&fqcn);
321 }
322 }
323
324 fn ensure_user_stubs_loaded(&self) {
325 self.db
326 .ingest_user_stubs(&self.user_stub_files, &self.user_stub_dirs);
327 }
328}