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}