use rustc_hash::FxHashMap;
use std::sync::{Arc, OnceLock};
pub(crate) mod analyzer_db;
pub(crate) mod attributes;
pub mod batch;
pub(crate) mod body_analysis;
#[doc(hidden)]
pub mod cache;
pub(crate) mod call;
pub(crate) mod class;
pub(crate) mod collector;
pub(crate) mod contradiction;
#[doc(hidden)]
pub mod db;
pub(crate) mod dead_code;
pub(crate) mod diagnostics;
pub(crate) mod docblock_nav;
pub(crate) mod expr;
pub mod file_analyzer;
mod file_extensions;
pub(crate) mod flow_state;
pub(crate) mod generic;
pub mod include_graph;
pub mod indexing;
#[doc(hidden)]
pub mod metrics;
pub(crate) mod narrowing;
#[doc(hidden)]
pub mod parse_cache;
#[doc(hidden)]
pub mod parser;
#[doc(hidden)]
pub mod perf_fixture;
pub mod php_version;
pub mod prelude;
pub mod recursion;
pub(crate) mod reference_key;
pub mod session;
pub mod source_provider;
pub(crate) mod stmt;
#[doc(hidden)]
pub mod stub_cache;
#[doc(hidden)]
pub mod stubs;
pub(crate) mod subtype;
pub mod suppression;
pub(crate) mod taint;
mod tmp_suffix;
pub(crate) mod type_env;
pub(crate) mod util;
pub use batch::{
analyze_source, analyze_source_with_options, dead_code_issue_kinds, discover_files,
discover_files_with_extensions, unused_variable_issue_kinds, AnalysisResult, BatchOptions,
};
pub use file_analyzer::{FileAnalysis, FileAnalyzer};
pub use file_extensions::PhpFileExtensions;
pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
pub use parser::type_from_hint::type_from_hint;
pub use parser::{DocblockParser, ParsedDocblock};
pub use php_version::{ParsePhpVersionError, PhpVersion};
pub use session::{AnalysisSession, AnalysisSnapshot, ReferenceIncludes, SubtypeClassSite};
pub use source_provider::{read_php_source, FsSourceProvider, SourceProvider};
pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
let w = written.trim_start_matches('\\');
let c = canonical.trim_start_matches('\\');
if w == c || !w.eq_ignore_ascii_case(c) {
return None;
}
let w_last = w.rsplit('\\').next().unwrap_or(w);
let c_last = c.rsplit('\\').next().unwrap_or(c);
if w_last != c_last {
let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
if w_prefix == c_prefix {
return Some((w_last.to_string(), c_last.to_string()));
}
}
Some((w.to_string(), c.to_string()))
}
pub use stubs::{
is_builtin_constant, is_builtin_function, stub_files, stub_path_for_class,
ChainedClassResolver, StubClassResolver, StubVfs,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Position {
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Range {
pub start: Position,
pub end: Position,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Name {
Class(std::sync::Arc<str>),
Function(std::sync::Arc<str>),
Method {
class: std::sync::Arc<str>,
name: std::sync::Arc<str>,
},
Property {
class: std::sync::Arc<str>,
name: std::sync::Arc<str>,
},
ClassConstant {
class: std::sync::Arc<str>,
name: std::sync::Arc<str>,
},
GlobalConstant(std::sync::Arc<str>),
}
impl Name {
pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
Name::Method {
class: class.into(),
name: std::sync::Arc::from(name.to_ascii_lowercase()),
}
}
pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
Name::Class(fqcn.into())
}
pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
Name::Function(fqn.into())
}
pub fn property(
class: impl Into<std::sync::Arc<str>>,
name: impl Into<std::sync::Arc<str>>,
) -> Self {
Name::Property {
class: class.into(),
name: name.into(),
}
}
pub fn class_constant(
class: impl Into<std::sync::Arc<str>>,
name: impl Into<std::sync::Arc<str>>,
) -> Self {
Name::ClassConstant {
class: class.into(),
name: name.into(),
}
}
pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
Name::GlobalConstant(fqn.into())
}
pub fn codebase_key(&self) -> String {
match self {
Name::Class(fqcn) => format!("cls:{fqcn}"),
Name::Function(fqn) => format!("fn:{fqn}"),
Name::Method { class, name } => format!("meth:{class}::{name}"),
Name::Property { class, name } => format!("prop:{class}::{name}"),
Name::ClassConstant { class, name } => format!("cnst:{class}::{name}"),
Name::GlobalConstant(fqn) => format!("gcnst:{fqn}"),
}
}
}
pub(crate) fn defining_file_lookup_key(symbol_key: &str) -> &str {
let stripped = symbol_key
.strip_prefix("meth:")
.or_else(|| symbol_key.strip_prefix("prop:"))
.or_else(|| symbol_key.strip_prefix("cnst:"))
.or_else(|| symbol_key.strip_prefix("cls:"))
.or_else(|| symbol_key.strip_prefix("fn:"))
.or_else(|| symbol_key.strip_prefix("gcnst:"))
.unwrap_or(symbol_key);
match stripped.split_once("::") {
Some((class, _)) => class,
None => stripped,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolLookupError {
NotFound,
NoSourceLocation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadOutcome {
AlreadyLoaded,
Loaded,
NotResolvable,
}
impl LoadOutcome {
pub fn is_loaded(self) -> bool {
!matches!(self, LoadOutcome::NotResolvable)
}
}
pub trait ClassResolver: Send + Sync {
fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
}
impl ClassResolver for composer::Psr4Map {
fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
composer::Psr4Map::resolve(self, fqcn)
}
}
impl std::fmt::Display for SymbolLookupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SymbolLookupError::NotFound => write!(f, "symbol not found"),
SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
}
}
}
impl std::error::Error for SymbolLookupError {}
#[derive(Debug)]
pub struct DependencyGraph {
files: Vec<Arc<str>>,
file_ids: FxHashMap<Arc<str>, u32>,
dependencies: Vec<Vec<u32>>,
dependents: Vec<Vec<u32>>,
legacy_dependencies: OnceLock<FxHashMap<String, Vec<String>>>,
legacy_dependents: OnceLock<FxHashMap<String, Vec<String>>>,
}
impl Clone for DependencyGraph {
fn clone(&self) -> Self {
Self {
files: self.files.clone(),
file_ids: self.file_ids.clone(),
dependencies: self.dependencies.clone(),
dependents: self.dependents.clone(),
legacy_dependencies: OnceLock::new(),
legacy_dependents: OnceLock::new(),
}
}
}
impl DependencyGraph {
pub(crate) fn from_compact_parts(
files: Vec<Arc<str>>,
file_ids: FxHashMap<Arc<str>, u32>,
dependencies: Vec<Vec<u32>>,
dependents: Vec<Vec<u32>>,
) -> Self {
Self {
files,
file_ids,
dependencies,
dependents,
legacy_dependencies: OnceLock::new(),
legacy_dependents: OnceLock::new(),
}
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn dependency_edge_count(&self) -> usize {
self.dependencies.iter().map(Vec::len).sum()
}
pub fn dependent_edge_count(&self) -> usize {
self.dependents.iter().map(Vec::len).sum()
}
fn file_path(&self, id: u32) -> &Arc<str> {
&self.files[id as usize]
}
fn stringify_adjacency(&self, adjacency: &[Vec<u32>]) -> FxHashMap<String, Vec<String>> {
adjacency
.iter()
.enumerate()
.filter(|(_, deps)| !deps.is_empty())
.map(|(file_id, deps)| {
(
self.files[file_id].to_string(),
deps.iter()
.map(|&dep_id| self.file_path(dep_id).to_string())
.collect(),
)
})
.collect()
}
fn direct_arcs<'a>(
&'a self,
file: &str,
adjacency: &'a [Vec<u32>],
) -> impl Iterator<Item = &'a Arc<str>> {
self.file_ids
.get(file)
.and_then(|&id| adjacency.get(id as usize))
.into_iter()
.flatten()
.map(|&id| self.file_path(id))
}
pub fn dependency_paths_of(&self, file: &str) -> Vec<Arc<str>> {
self.direct_arcs(file, &self.dependencies)
.cloned()
.collect()
}
pub fn dependent_paths_of(&self, file: &str) -> Vec<Arc<str>> {
self.direct_arcs(file, &self.dependents).cloned().collect()
}
pub fn dependencies_of(&self, file: &str) -> &[String] {
self.legacy_dependencies
.get_or_init(|| self.stringify_adjacency(&self.dependencies))
.get(file)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn dependents_of(&self, file: &str) -> &[String] {
self.legacy_dependents
.get_or_init(|| self.stringify_adjacency(&self.dependents))
.get(file)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
let mut visited = rustc_hash::FxHashSet::default();
let Some(&seed) = self.file_ids.get(file) else {
return Vec::new();
};
let mut queue = vec![seed];
visited.insert(seed);
let mut result = Vec::new();
while let Some(current) = queue.pop() {
for &dep in &self.dependencies[current as usize] {
if visited.insert(dep) {
queue.push(dep);
result.push(self.file_path(dep).to_string());
}
}
}
result
}
pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
let mut visited = rustc_hash::FxHashSet::default();
let Some(&seed) = self.file_ids.get(file) else {
return Vec::new();
};
let mut queue = vec![seed];
visited.insert(seed);
let mut result = Vec::new();
while let Some(current) = queue.pop() {
for &dep in &self.dependents[current as usize] {
if visited.insert(dep) {
queue.push(dep);
result.push(self.file_path(dep).to_string());
}
}
}
result
}
}
pub mod symbol;
pub use mir_codebase::definitions::{
DeclaredParam, FunctionDef, TagDescriptions, TemplateParam, Visibility,
};
pub use mir_issues::{Issue, IssueKind, Severity};
pub use mir_types::Type;
#[cfg(test)]
mod dependency_graph_tests {
use super::*;
fn graph(edges: &[(&str, &[&str])]) -> DependencyGraph {
let mut files: Vec<Arc<str>> = edges
.iter()
.flat_map(|(from, deps)| {
std::iter::once(*from)
.chain(deps.iter().copied())
.map(Arc::<str>::from)
})
.collect();
files.sort();
files.dedup();
let file_ids: FxHashMap<Arc<str>, u32> = files
.iter()
.enumerate()
.map(|(id, file)| (file.clone(), id as u32))
.collect();
let mut dependencies = vec![Vec::new(); files.len()];
let mut dependents = vec![Vec::new(); files.len()];
for (from, deps) in edges {
let from_id = file_ids[*from];
for dep in *deps {
let dep_id = file_ids[*dep];
dependencies[from_id as usize].push(dep_id);
dependents[dep_id as usize].push(from_id);
}
}
for adjacency in [&mut dependencies, &mut dependents] {
for deps in adjacency {
deps.sort();
deps.dedup();
}
}
DependencyGraph::from_compact_parts(files, file_ids, dependencies, dependents)
}
#[test]
fn transitive_traversal_deduplicates_diamond_graphs() {
let graph = graph(&[
("a.php", &["b.php", "c.php"]),
("b.php", &["d.php"]),
("c.php", &["d.php"]),
]);
let deps = graph.transitive_dependencies("a.php");
assert_eq!(
deps.iter().filter(|path| path.as_str() == "d.php").count(),
1
);
assert_eq!(deps.len(), 3);
let dependents = graph.transitive_dependents("d.php");
assert_eq!(
dependents
.iter()
.filter(|path| path.as_str() == "a.php")
.count(),
1
);
assert_eq!(dependents.len(), 3);
}
#[test]
fn direct_arc_accessors_do_not_require_legacy_string_maps() {
let graph = graph(&[("consumer.php", &["service.php"])]);
let deps = graph.dependency_paths_of("consumer.php");
assert_eq!(deps, vec![Arc::<str>::from("service.php")]);
assert!(graph.legacy_dependencies.get().is_none());
let dependents = graph.dependent_paths_of("service.php");
assert_eq!(dependents, vec![Arc::<str>::from("consumer.php")]);
assert!(graph.legacy_dependents.get().is_none());
assert_eq!(
graph.dependencies_of("consumer.php"),
&["service.php".to_string()]
);
assert!(graph.legacy_dependencies.get().is_some());
}
}
pub fn location_from_span(
span: php_ast::Span,
file: std::sync::Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
) -> mir_types::Location {
let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
let (line_end, col_end) = if span.start < span.end {
diagnostics::offset_to_line_col(source, span.end, source_map)
} else {
(line, col_start)
};
mir_types::Location {
file,
line,
line_end,
col_start,
col_end: diagnostics::clamp_col_end(line, line_end, col_start, col_end),
}
}
pub use symbol::{DeclarationKind, DocumentSymbol, ReferenceKind, ResolvedSymbol};
pub mod composer;
pub use composer::{ComposerError, Psr4Map};
pub use include_graph::{include_closure, VendorTargets};
pub use type_env::ScopeId;
#[doc(hidden)]
pub mod test_utils;