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(&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 [`crate::FileAnalyzer::analyze`] to keep essentials-only mode
80    /// correct without forcing callers to enumerate which stubs they need.
81    /// Idempotent — already-loaded stubs are skipped via [`Self::loaded_stubs`].
82    ///
83    /// The discovery scan is a coarse identifier sweep (see
84    /// [`crate::stubs::collect_referenced_builtin_paths`]) — it may pull in
85    /// a slightly larger set than the file strictly needs, but never misses
86    /// a referenced built-in. Cost is sub-millisecond per file.
87    ///
88    /// Fast path: if every embedded stub is already loaded (e.g. after a
89    /// batch tool called [`Self::ensure_all_stubs`]), the source scan
90    /// is skipped entirely.
91    pub fn ensure_stubs_for_source(&self, source: &str) {
92        // Cheap check first: skip the scan entirely when we already know we
93        // have everything. Avoids a ~50-500µs source walk on every analyze
94        // call in batch / warm-session scenarios.
95        {
96            let loaded = self.db.loaded_stubs.lock();
97            if loaded.len() >= crate::stubs::stub_files().len() {
98                return;
99            }
100        }
101        let paths = crate::stubs::collect_referenced_builtin_paths(source);
102        if paths.is_empty() {
103            return;
104        }
105        self.db.ingest_stub_paths(&paths, self.php_version);
106    }
107
108    /// Discover and ingest stubs by walking the parsed AST of a PHP file.
109    ///
110    /// Similar to [`Self::ensure_stubs_for_source`], but takes an already-parsed
111    /// AST instead of raw source text. Produces zero false positives since it
112    /// only extracts identifiers from actual AST nodes (not from strings or
113    /// comments). Preferred over `ensure_stubs_for_source` when the AST is
114    /// already available (e.g., in [`crate::FileAnalyzer`]).
115    ///
116    /// Idempotent and skips the scan if all stubs are already loaded.
117    pub fn ensure_stubs_for_ast(&self, program: &php_ast::owned::Program) {
118        {
119            let loaded = self.db.loaded_stubs.lock();
120            if loaded.len() >= crate::stubs::stub_files().len() {
121                return;
122            }
123        }
124        let paths = crate::stubs::collect_referenced_builtin_paths_from_ast(program);
125        if paths.is_empty() {
126            return;
127        }
128        self.db.ingest_stub_paths(&paths, self.php_version);
129    }
130
131    /// Returns true if this session has a configured class resolver
132    /// (typically a PSR-4 / classmap autoloader chained with the stub
133    /// resolver). Used by `FileAnalyzer` to skip the AST-scan preload
134    /// when no resolver is wired up.
135    pub fn has_resolver(&self) -> bool {
136        self.resolver.is_some()
137    }
138
139    /// Run both pre-passes (builtin-stub loading and PSR-4 class preloading)
140    /// in one call.  Replaces the two separate `ensure_stubs_for_ast` /
141    /// `preload_psr4_classes_for_ast` calls at every `FileAnalyzer::analyze`
142    /// site.
143    pub fn prepare_ast_for_analysis(&self, program: &php_ast::owned::Program, file: &str) {
144        self.ensure_stubs_for_ast(program);
145        self.priority_index_for_ast(program, file);
146    }
147
148    /// Priority-index the classes directly referenced by `file`'s AST.
149    ///
150    /// In the eager-static-input model the background indexer
151    /// ([`Self::index_batch`]) walks the whole vendor tree, but it may not have
152    /// reached every file the open buffer references yet. To avoid a transient
153    /// false `UndefinedClass` during the warm-up window, this **reorders** that
154    /// static work: it resolves the buffer's *direct* class references and
155    /// loads any not-yet-indexed ones immediately, jumping them to the front of
156    /// the queue.
157    ///
158    /// This is bounded by the number of distinct direct references in **one**
159    /// file — no transitive BFS, no depth/total budget, no pinning. Inheritance
160    /// ancestors and signature types of those classes are picked up by the
161    /// background walk (or, for navigation, by [`Self::hover`] /
162    /// [`Self::definition_of`]). Because `bump_workspace_revision` no longer
163    /// nulls the workspace index singleton, each [`Self::load_class`] here costs
164    /// only a resolver lookup + parse (or cache hit) + one tier-aware merge,
165    /// invalidating just the actively-analyzed file's memo once — not the whole
166    /// cache. Once background indexing completes this is a no-op (every
167    /// reference already resolves).
168    pub fn priority_index_for_ast(&self, program: &php_ast::owned::Program, file: &str) {
169        if self.resolver.is_none() {
170            return;
171        }
172        let refs = collect_class_refs_from_ast(program);
173        if refs.is_empty() {
174            return;
175        }
176        // Resolve names against the file's namespace/imports up front, then
177        // drop the snapshot before loading (which mutates inputs).
178        let resolved: Vec<String> = {
179            let db = self.snapshot_db();
180            refs.into_iter()
181                .map(|raw| crate::db::resolve_name(&db, file, &raw))
182                .collect()
183        };
184        for fqcn in resolved {
185            // load_class is a no-op when the class is already indexed (the
186            // common case once the background walk has passed this file).
187            self.load_class(&fqcn);
188        }
189    }
190
191    fn ensure_user_stubs_loaded(&self) {
192        self.db
193            .ingest_user_stubs(&self.user_stub_files, &self.user_stub_dirs);
194    }
195}