Skip to main content

mir_analyzer/
lib.rs

1pub(crate) mod arena;
2#[doc(hidden)]
3pub mod cache;
4pub(crate) mod call;
5pub(crate) mod class;
6pub(crate) mod collector;
7pub(crate) mod context;
8#[doc(hidden)]
9pub mod db;
10pub(crate) mod dead_code;
11pub(crate) mod diagnostics;
12pub(crate) mod expr;
13pub mod file_analyzer;
14pub(crate) mod generic;
15pub(crate) mod narrowing;
16#[doc(hidden)]
17pub mod parser;
18pub(crate) mod pass2;
19pub mod php_version;
20pub mod project;
21pub mod session;
22pub(crate) mod shared_db;
23pub(crate) mod stmt;
24#[doc(hidden)]
25pub mod stub_cache;
26#[doc(hidden)]
27pub mod stubs;
28pub(crate) mod taint;
29pub(crate) mod type_env;
30
31pub use file_analyzer::{BatchFileAnalyzer, FileAnalysis, FileAnalyzer, ParsedFile};
32pub use parser::type_from_hint::type_from_hint;
33pub use parser::{DocblockParser, ParsedDocblock};
34pub use php_version::{ParsePhpVersionError, PhpVersion};
35pub use project::{AnalysisResult, ProjectAnalyzer};
36pub use session::AnalysisSession;
37pub use stubs::{is_builtin_function, stub_files, StubVfs};
38
39// ============================================================================
40// API Unification: ProjectAnalyzer and AnalysisSession
41// ============================================================================
42//
43// `ProjectAnalyzer` (batch-oriented) and `AnalysisSession` (incremental) are
44// now unified under a single analysis engine. Both share the same Salsa database,
45// definition collection, and Pass 2 type inference logic. The difference is
46// ownership model and parallelization strategy:
47//
48// - `ProjectAnalyzer`: Owns the database and all files; analyzes them in parallel.
49//   Best for CLI, CI, and bulk analysis. Configuration via public fields before
50//   calling `analyze()`.
51//
52// - `AnalysisSession`: Incremental file-by-file analysis; clients ingest files
53//   as they change. Best for LSP servers and watch modes. Configuration via
54//   builder pattern (with_cache, with_psr4, etc.).
55//
56// New code should prefer `AnalysisSession` for flexibility; `ProjectAnalyzer`
57// is maintained for backward compatibility with batch workflows.
58
59/// A position in source code: 1-based line, 0-based codepoint column.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
61pub struct Position {
62    pub line: u32,
63    pub column: u32,
64}
65
66/// A range in source code: start and end positions.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct Range {
69    pub start: Position,
70    pub end: Position,
71}
72
73/// A semantic identifier for a code entity that the analyzer can resolve.
74///
75/// Replaces the previous stringly-typed `&str` keys. Method names are
76/// normalized (lowercased) at construction since PHP method dispatch is
77/// case-insensitive — this prevents a class of correctness bugs where
78/// callers pass mixed-case names and get empty results.
79#[derive(Debug, Clone, PartialEq, Eq, Hash)]
80pub enum Symbol {
81    /// A class, interface, trait, or enum (FQCN).
82    Class(std::sync::Arc<str>),
83    /// A global function (FQN).
84    Function(std::sync::Arc<str>),
85    /// An instance or static method.
86    Method {
87        class: std::sync::Arc<str>,
88        name: std::sync::Arc<str>,
89    },
90    /// A class property.
91    Property {
92        class: std::sync::Arc<str>,
93        name: std::sync::Arc<str>,
94    },
95    /// A class / interface / enum constant.
96    ClassConstant {
97        class: std::sync::Arc<str>,
98        name: std::sync::Arc<str>,
99    },
100    /// A global constant.
101    GlobalConstant(std::sync::Arc<str>),
102}
103
104impl Symbol {
105    /// Construct a method symbol. Normalizes `name` to lowercase since PHP
106    /// methods are case-insensitive.
107    pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
108        Symbol::Method {
109            class: class.into(),
110            name: std::sync::Arc::from(name.to_ascii_lowercase()),
111        }
112    }
113
114    /// Construct a class symbol.
115    pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
116        Symbol::Class(fqcn.into())
117    }
118
119    /// Construct a function symbol.
120    pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
121        Symbol::Function(fqn.into())
122    }
123
124    /// Construct a property symbol.
125    pub fn property(
126        class: impl Into<std::sync::Arc<str>>,
127        name: impl Into<std::sync::Arc<str>>,
128    ) -> Self {
129        Symbol::Property {
130            class: class.into(),
131            name: name.into(),
132        }
133    }
134
135    /// Construct a class constant symbol.
136    pub fn class_constant(
137        class: impl Into<std::sync::Arc<str>>,
138        name: impl Into<std::sync::Arc<str>>,
139    ) -> Self {
140        Symbol::ClassConstant {
141            class: class.into(),
142            name: name.into(),
143        }
144    }
145
146    /// Construct a global constant symbol.
147    pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
148        Symbol::GlobalConstant(fqn.into())
149    }
150
151    /// The codebase lookup key for this symbol (used internally for the
152    /// reference-locations index). Stable across releases.
153    pub fn codebase_key(&self) -> String {
154        match self {
155            Symbol::Class(fqcn) => fqcn.to_string(),
156            Symbol::Function(fqn) => fqn.to_string(),
157            Symbol::Method { class, name } => format!("{class}::{name}"),
158            Symbol::Property { class, name } => format!("{class}::{name}"),
159            Symbol::ClassConstant { class, name } => format!("{class}::{name}"),
160            Symbol::GlobalConstant(fqn) => fqn.to_string(),
161        }
162    }
163}
164
165/// Reason a symbol lookup did not return a location.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub enum SymbolLookupError {
168    /// No such symbol exists in the codebase.
169    NotFound,
170    /// The symbol exists but has no recorded source location (e.g. a
171    /// stub-only declaration without a span).
172    NoSourceLocation,
173}
174
175/// Result of a lazy-load attempt.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum LazyLoadOutcome {
178    /// The symbol was already present in the session; no work performed.
179    AlreadyLoaded,
180    /// The symbol was resolved by the configured [`ClassResolver`] and the
181    /// defining file was ingested.
182    Loaded,
183    /// No resolver is configured, the resolver could not map the FQCN to a
184    /// file, or the resolved file could not be read / did not define the
185    /// requested symbol.
186    NotResolvable,
187}
188
189/// Pluggable strategy for mapping a fully-qualified class name to the file
190/// that should define it. The analyzer never touches `vendor/` or the
191/// filesystem on its own — it asks a `ClassResolver` when a symbol is needed.
192///
193/// `mir_analyzer::Psr4Map` is the built-in implementation for Composer-based
194/// projects. Consumers with non-Composer conventions (WordPress, Drupal, a
195/// custom autoloader, a workspace-walk index) supply their own.
196pub trait ClassResolver: Send + Sync {
197    /// Resolve `fqcn` to the file that defines it. Returning `None` causes
198    /// the analyzer to fall back to emitting `UndefinedClass`.
199    fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
200}
201
202impl ClassResolver for composer::Psr4Map {
203    fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
204        composer::Psr4Map::resolve(self, fqcn)
205    }
206}
207
208impl std::fmt::Display for SymbolLookupError {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            SymbolLookupError::NotFound => write!(f, "symbol not found"),
212            SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
213        }
214    }
215}
216
217impl std::error::Error for SymbolLookupError {}
218
219/// Hover information for a symbol at a source location.
220/// Includes the inferred type, optional docstring, and location of definition.
221#[derive(Debug, Clone)]
222pub struct HoverInfo {
223    /// Inferred type of the symbol.
224    pub ty: Type,
225    /// Docstring / documentation comment for the symbol (if available).
226    pub docstring: Option<String>,
227    /// Source location of the symbol's definition.
228    pub definition: Option<mir_codebase::storage::Location>,
229}
230
231/// File dependency graph: tracks which files depend on which other files.
232/// Used for incremental invalidation in LSP servers and build systems.
233#[derive(Debug, Clone)]
234pub struct DependencyGraph {
235    /// Direct dependencies: file → [files it depends on]
236    dependencies: std::collections::HashMap<String, Vec<String>>,
237    /// Reverse dependencies: file → [files that depend on it]
238    dependents: std::collections::HashMap<String, Vec<String>>,
239}
240
241impl DependencyGraph {
242    /// Files that `file` directly depends on (imports, parent classes, interfaces, traits).
243    pub fn dependencies_of(&self, file: &str) -> &[String] {
244        self.dependencies
245            .get(file)
246            .map(|v| v.as_slice())
247            .unwrap_or(&[])
248    }
249
250    /// Files that directly depend on `file` (reverse edge).
251    pub fn dependents_of(&self, file: &str) -> &[String] {
252        self.dependents
253            .get(file)
254            .map(|v| v.as_slice())
255            .unwrap_or(&[])
256    }
257
258    /// All files transitively depended upon by `file` (including indirect).
259    pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
260        let mut visited = std::collections::HashSet::new();
261        let mut queue = vec![file.to_string()];
262        let mut result = Vec::new();
263
264        while let Some(current) = queue.pop() {
265            if !visited.insert(current.clone()) {
266                continue;
267            }
268            for dep in self.dependencies_of(&current) {
269                if !visited.contains(dep) {
270                    queue.push(dep.clone());
271                    result.push(dep.clone());
272                }
273            }
274        }
275        result
276    }
277
278    /// All files that transitively depend on `file` (reverse transitive).
279    pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
280        let mut visited = std::collections::HashSet::new();
281        let mut queue = vec![file.to_string()];
282        let mut result = Vec::new();
283
284        while let Some(current) = queue.pop() {
285            if !visited.insert(current.clone()) {
286                continue;
287            }
288            for dep in self.dependents_of(&current) {
289                if !visited.contains(dep) {
290                    queue.push(dep.clone());
291                    result.push(dep.clone());
292                }
293            }
294        }
295        result
296    }
297}
298
299pub mod symbol;
300pub use mir_codebase::storage::{FnParam, TemplateParam, Visibility};
301pub use mir_issues::{Issue, IssueKind, Location, Severity};
302pub use mir_types::Union as Type;
303
304/// Convert a parser [`php_ast::Span`] (byte-offset range) into a
305/// [`mir_codebase::storage::Location`] (file path + 1-based line range +
306/// 0-based codepoint columns) using `source` and the parser's `source_map`.
307///
308/// This is the canonical way for consumers to translate Pass-2 result spans
309/// (e.g. [`crate::symbol::ResolvedSymbol::span`]) into source locations they
310/// can hand to their own protocol layer. Consumers that need different
311/// position semantics (LSP UTF-16 code units, byte offsets, etc.) translate
312/// from this `Location` rather than re-implementing the column math.
313pub fn location_from_span(
314    span: php_ast::Span,
315    file: std::sync::Arc<str>,
316    source: &str,
317    source_map: &php_rs_parser::source_map::SourceMap,
318) -> mir_codebase::storage::Location {
319    let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
320    let (line_end, col_end) = if span.start < span.end {
321        diagnostics::offset_to_line_col(source, span.end, source_map)
322    } else {
323        (line, col_start)
324    };
325    mir_codebase::storage::Location {
326        file,
327        line,
328        line_end,
329        col_start,
330        col_end: col_end.max(col_start.saturating_add(1)),
331    }
332}
333pub use symbol::{DocumentSymbol, DocumentSymbolKind, ResolvedSymbol, SymbolKind};
334
335pub mod composer;
336pub use composer::{ComposerError, Psr4Map};
337pub use type_env::ScopeId;
338
339#[doc(hidden)]
340pub mod test_utils;