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;
42pub(crate) mod util;
43
44pub use batch::{
45 analyze_source, dead_code_issue_kinds, discover_files, AnalysisResult, BatchOptions,
46};
47pub use file_analyzer::{FileAnalysis, FileAnalyzer};
48pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
49pub use parser::type_from_hint::type_from_hint;
50pub use parser::{DocblockParser, ParsedDocblock};
51pub use php_version::{ParsePhpVersionError, PhpVersion};
52pub use session::{AnalysisSession, SubtypeClassSite};
53pub use source_provider::{FsSourceProvider, SourceProvider};
54
55pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
59 let w = written.trim_start_matches('\\');
60 let c = canonical.trim_start_matches('\\');
61 if w == c || !w.eq_ignore_ascii_case(c) {
62 return None;
63 }
64 let w_last = w.rsplit('\\').next().unwrap_or(w);
65 let c_last = c.rsplit('\\').next().unwrap_or(c);
66 if w_last != c_last {
67 let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
68 let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
69 if w_prefix == c_prefix {
70 return Some((w_last.to_string(), c_last.to_string()));
71 }
72 }
73 Some((w.to_string(), c.to_string()))
74}
75pub use stubs::{
76 is_builtin_function, stub_files, stub_path_for_class, ChainedClassResolver, StubClassResolver,
77 StubVfs,
78};
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
101pub struct Position {
102 pub line: u32,
103 pub column: u32,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108pub struct Range {
109 pub start: Position,
110 pub end: Position,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub enum Name {
121 Class(std::sync::Arc<str>),
123 Function(std::sync::Arc<str>),
125 Method {
127 class: std::sync::Arc<str>,
128 name: std::sync::Arc<str>,
129 },
130 Property {
132 class: std::sync::Arc<str>,
133 name: std::sync::Arc<str>,
134 },
135 ClassConstant {
137 class: std::sync::Arc<str>,
138 name: std::sync::Arc<str>,
139 },
140 GlobalConstant(std::sync::Arc<str>),
142}
143
144impl Name {
145 pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
148 Name::Method {
149 class: class.into(),
150 name: std::sync::Arc::from(name.to_ascii_lowercase()),
151 }
152 }
153
154 pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
156 Name::Class(fqcn.into())
157 }
158
159 pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
161 Name::Function(fqn.into())
162 }
163
164 pub fn property(
166 class: impl Into<std::sync::Arc<str>>,
167 name: impl Into<std::sync::Arc<str>>,
168 ) -> Self {
169 Name::Property {
170 class: class.into(),
171 name: name.into(),
172 }
173 }
174
175 pub fn class_constant(
177 class: impl Into<std::sync::Arc<str>>,
178 name: impl Into<std::sync::Arc<str>>,
179 ) -> Self {
180 Name::ClassConstant {
181 class: class.into(),
182 name: name.into(),
183 }
184 }
185
186 pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
188 Name::GlobalConstant(fqn.into())
189 }
190
191 pub fn codebase_key(&self) -> String {
201 match self {
202 Name::Class(fqcn) => format!("cls:{fqcn}"),
203 Name::Function(fqn) => format!("fn:{fqn}"),
204 Name::Method { class, name } => format!("meth:{class}::{name}"),
205 Name::Property { class, name } => format!("prop:{class}::{name}"),
206 Name::ClassConstant { class, name } => format!("cnst:{class}::{name}"),
207 Name::GlobalConstant(fqn) => format!("gcnst:{fqn}"),
208 }
209 }
210}
211
212pub(crate) fn defining_file_lookup_key(symbol_key: &str) -> &str {
220 let stripped = symbol_key
221 .strip_prefix("meth:")
222 .or_else(|| symbol_key.strip_prefix("prop:"))
223 .or_else(|| symbol_key.strip_prefix("cnst:"))
224 .or_else(|| symbol_key.strip_prefix("cls:"))
225 .or_else(|| symbol_key.strip_prefix("fn:"))
226 .or_else(|| symbol_key.strip_prefix("gcnst:"))
227 .unwrap_or(symbol_key);
228 match stripped.split_once("::") {
229 Some((class, _)) => class,
230 None => stripped,
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum SymbolLookupError {
237 NotFound,
239 NoSourceLocation,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum LoadOutcome {
247 AlreadyLoaded,
249 Loaded,
252 NotResolvable,
256}
257
258impl LoadOutcome {
259 pub fn is_loaded(self) -> bool {
262 !matches!(self, LoadOutcome::NotResolvable)
263 }
264}
265
266pub trait ClassResolver: Send + Sync {
274 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
277}
278
279impl ClassResolver for composer::Psr4Map {
280 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
281 composer::Psr4Map::resolve(self, fqcn)
282 }
283}
284
285impl std::fmt::Display for SymbolLookupError {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 match self {
288 SymbolLookupError::NotFound => write!(f, "symbol not found"),
289 SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
290 }
291 }
292}
293
294impl std::error::Error for SymbolLookupError {}
295
296#[derive(Debug, Clone)]
299pub struct HoverInfo {
300 pub ty: Type,
302 pub docstring: Option<String>,
304 pub definition: Option<mir_types::Location>,
306}
307
308#[derive(Debug, Clone)]
311pub struct DependencyGraph {
312 dependencies: FxHashMap<String, Vec<String>>,
314 dependents: FxHashMap<String, Vec<String>>,
316}
317
318impl DependencyGraph {
319 pub fn dependencies_of(&self, file: &str) -> &[String] {
321 self.dependencies
322 .get(file)
323 .map(|v| v.as_slice())
324 .unwrap_or(&[])
325 }
326
327 pub fn dependents_of(&self, file: &str) -> &[String] {
329 self.dependents
330 .get(file)
331 .map(|v| v.as_slice())
332 .unwrap_or(&[])
333 }
334
335 pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
337 let mut visited = rustc_hash::FxHashSet::default();
338 let mut queue = vec![file.to_string()];
339 let mut result = Vec::new();
340
341 while let Some(current) = queue.pop() {
342 if !visited.insert(current.clone()) {
343 continue;
344 }
345 for dep in self.dependencies_of(¤t) {
346 if !visited.contains(dep) {
347 queue.push(dep.clone());
348 result.push(dep.clone());
349 }
350 }
351 }
352 result
353 }
354
355 pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
357 let mut visited = rustc_hash::FxHashSet::default();
358 let mut queue = vec![file.to_string()];
359 let mut result = Vec::new();
360
361 while let Some(current) = queue.pop() {
362 if !visited.insert(current.clone()) {
363 continue;
364 }
365 for dep in self.dependents_of(¤t) {
366 if !visited.contains(dep) {
367 queue.push(dep.clone());
368 result.push(dep.clone());
369 }
370 }
371 }
372 result
373 }
374}
375
376pub mod symbol;
377pub use mir_codebase::definitions::{DeclaredParam, TemplateParam, Visibility};
378pub use mir_issues::{Issue, IssueKind, Severity};
379pub use mir_types::Type;
380
381pub fn location_from_span(
391 span: php_ast::Span,
392 file: std::sync::Arc<str>,
393 source: &str,
394 source_map: &php_rs_parser::source_map::SourceMap,
395) -> mir_types::Location {
396 let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
397 let (line_end, col_end) = if span.start < span.end {
398 diagnostics::offset_to_line_col(source, span.end, source_map)
399 } else {
400 (line, col_start)
401 };
402 mir_types::Location {
403 file,
404 line,
405 line_end,
406 col_start,
407 col_end: diagnostics::clamp_col_end(line, line_end, col_start, col_end),
408 }
409}
410pub use symbol::{DeclarationKind, DocumentSymbol, ReferenceKind, ResolvedSymbol};
411
412pub mod composer;
413pub use composer::{ComposerError, Psr4Map};
414pub use type_env::ScopeId;
415
416#[doc(hidden)]
417pub mod test_utils;