Skip to main content

mir_analyzer/
file_analyzer.rs

1//! Per-file analysis entry point for incremental analysis.
2//!
3//! [`FileAnalyzer`] runs single-pass body analysis against an [`AnalysisSession`] and
4//! returns issues + resolved symbols for one file. Unlike
5//! [`crate::ProjectAnalyzer::re_analyze_file`], it does **not** run the
6//! inference-only body analysis sweep — that's a batch concern. For cross-file
7//! inferred return types, schedule a project-wide inference sweep on idle.
8//!
9//! Caller is responsible for parsing the file and passing owned AST.
10//! The session must already have definition collection state for any files whose definitions
11//! this analysis depends on; call [`AnalysisSession::ingest_file`] first.
12//!
13//! For batch multi-file analysis, use [`BatchFileAnalyzer::analyze_batch`]
14//! which parallelizes analysis across multiple pre-parsed files.
15
16use std::sync::Arc;
17
18use mir_issues::Issue;
19use php_ast::owned::Program;
20use php_rs_parser::source_map::SourceMap;
21use rayon::prelude::*;
22
23use crate::body_analysis::BodyAnalyzer;
24use crate::db::MirDatabase;
25use crate::session::AnalysisSession;
26use crate::symbol::ResolvedSymbol;
27
28/// Result of a single-file analysis.
29pub struct FileAnalysis {
30    pub issues: Vec<Issue>,
31    pub symbols: Vec<ResolvedSymbol>,
32}
33
34impl FileAnalysis {
35    /// Return the innermost resolved symbol whose span contains `byte_offset`,
36    /// or `None` if no symbol was recorded at that position.
37    ///
38    /// Entry point for hover / go-to-definition flows: callers map
39    /// (line, column) → byte offset → resolved symbol, then look up the
40    /// symbol's definition via [`crate::AnalysisSession::definition_of`] or
41    /// type info via [`ResolvedSymbol::resolved_type`].
42    pub fn symbol_at(&self, byte_offset: u32) -> Option<&ResolvedSymbol> {
43        self.symbols
44            .iter()
45            .filter(|s| s.span.start <= byte_offset && byte_offset < s.span.end)
46            .min_by_key(|s| s.span.end - s.span.start)
47    }
48}
49
50/// Per-file body analysis analyzer bound to an [`AnalysisSession`]. Cheap to
51/// construct — typically held transiently per analysis call.
52pub struct FileAnalyzer<'a> {
53    session: &'a AnalysisSession,
54}
55
56impl<'a> FileAnalyzer<'a> {
57    pub fn new(session: &'a AnalysisSession) -> Self {
58        Self { session }
59    }
60
61    /// Run body analysis against a db snapshot.
62    ///
63    /// body analysis runs against a cloned db snapshot — the lock is not held during
64    /// analysis, so concurrent edits and reads on the session proceed without
65    /// blocking on this call. PSR-4-mapped classes referenced in the AST are
66    /// pre-loaded before body analysis so `find_class_like` resolves them in a single
67    /// pass via the salsa query graph.
68    pub fn analyze(
69        &self,
70        file: Arc<str>,
71        source: &str,
72        program: &Program,
73        source_map: &SourceMap,
74    ) -> FileAnalysis {
75        crate::metrics::record_file_analysis();
76        self.session
77            .prepare_ast_for_analysis(program, file.as_ref());
78
79        let _scope = crate::metrics::BodyAnalysisScope::new();
80        let db = self.session.snapshot_db();
81        let driver = BodyAnalyzer::new(&db, self.session.php_version());
82        let (issues, symbols) = driver.analyze_bodies(program, file, source, source_map);
83        self.session
84            .commit_ref_locs_batch(db.take_pending_ref_locs());
85        FileAnalysis { issues, symbols }
86    }
87}
88
89/// Batch file analyzer for parallel multi-file analysis.
90///
91/// `BatchFileAnalyzer` processes pre-parsed files in parallel using rayon,
92/// making it efficient for analyzing many files at once (e.g., cold-start analysis).
93pub struct BatchFileAnalyzer<'a> {
94    session: &'a AnalysisSession,
95}
96
97/// A pre-parsed file ready for batch analysis.
98pub struct ParsedFile {
99    pub(crate) file: Arc<str>,
100    pub(crate) source: Arc<str>,
101    pub(crate) program: Program,
102    pub(crate) source_map: SourceMap,
103}
104
105impl ParsedFile {
106    /// File path this `ParsedFile` represents.
107    pub fn file(&self) -> &Arc<str> {
108        &self.file
109    }
110
111    /// Source text for this file.
112    pub fn source(&self) -> &Arc<str> {
113        &self.source
114    }
115
116    /// Create a `ParsedFile` from an owned program and source map.
117    pub fn new(file: Arc<str>, source: Arc<str>, program: Program, source_map: SourceMap) -> Self {
118        Self {
119            file,
120            source,
121            program,
122            source_map,
123        }
124    }
125}
126
127impl<'a> BatchFileAnalyzer<'a> {
128    pub fn new(session: &'a AnalysisSession) -> Self {
129        Self { session }
130    }
131
132    /// Analyze multiple pre-parsed files in parallel.
133    ///
134    /// Each rayon worker gets its own cloned database snapshot, so concurrent
135    /// analysis proceeds without lock contention on the session.
136    pub fn analyze_batch(&self, files: Vec<ParsedFile>) -> Vec<(Arc<str>, FileAnalysis)> {
137        // First pass: collect all ASTs and auto-discover stubs.
138        files.iter().for_each(|file| {
139            self.session.ensure_stubs_for_ast(&file.program);
140        });
141
142        // Second pass: analyze files in parallel.
143        // Each rayon worker gets its own database clone (Salsa is Send but !Sync).
144        let db = self.session.snapshot_db();
145        let results: Vec<(Arc<str>, FileAnalysis, Vec<crate::db::RefLoc>)> = files
146            .into_par_iter()
147            .map_with(db, |db, file| {
148                let driver = BodyAnalyzer::new(db as &dyn MirDatabase, self.session.php_version());
149                let (issues, symbols) = driver.analyze_bodies(
150                    &file.program,
151                    file.file.clone(),
152                    &file.source,
153                    &file.source_map,
154                );
155                let pending = db.take_pending_ref_locs();
156                let analysis = FileAnalysis { issues, symbols };
157                (file.file, analysis, pending)
158            })
159            .collect();
160        let mut all_ref_locs = Vec::new();
161        let mut out = Vec::with_capacity(results.len());
162        for (file, analysis, ref_locs) in results {
163            all_ref_locs.extend(ref_locs);
164            out.push((file, analysis));
165        }
166        self.session.commit_ref_locs_batch(all_ref_locs);
167        out
168    }
169}