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}