1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
use super::*;
impl AnalysisSession {
/// Deprecated — stub loading is now fully lazy per-AST.
///
/// This is an alias for [`Self::ensure_all_stubs`] kept for API
/// compatibility. Internal analysis paths use [`Self::prepare_ast_for_analysis`]
/// which loads only the stubs referenced by the file under analysis.
#[deprecated(note = "use ensure_all_stubs() or ensure_stubs_for_ast() instead")]
pub fn ensure_essential_stubs(&self) {
self.ensure_all_stubs();
}
/// Load every embedded PHP stub plus any configured user stubs.
/// Use for batch tools (CLI, full project analysis) where comprehensive
/// symbol coverage matters more than cold-start latency.
pub fn ensure_all_stubs(&self) {
let paths: Vec<&'static str> = crate::stubs::stub_files().iter().map(|&(p, _)| p).collect();
self.db.ingest_stub_paths(&paths, self.php_version);
self.ensure_user_stubs_loaded();
}
/// Ensure the embedded stub that defines `name` (a function) is ingested.
/// Returns `true` when a matching stub exists (whether or not it was
/// already loaded), `false` when `name` isn't a known PHP built-in.
///
/// Most callers should use [`Self::ensure_stubs_for_ast`] instead —
/// it auto-discovers needed stubs from a parsed file.
#[doc(hidden)]
pub fn ensure_stub_for_function(&self, name: &str) -> bool {
match crate::stubs::stub_path_for_function(name) {
Some(path) => {
self.db.ingest_stub_paths(&[path], self.php_version);
true
}
None => false,
}
}
/// Ensure the embedded stub that defines `fqcn` (a class / interface /
/// trait / enum) is ingested. Case-insensitive lookup with optional
/// leading backslash.
///
/// Most callers should use [`Self::ensure_stubs_for_ast`] instead.
#[doc(hidden)]
pub fn ensure_stub_for_class(&self, fqcn: &str) -> bool {
match crate::stubs::stub_path_for_class(fqcn) {
Some(path) => {
self.db.ingest_stub_paths(&[path], self.php_version);
true
}
None => false,
}
}
/// Ensure the embedded stub that defines `name` (a constant) is ingested.
///
/// Most callers should use [`Self::ensure_stubs_for_ast`] instead.
#[doc(hidden)]
pub fn ensure_stub_for_constant(&self, name: &str) -> bool {
match crate::stubs::stub_path_for_constant(name) {
Some(path) => {
self.db.ingest_stub_paths(&[path], self.php_version);
true
}
None => false,
}
}
/// Number of distinct embedded stubs currently ingested into the session.
/// Useful for diagnostics and bench reporting.
pub fn loaded_stub_count(&self) -> usize {
self.db.loaded_stubs.lock().len()
}
/// Auto-discover and ingest the embedded stubs needed to cover every
/// built-in PHP function / class / constant referenced by `source`.
///
/// Used by [`crate::FileAnalyzer::analyze`] to keep essentials-only mode
/// correct without forcing callers to enumerate which stubs they need.
/// Idempotent — already-loaded stubs are skipped via [`Self::loaded_stubs`].
///
/// The discovery scan is a coarse identifier sweep (see
/// [`crate::stubs::collect_referenced_builtin_paths`]) — it may pull in
/// a slightly larger set than the file strictly needs, but never misses
/// a referenced built-in. Cost is sub-millisecond per file.
///
/// Fast path: if every embedded stub is already loaded (e.g. after a
/// batch tool called [`Self::ensure_all_stubs`]), the source scan
/// is skipped entirely.
pub fn ensure_stubs_for_source(&self, source: &str) {
// Cheap check first: skip the scan entirely when we already know we
// have everything. Avoids a ~50-500µs source walk on every analyze
// call in batch / warm-session scenarios.
{
let loaded = self.db.loaded_stubs.lock();
if loaded.len() >= crate::stubs::stub_files().len() {
return;
}
}
let paths = crate::stubs::collect_referenced_builtin_paths(source);
if paths.is_empty() {
return;
}
self.db.ingest_stub_paths(&paths, self.php_version);
}
/// Discover and ingest stubs by walking the parsed AST of a PHP file.
///
/// Similar to [`Self::ensure_stubs_for_source`], but takes an already-parsed
/// AST instead of raw source text. Produces zero false positives since it
/// only extracts identifiers from actual AST nodes (not from strings or
/// comments). Preferred over `ensure_stubs_for_source` when the AST is
/// already available (e.g., in [`crate::FileAnalyzer`]).
///
/// Idempotent and skips the scan if all stubs are already loaded.
pub fn ensure_stubs_for_ast(&self, program: &php_ast::owned::Program) {
{
let loaded = self.db.loaded_stubs.lock();
if loaded.len() >= crate::stubs::stub_files().len() {
return;
}
}
let paths = crate::stubs::collect_referenced_builtin_paths_from_ast(program);
if paths.is_empty() {
return;
}
self.db.ingest_stub_paths(&paths, self.php_version);
}
/// Returns true if this session has a configured class resolver
/// (typically a PSR-4 / classmap autoloader chained with the stub
/// resolver). Used by `FileAnalyzer` to skip the AST-scan preload
/// when no resolver is wired up.
pub fn has_resolver(&self) -> bool {
self.resolver.is_some()
}
/// Index vendor `autoload.files` entries the first time analysis runs.
///
/// Composer's `autoload.files` lists files that define global functions and
/// constants (e.g. Laravel helpers). These are invisible to the PSR-4 class
/// resolver — there is no function-name → file-path mapping without
/// parsing them first. Rather than per-function lazy resolution, this
/// loads all pending vendor eager files at once on the first
/// [`Self::prepare_ast_for_analysis`] call.
///
/// The mutex is held for the duration of the load, so concurrent callers
/// block here until the files are indexed. Subsequent calls see `None`
/// and return immediately (O(1)). Files are read via the session's
/// [`crate::SourceProvider`], so LSP VFS overrides are respected.
pub(crate) fn ensure_vendor_eager_functions(&self) {
let mut guard = self.pending_eager_function_files.lock();
let files = match guard.take() {
None => return,
Some(f) if f.is_empty() => return,
Some(f) => f,
};
// Guard remains held (now `None`) — concurrent callers block here
// until `index_batch` returns and all functions are indexed.
let sources: Vec<(std::sync::Arc<str>, std::sync::Arc<str>)> = files
.iter()
.filter_map(|p| {
let text = self.source_provider.read(p.to_string_lossy().as_ref())?;
Some((std::sync::Arc::from(p.to_string_lossy().as_ref()), text))
})
.collect();
if !sources.is_empty() {
let cancel = crate::IndexCancel::new();
self.index_batch(&sources, crate::IndexParallelism::Sequential, &cancel);
}
}
/// Run all pre-passes (builtin-stub loading, vendor-eager-file loading,
/// and PSR-4 class preloading) before body analysis of a single file.
///
/// Replaces the two separate `ensure_stubs_for_ast` /
/// `preload_psr4_classes_for_ast` calls at every `FileAnalyzer::analyze`
/// site.
pub fn prepare_ast_for_analysis(&self, program: &php_ast::owned::Program, file: &str) {
self.ensure_stubs_for_ast(program);
self.ensure_vendor_eager_functions();
self.priority_index_for_ast(program, file);
}
/// Priority-index the classes directly referenced by `file`'s AST.
///
/// In the eager-static-input model the background indexer
/// ([`Self::index_batch`]) walks the whole vendor tree, but it may not have
/// reached every file the open buffer references yet. To avoid a transient
/// false `UndefinedClass` during the warm-up window, this **reorders** that
/// static work: it resolves the buffer's *direct* class references and
/// loads any not-yet-indexed ones immediately, jumping them to the front of
/// the queue.
///
/// This is bounded by the number of distinct direct references in **one**
/// file — no transitive BFS, no depth/total budget, no pinning. Inheritance
/// ancestors and signature types of those classes are picked up by the
/// background walk (or, for navigation, by [`Self::hover`] /
/// [`Self::definition_of`]). Because `bump_workspace_revision` no longer
/// nulls the workspace index singleton, each [`Self::load_class`] here costs
/// only a resolver lookup + parse (or cache hit) + one tier-aware merge,
/// invalidating just the actively-analyzed file's memo once — not the whole
/// cache. Once background indexing completes this is a no-op (every
/// reference already resolves).
pub fn priority_index_for_ast(&self, program: &php_ast::owned::Program, file: &str) {
if self.resolver.is_none() {
return;
}
let refs = collect_class_refs_from_ast(program);
if refs.is_empty() {
return;
}
// Resolve names against the file's namespace/imports up front, then
// drop the snapshot before loading (which mutates inputs).
let resolved: Vec<String> = {
let db = self.snapshot_db();
refs.into_iter()
.map(|raw| crate::db::resolve_name(&db, file, &raw))
.collect()
};
for fqcn in resolved {
// load_class is a no-op when the class is already indexed (the
// common case once the background walk has passed this file).
self.load_class(&fqcn);
}
}
fn ensure_user_stubs_loaded(&self) {
self.db
.ingest_user_stubs(&self.user_stub_files, &self.user_stub_dirs);
}
}