Skip to main content

mir_analyzer/
file_analyzer.rs

1//! Per-file analysis entry point for incremental analysis.
2//!
3//! [`FileAnalyzer`] runs a **single** body-analysis pass against an
4//! [`AnalysisSession`] snapshot. In the eager-static-input model the workspace
5//! symbol index is built up front by the background indexer
6//! ([`AnalysisSession::index_batch`]), so `find_class_like` resolves vendor
7//! classes directly — there is no lazy-load / retry loop. The only on-demand
8//! work is [`AnalysisSession::priority_index_for_ast`], which faults in the
9//! open file's *direct* references if the background walk hasn't reached them
10//! yet, keeping warm-up free of transient false positives.
11//!
12//! For batch multi-file analysis, use [`BatchFileAnalyzer::analyze_batch`]
13//! which parallelizes analysis across multiple pre-parsed files.
14
15use std::sync::Arc;
16
17use mir_issues::Issue;
18use php_ast::owned::Program;
19use php_rs_parser::source_map::SourceMap;
20use rayon::prelude::*;
21
22use crate::body_analysis::BodyAnalyzer;
23use crate::db::MirDatabase;
24use crate::session::AnalysisSession;
25use crate::symbol::ResolvedSymbol;
26
27/// Result of a single-file analysis.
28pub struct FileAnalysis {
29    pub issues: Vec<Issue>,
30    pub symbols: Vec<ResolvedSymbol>,
31}
32
33impl FileAnalysis {
34    /// Return the innermost resolved symbol whose span contains `byte_offset`,
35    /// or `None` if no symbol was recorded at that position.
36    ///
37    /// Entry point for hover / go-to-definition flows: callers map
38    /// (line, column) → byte offset → resolved symbol, then look up the
39    /// symbol's definition via [`crate::AnalysisSession::definition_of`] or
40    /// type info via [`ResolvedSymbol::resolved_type`].
41    pub fn symbol_at(&self, byte_offset: u32) -> Option<&ResolvedSymbol> {
42        self.symbols
43            .iter()
44            .filter(|s| s.span.start <= byte_offset && byte_offset < s.span.end)
45            .min_by_key(|s| s.span.end - s.span.start)
46    }
47}
48
49/// Per-file body analysis analyzer bound to an [`AnalysisSession`]. Cheap to
50/// construct — typically held transiently per analysis call.
51pub struct FileAnalyzer<'a> {
52    session: &'a AnalysisSession,
53}
54
55impl<'a> FileAnalyzer<'a> {
56    pub fn new(session: &'a AnalysisSession) -> Self {
57        Self { session }
58    }
59
60    /// Run a single body-analysis pass against a frozen db snapshot.
61    ///
62    /// `priority_index_for_ast` runs first to fault in any of this file's
63    /// direct class references not yet reached by the background indexer; then
64    /// one snapshot is analyzed and its reference locations committed. The lock
65    /// is not held during analysis, so concurrent edits and reads proceed.
66    pub fn analyze(
67        &self,
68        file: Arc<str>,
69        source: &str,
70        program: &Program,
71        source_map: &SourceMap,
72    ) -> FileAnalysis {
73        crate::metrics::record_file_analysis();
74
75        // Priority-index the buffer's direct class references so any not yet
76        // reached by the background indexer resolve in this single pass (no
77        // transient false UndefinedClass during warm-up). Once indexing
78        // completes this is a no-op.
79        // Capture (text, generation) BEFORE the warm-up: if a concurrent edit
80        // swaps the input text mid-flight, the stored Arc no longer matches
81        // and the mark is dead on arrival — the safe direction.
82        let prepare_generation = self.session.prepare_generation_snapshot();
83        let ingested_text = {
84            let db = self.session.snapshot_db();
85            db.lookup_source_file(file.as_ref())
86                .map(|sf| sf.text(&db as &dyn crate::db::MirDatabase))
87        };
88        self.session
89            .prepare_ast_for_analysis(program, file.as_ref());
90        // Record the warm-up so later Phase-1 sweeps (references, dependent
91        // re-analysis) skip this file's parse + AST walk while its salsa
92        // input text is unchanged.
93        if let Some(text) = ingested_text {
94            self.session
95                .mark_prepared_for_analysis(&file, text, prepare_generation);
96        }
97
98        let _scope = crate::metrics::BodyAnalysisScope::new();
99
100        // Single pass against a frozen snapshot. With the eager-static-input
101        // model the workspace index is complete (or priority-indexed for this
102        // file's direct refs), so there are no body-analysis "misses" to fault
103        // in — no retry loop, no whole-file re-analysis.
104        let db = self.session.snapshot_db();
105        let driver = BodyAnalyzer::new(&db, self.session.php_version());
106        let (issues, symbols) = driver.analyze_bodies(program, file.clone(), source, source_map);
107        self.session
108            .commit_ref_locs_batch(db.take_pending_ref_locs());
109        FileAnalysis { issues, symbols }
110    }
111}
112
113/// Batch file analyzer for parallel multi-file analysis.
114///
115/// `BatchFileAnalyzer` processes pre-parsed files in parallel using rayon,
116/// making it efficient for analyzing many files at once (e.g., cold-start analysis).
117pub struct BatchFileAnalyzer<'a> {
118    session: &'a AnalysisSession,
119}
120
121/// A pre-parsed file ready for batch analysis.
122pub struct ParsedFile {
123    pub(crate) file: Arc<str>,
124    pub(crate) source: Arc<str>,
125    pub(crate) program: Program,
126    pub(crate) source_map: SourceMap,
127}
128
129impl ParsedFile {
130    /// File path this `ParsedFile` represents.
131    pub fn file(&self) -> &Arc<str> {
132        &self.file
133    }
134
135    /// Source text for this file.
136    pub fn source(&self) -> &Arc<str> {
137        &self.source
138    }
139
140    /// Create a `ParsedFile` from an owned program and source map.
141    pub fn new(file: Arc<str>, source: Arc<str>, program: Program, source_map: SourceMap) -> Self {
142        Self {
143            file,
144            source,
145            program,
146            source_map,
147        }
148    }
149}
150
151impl<'a> BatchFileAnalyzer<'a> {
152    pub fn new(session: &'a AnalysisSession) -> Self {
153        Self { session }
154    }
155
156    /// Analyze multiple pre-parsed files in parallel.
157    ///
158    /// Each rayon worker gets its own cloned database snapshot, so concurrent
159    /// analysis proceeds without lock contention on the session.
160    pub fn analyze_batch(&self, files: Vec<ParsedFile>) -> Vec<(Arc<str>, FileAnalysis)> {
161        // First pass: collect all ASTs and auto-discover stubs.
162        // Also lazy-load vendor autoload.files globals once so they are in the
163        // workspace index before the parallel analysis snapshot is taken.
164        self.session.ensure_vendor_eager_functions();
165        files.iter().for_each(|file| {
166            self.session.ensure_stubs_for_ast(&file.program);
167        });
168
169        // Second pass: analyze files in parallel.
170        // Each rayon worker gets its own database clone (Salsa is Send but !Sync).
171        let db = self.session.snapshot_db();
172        let results: Vec<(Arc<str>, FileAnalysis, Vec<crate::db::RefLoc>)> = files
173            .into_par_iter()
174            .map_with(db, |db, file| {
175                let driver = BodyAnalyzer::new(db as &dyn MirDatabase, self.session.php_version());
176                let (issues, symbols) = driver.analyze_bodies(
177                    &file.program,
178                    file.file.clone(),
179                    &file.source,
180                    &file.source_map,
181                );
182                let pending = db.take_pending_ref_locs();
183                let analysis = FileAnalysis { issues, symbols };
184                (file.file, analysis, pending)
185            })
186            .collect();
187        let mut all_ref_locs = Vec::new();
188        let mut out = Vec::with_capacity(results.len());
189        for (file, analysis, ref_locs) in results {
190            all_ref_locs.extend(ref_locs);
191            out.push((file, analysis));
192        }
193        self.session.commit_ref_locs_batch(all_ref_locs);
194        out
195    }
196}