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 bulk multi-file work, use the session sweeps
13//! ([`AnalysisSession::reanalyze_files_cancellable`]) — memoized and
14//! cancellable — or the CLI batch pipeline.
15
16use std::sync::Arc;
17
18use mir_issues::Issue;
19use php_ast::owned::Program;
20use php_rs_parser::source_map::SourceMap;
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        // Primary: cursor is on an identifier token.
43        if let Some(sym) = self
44            .symbols
45            .iter()
46            .filter(|s| s.span.start <= byte_offset && byte_offset < s.span.end)
47            .min_by_key(|s| s.span.end - s.span.start)
48        {
49            return Some(sym);
50        }
51
52        // Fallback: cursor is in a call-expression gap (e.g. the whitespace,
53        // argument list, or trailing `->` between two chained method calls).
54        // Match against the full expression span recorded for call-like
55        // symbols and return the innermost (smallest) enclosing call —
56        // mirrors `crate::batch::BatchAnalysis::symbol_at`, which already
57        // does this; this single-file path fell out of sync with it.
58        self.symbols
59            .iter()
60            .filter(|s| {
61                s.expr_span
62                    .is_some_and(|es| es.start <= byte_offset && byte_offset < es.end)
63            })
64            .min_by_key(|s| {
65                let es = s.expr_span.unwrap();
66                es.end - es.start
67            })
68    }
69}
70
71/// Per-file body analysis analyzer bound to an [`AnalysisSession`]. Cheap to
72/// construct — typically held transiently per analysis call.
73pub struct FileAnalyzer<'a> {
74    session: &'a AnalysisSession,
75}
76
77impl<'a> FileAnalyzer<'a> {
78    pub fn new(session: &'a AnalysisSession) -> Self {
79        Self { session }
80    }
81
82    /// Run a single body-analysis pass against a frozen db snapshot.
83    ///
84    /// `priority_index_for_ast` runs first to fault in any of this file's
85    /// direct class references not yet reached by the background indexer; then
86    /// one snapshot is analyzed and its reference locations committed. The lock
87    /// is not held during analysis, so concurrent edits and reads proceed.
88    pub fn analyze(
89        &self,
90        file: Arc<str>,
91        source: &str,
92        program: &Program,
93        source_map: &SourceMap,
94    ) -> FileAnalysis {
95        crate::metrics::record_file_analysis();
96        // Reconcile mirror-only writes with the symbol-index singleton before
97        // resolution runs against it (no-op when nothing is pending).
98        self.session.settle_workspace_index();
99
100        // Priority-index the buffer's direct class references so any not yet
101        // reached by the background indexer resolve in this single pass (no
102        // transient false UndefinedClass during warm-up). Once indexing
103        // completes this is a no-op.
104        // Capture (text, generation) BEFORE the warm-up: if a concurrent edit
105        // swaps the input text mid-flight, the stored Arc no longer matches
106        // and the mark is dead on arrival — the safe direction.
107        let prepare_generation = self.session.prepare_generation_snapshot();
108        let ingested_text = {
109            let db = self.session.snapshot_db();
110            db.lookup_source_file(file.as_ref())
111                .map(|sf| sf.text(&db as &dyn crate::db::MirDatabase).clone())
112        };
113        self.session
114            .prepare_ast_for_analysis(program, file.as_ref());
115        // Record the warm-up so later Phase-1 sweeps (references, dependent
116        // re-analysis) skip this file's parse + AST walk while its salsa
117        // input text is unchanged.
118        if let Some(text) = ingested_text.clone() {
119            self.session
120                .mark_prepared_for_analysis(&file, text, prepare_generation);
121        }
122
123        let _scope = crate::metrics::BodyAnalysisScope::new();
124
125        // Generation before the analysis snapshot — after the warm-up, so
126        // its lazy loads don't immediately stale the commit; a file add
127        // racing the analysis still leaves the mark stale, never fresh.
128        let commit_gen = self.session.index_generation();
129        // Single pass against a frozen snapshot. With the eager-static-input
130        // model the workspace index is complete (or priority-indexed for this
131        // file's direct refs), so there are no body-analysis "misses" to fault
132        // in — no retry loop, no whole-file re-analysis.
133        let db = self.session.snapshot_db();
134        let driver = BodyAnalyzer::new(&db, self.session.php_version());
135        let (issues, symbols) = driver.analyze_bodies(program, file.clone(), source, source_map);
136        // Replace (not append): this pass produced the file's complete
137        // reference set, and marking freshness against the pre-analysis text
138        // keeps the mark dead-on-arrival if a concurrent edit swapped the
139        // input mid-flight (Arc identity no longer matches).
140        let resolved = !crate::db::issues_have_unresolved_names(&issues);
141        self.session.commit_file_refs(
142            &file,
143            ingested_text,
144            db.take_pending_ref_locs(),
145            commit_gen,
146            resolved,
147        );
148        FileAnalysis { issues, symbols }
149    }
150}