Skip to main content

mir_analyzer/
file_analyzer.rs

1//! Per-file analysis entry point for incremental analysis.
2//!
3//! [`FileAnalyzer`] runs single-pass Pass 2 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 Pass 2 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 (so they keep ownership of the
10//! arena and AST). The session must already have Pass 1 state for any files
11//! whose definitions this analysis depends on; call
12//! [`AnalysisSession::ingest_file`] first.
13
14use std::sync::Arc;
15
16use mir_issues::Issue;
17use php_ast::ast::Program;
18use php_rs_parser::source_map::SourceMap;
19
20use crate::pass2::Pass2Driver;
21use crate::session::AnalysisSession;
22use crate::symbol::ResolvedSymbol;
23
24/// Result of a single-file analysis.
25pub struct FileAnalysis {
26    pub issues: Vec<Issue>,
27    pub symbols: Vec<ResolvedSymbol>,
28}
29
30impl FileAnalysis {
31    /// Return the innermost resolved symbol whose span contains `byte_offset`,
32    /// or `None` if no symbol was recorded at that position.
33    ///
34    /// Entry point for hover / go-to-definition flows: callers map
35    /// (line, column) → byte offset → resolved symbol, then look up the
36    /// symbol's definition via [`crate::AnalysisSession::definition_of`] or
37    /// type info via [`ResolvedSymbol::resolved_type`].
38    pub fn symbol_at(&self, byte_offset: u32) -> Option<&ResolvedSymbol> {
39        self.symbols
40            .iter()
41            .filter(|s| s.span.start <= byte_offset && byte_offset < s.span.end)
42            .min_by_key(|s| s.span.end - s.span.start)
43    }
44}
45
46/// Per-file Pass 2 analyzer bound to an [`AnalysisSession`]. Cheap to
47/// construct — typically held transiently per analysis call.
48pub struct FileAnalyzer<'a> {
49    session: &'a AnalysisSession,
50}
51
52impl<'a> FileAnalyzer<'a> {
53    pub fn new(session: &'a AnalysisSession) -> Self {
54        Self { session }
55    }
56
57    /// Single-pass Pass 2. Returns issues and per-expression resolved symbols.
58    ///
59    /// Pass 2 runs against a cloned db snapshot — the lock is not held during
60    /// analysis, so concurrent edits and reads on the session proceed without
61    /// blocking on this call.
62    ///
63    /// Stub loading: ensures the session's essentials are loaded, then auto-
64    /// discovers any extension stubs (`imagecreate` → gd, `ReflectionClass` →
65    /// Reflection, …) referenced by `source` and lazy-ingests them. This
66    /// keeps essentials-only sessions correct without callers having to
67    /// enumerate stubs by hand. Call `ensure_all_stubs_loaded` once if the
68    /// consumer prefers eager loading instead.
69    pub fn analyze(
70        &self,
71        file: Arc<str>,
72        source: &str,
73        program: &Program<'_, '_>,
74        source_map: &SourceMap,
75    ) -> FileAnalysis {
76        self.session.ensure_essential_stubs_loaded();
77        self.session.ensure_stubs_for_source(source);
78        let db = self.session.snapshot_db();
79        let driver = Pass2Driver::new(&db, self.session.php_version());
80        let (issues, symbols) = driver.analyze_bodies(program, file, source, source_map);
81        FileAnalysis { issues, symbols }
82    }
83}