Skip to main content

mir_analyzer/
lib.rs

1use rustc_hash::FxHashMap;
2
3pub(crate) mod analyzer_db;
4pub(crate) mod attributes;
5pub mod batch;
6pub(crate) mod body_analysis;
7#[doc(hidden)]
8pub mod cache;
9pub(crate) mod call;
10pub(crate) mod class;
11pub(crate) mod collector;
12pub(crate) mod contradiction;
13#[doc(hidden)]
14pub mod db;
15pub(crate) mod dead_code;
16pub(crate) mod diagnostics;
17pub(crate) mod expr;
18pub mod file_analyzer;
19pub(crate) mod flow_state;
20pub(crate) mod generic;
21pub mod indexing;
22#[doc(hidden)]
23pub mod metrics;
24pub(crate) mod narrowing;
25#[doc(hidden)]
26pub mod parse_cache;
27#[doc(hidden)]
28pub mod parser;
29pub mod php_version;
30pub mod prelude;
31pub mod session;
32pub mod source_provider;
33pub(crate) mod stmt;
34#[doc(hidden)]
35pub mod stub_cache;
36#[doc(hidden)]
37pub mod stubs;
38pub(crate) mod subtype;
39pub mod suppression;
40pub(crate) mod taint;
41pub(crate) mod type_env;
42
43pub use batch::{
44    analyze_source, dead_code_issue_kinds, discover_files, AnalysisResult, BatchOptions,
45};
46pub use file_analyzer::{BatchFileAnalyzer, FileAnalysis, FileAnalyzer, ParsedFile};
47pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
48pub use parser::type_from_hint::type_from_hint;
49pub use parser::{DocblockParser, ParsedDocblock};
50pub use php_version::{ParsePhpVersionError, PhpVersion};
51pub use session::AnalysisSession;
52pub use source_provider::{FsSourceProvider, SourceProvider};
53
54/// Returns `Some((used, canonical))` when `written` and `canonical` FQCNs differ only in casing.
55/// Uses the short (last-segment) form when only the final segment is wrong and the namespace
56/// prefix is already correct; otherwise returns the full path so the mismatch is visible.
57pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
58    let w = written.trim_start_matches('\\');
59    let c = canonical.trim_start_matches('\\');
60    if w == c || !w.eq_ignore_ascii_case(c) {
61        return None;
62    }
63    let w_last = w.rsplit('\\').next().unwrap_or(w);
64    let c_last = c.rsplit('\\').next().unwrap_or(c);
65    if w_last != c_last {
66        let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
67        let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
68        if w_prefix == c_prefix {
69            return Some((w_last.to_string(), c_last.to_string()));
70        }
71    }
72    Some((w.to_string(), c.to_string()))
73}
74pub use stubs::{
75    is_builtin_function, stub_files, stub_path_for_class, ChainedClassResolver, StubClassResolver,
76    StubVfs,
77};
78
79// ============================================================================
80// Analysis entry points
81// ============================================================================
82//
83// `AnalysisSession` is the single analysis engine. It supports two usage modes:
84//
85// - Batch (CLI, CI, bulk analysis): use `analyze_paths` / `BatchOptions` to
86//   run definition collection and body analysis over many files in parallel.
87//
88// - Incremental (LSP, watch mode): ingest files as they change; per-file
89//   results come from `FileAnalyzer::analyze`. Builder-style configuration
90//   (`with_cache`, `with_psr4`, …).
91//
92// The two phases of analysis are:
93//   1. Definition collection — discovers classes, functions, constants in a
94//      file and registers them in the salsa database.
95//   2. Body analysis (`BodyAnalyzer`) — walks function/method bodies,
96//      inferring types and emitting issues.
97
98/// A position in source code: 1-based line, 0-based codepoint column.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
100pub struct Position {
101    pub line: u32,
102    pub column: u32,
103}
104
105/// A range in source code: start and end positions.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct Range {
108    pub start: Position,
109    pub end: Position,
110}
111
112/// A semantic identifier for a code entity that the analyzer can resolve.
113///
114/// Replaces the previous stringly-typed `&str` keys. Method names are
115/// normalized (lowercased) at construction since PHP method dispatch is
116/// case-insensitive — this prevents a class of correctness bugs where
117/// callers pass mixed-case names and get empty results.
118#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub enum Name {
120    /// A class, interface, trait, or enum (FQCN).
121    Class(std::sync::Arc<str>),
122    /// A global function (FQN).
123    Function(std::sync::Arc<str>),
124    /// An instance or static method.
125    Method {
126        class: std::sync::Arc<str>,
127        name: std::sync::Arc<str>,
128    },
129    /// A class property.
130    Property {
131        class: std::sync::Arc<str>,
132        name: std::sync::Arc<str>,
133    },
134    /// A class / interface / enum constant.
135    ClassConstant {
136        class: std::sync::Arc<str>,
137        name: std::sync::Arc<str>,
138    },
139    /// A global constant.
140    GlobalConstant(std::sync::Arc<str>),
141}
142
143impl Name {
144    /// Construct a method symbol. Normalizes `name` to lowercase since PHP
145    /// methods are case-insensitive.
146    pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
147        Name::Method {
148            class: class.into(),
149            name: std::sync::Arc::from(name.to_ascii_lowercase()),
150        }
151    }
152
153    /// Construct a class symbol.
154    pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
155        Name::Class(fqcn.into())
156    }
157
158    /// Construct a function symbol.
159    pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
160        Name::Function(fqn.into())
161    }
162
163    /// Construct a property symbol.
164    pub fn property(
165        class: impl Into<std::sync::Arc<str>>,
166        name: impl Into<std::sync::Arc<str>>,
167    ) -> Self {
168        Name::Property {
169            class: class.into(),
170            name: name.into(),
171        }
172    }
173
174    /// Construct a class constant symbol.
175    pub fn class_constant(
176        class: impl Into<std::sync::Arc<str>>,
177        name: impl Into<std::sync::Arc<str>>,
178    ) -> Self {
179        Name::ClassConstant {
180            class: class.into(),
181            name: name.into(),
182        }
183    }
184
185    /// Construct a global constant symbol.
186    pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
187        Name::GlobalConstant(fqn.into())
188    }
189
190    /// The codebase lookup key for this symbol (used internally for the
191    /// reference-locations index). Stable across releases.
192    pub fn codebase_key(&self) -> String {
193        match self {
194            Name::Class(fqcn) => fqcn.to_string(),
195            Name::Function(fqn) => fqn.to_string(),
196            Name::Method { class, name } => format!("{class}::{name}"),
197            Name::Property { class, name } => format!("{class}::{name}"),
198            Name::ClassConstant { class, name } => format!("{class}::{name}"),
199            Name::GlobalConstant(fqn) => fqn.to_string(),
200        }
201    }
202}
203
204/// Reason a symbol lookup did not return a location.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub enum SymbolLookupError {
207    /// No such symbol exists in the codebase.
208    NotFound,
209    /// The symbol exists but has no recorded source location (e.g. a
210    /// stub-only declaration without a span).
211    NoSourceLocation,
212}
213
214/// Outcome of a [`AnalysisSession::load_class`] attempt.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum LoadOutcome {
217    /// The symbol was already present in the session; no work performed.
218    AlreadyLoaded,
219    /// The symbol was resolved by the configured [`ClassResolver`] and the
220    /// defining file was ingested.
221    Loaded,
222    /// No resolver is configured, the resolver could not map the FQCN to a
223    /// file, or the resolved file could not be read / did not define the
224    /// requested symbol.
225    NotResolvable,
226}
227
228impl LoadOutcome {
229    /// `true` when the symbol is now present in the session (whether it was
230    /// already there or just freshly loaded).
231    pub fn is_loaded(self) -> bool {
232        !matches!(self, LoadOutcome::NotResolvable)
233    }
234}
235
236/// Pluggable strategy for mapping a fully-qualified class name to the file
237/// that should define it. The analyzer never touches `vendor/` or the
238/// filesystem on its own — it asks a `ClassResolver` when a symbol is needed.
239///
240/// `mir_analyzer::Psr4Map` is the built-in implementation for Composer-based
241/// projects. Consumers with non-Composer conventions (WordPress, Drupal, a
242/// custom autoloader, a workspace-walk index) supply their own.
243pub trait ClassResolver: Send + Sync {
244    /// Resolve `fqcn` to the file that defines it. Returning `None` causes
245    /// the analyzer to fall back to emitting `UndefinedClass`.
246    fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
247}
248
249impl ClassResolver for composer::Psr4Map {
250    fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
251        composer::Psr4Map::resolve(self, fqcn)
252    }
253}
254
255impl std::fmt::Display for SymbolLookupError {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        match self {
258            SymbolLookupError::NotFound => write!(f, "symbol not found"),
259            SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
260        }
261    }
262}
263
264impl std::error::Error for SymbolLookupError {}
265
266/// Hover information for a symbol at a source location.
267/// Includes the inferred type, optional docstring, and location of definition.
268#[derive(Debug, Clone)]
269pub struct HoverInfo {
270    /// Inferred type of the symbol.
271    pub ty: Type,
272    /// Docstring / documentation comment for the symbol (if available).
273    pub docstring: Option<String>,
274    /// Source location of the symbol's definition.
275    pub definition: Option<mir_types::Location>,
276}
277
278/// File dependency graph: tracks which files depend on which other files.
279/// Used for incremental invalidation in LSP servers and build systems.
280#[derive(Debug, Clone)]
281pub struct DependencyGraph {
282    /// Direct dependencies: file → [files it depends on]
283    dependencies: FxHashMap<String, Vec<String>>,
284    /// Reverse dependencies: file → [files that depend on it]
285    dependents: FxHashMap<String, Vec<String>>,
286}
287
288impl DependencyGraph {
289    /// Files that `file` directly depends on (imports, parent classes, interfaces, traits).
290    pub fn dependencies_of(&self, file: &str) -> &[String] {
291        self.dependencies
292            .get(file)
293            .map(|v| v.as_slice())
294            .unwrap_or(&[])
295    }
296
297    /// Files that directly depend on `file` (reverse edge).
298    pub fn dependents_of(&self, file: &str) -> &[String] {
299        self.dependents
300            .get(file)
301            .map(|v| v.as_slice())
302            .unwrap_or(&[])
303    }
304
305    /// All files transitively depended upon by `file` (including indirect).
306    pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
307        let mut visited = rustc_hash::FxHashSet::default();
308        let mut queue = vec![file.to_string()];
309        let mut result = Vec::new();
310
311        while let Some(current) = queue.pop() {
312            if !visited.insert(current.clone()) {
313                continue;
314            }
315            for dep in self.dependencies_of(&current) {
316                if !visited.contains(dep) {
317                    queue.push(dep.clone());
318                    result.push(dep.clone());
319                }
320            }
321        }
322        result
323    }
324
325    /// All files that transitively depend on `file` (reverse transitive).
326    pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
327        let mut visited = rustc_hash::FxHashSet::default();
328        let mut queue = vec![file.to_string()];
329        let mut result = Vec::new();
330
331        while let Some(current) = queue.pop() {
332            if !visited.insert(current.clone()) {
333                continue;
334            }
335            for dep in self.dependents_of(&current) {
336                if !visited.contains(dep) {
337                    queue.push(dep.clone());
338                    result.push(dep.clone());
339                }
340            }
341        }
342        result
343    }
344}
345
346pub mod symbol;
347pub use mir_codebase::storage::{FnParam, TemplateParam, Visibility};
348pub use mir_issues::{Issue, IssueKind, Severity};
349pub use mir_types::Type;
350
351/// Convert a parser [`php_ast::Span`] (byte-offset range) into a
352/// [`mir_types::Location`] (file path + 1-based line range +
353/// 0-based codepoint columns) using `source` and the parser's `source_map`.
354///
355/// This is the canonical way for consumers to translate body-analysis result spans
356/// (e.g. [`crate::symbol::ResolvedSymbol::span`]) into source locations they
357/// can hand to their own protocol layer. Consumers that need different
358/// position semantics (LSP UTF-16 code units, byte offsets, etc.) translate
359/// from this `Location` rather than re-implementing the column math.
360pub fn location_from_span(
361    span: php_ast::Span,
362    file: std::sync::Arc<str>,
363    source: &str,
364    source_map: &php_rs_parser::source_map::SourceMap,
365) -> mir_types::Location {
366    let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
367    let (line_end, col_end) = if span.start < span.end {
368        diagnostics::offset_to_line_col(source, span.end, source_map)
369    } else {
370        (line, col_start)
371    };
372    mir_types::Location {
373        file,
374        line,
375        line_end,
376        col_start,
377        col_end: col_end.max(col_start.saturating_add(1)),
378    }
379}
380pub use symbol::{DeclarationKind, DocumentSymbol, ReferenceKind, ResolvedSymbol};
381
382pub mod composer;
383pub use composer::{ComposerError, Psr4Map};
384pub use type_env::ScopeId;
385
386#[doc(hidden)]
387pub mod test_utils;