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
54pub(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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
100pub struct Position {
101 pub line: u32,
102 pub column: u32,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct Range {
108 pub start: Position,
109 pub end: Position,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub enum Name {
120 Class(std::sync::Arc<str>),
122 Function(std::sync::Arc<str>),
124 Method {
126 class: std::sync::Arc<str>,
127 name: std::sync::Arc<str>,
128 },
129 Property {
131 class: std::sync::Arc<str>,
132 name: std::sync::Arc<str>,
133 },
134 ClassConstant {
136 class: std::sync::Arc<str>,
137 name: std::sync::Arc<str>,
138 },
139 GlobalConstant(std::sync::Arc<str>),
141}
142
143impl Name {
144 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 pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
155 Name::Class(fqcn.into())
156 }
157
158 pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
160 Name::Function(fqn.into())
161 }
162
163 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 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 pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
187 Name::GlobalConstant(fqn.into())
188 }
189
190 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#[derive(Debug, Clone, PartialEq, Eq)]
206pub enum SymbolLookupError {
207 NotFound,
209 NoSourceLocation,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum LoadOutcome {
217 AlreadyLoaded,
219 Loaded,
222 NotResolvable,
226}
227
228impl LoadOutcome {
229 pub fn is_loaded(self) -> bool {
232 !matches!(self, LoadOutcome::NotResolvable)
233 }
234}
235
236pub trait ClassResolver: Send + Sync {
244 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#[derive(Debug, Clone)]
269pub struct HoverInfo {
270 pub ty: Type,
272 pub docstring: Option<String>,
274 pub definition: Option<mir_types::Location>,
276}
277
278#[derive(Debug, Clone)]
281pub struct DependencyGraph {
282 dependencies: FxHashMap<String, Vec<String>>,
284 dependents: FxHashMap<String, Vec<String>>,
286}
287
288impl DependencyGraph {
289 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 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 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(¤t) {
316 if !visited.contains(dep) {
317 queue.push(dep.clone());
318 result.push(dep.clone());
319 }
320 }
321 }
322 result
323 }
324
325 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(¤t) {
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
351pub 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;