use std::mem::ManuallyDrop;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use crate::cache::{hash_content, AnalysisCache};
use crate::db::{
collect_file_definitions, collect_file_definitions_uncached, FileDefinitions, MirDatabase,
MirDb, SourceFile,
};
use crate::pass2::Pass2Driver;
use crate::php_version::PhpVersion;
use mir_issues::Issue;
use mir_types::Union;
use salsa::Setter as _;
pub use crate::pass2::merge_return_types;
pub struct ProjectAnalyzer {
pub cache: Option<AnalysisCache>,
pub on_file_done: Option<Arc<dyn Fn() + Send + Sync>>,
pub psr4: Option<Arc<crate::composer::Psr4Map>>,
stubs_loaded: std::sync::atomic::AtomicBool,
pub find_dead_code: bool,
pub php_version: Option<PhpVersion>,
pub stub_files: Vec<PathBuf>,
pub stub_dirs: Vec<PathBuf>,
salsa: std::sync::Mutex<(MirDb, HashMap<Arc<str>, SourceFile>)>,
}
struct ParsedProjectFile {
file: Arc<str>,
source: Arc<str>,
parsed: ManuallyDrop<php_rs_parser::ParseResult<'static, 'static>>,
arena: ManuallyDrop<Box<bumpalo::Bump>>,
}
impl ParsedProjectFile {
fn new(file: Arc<str>, source: Arc<str>) -> Self {
let arena = Box::new(bumpalo::Bump::new());
let parsed = php_rs_parser::parse(&arena, &source);
let parsed = unsafe {
std::mem::transmute::<
php_rs_parser::ParseResult<'_, '_>,
php_rs_parser::ParseResult<'static, 'static>,
>(parsed)
};
Self {
file,
source,
parsed: ManuallyDrop::new(parsed),
arena: ManuallyDrop::new(arena),
}
}
fn source(&self) -> &str {
self.source.as_ref()
}
fn parsed(&self) -> &php_rs_parser::ParseResult<'_, '_> {
&self.parsed
}
}
impl Drop for ParsedProjectFile {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.parsed);
ManuallyDrop::drop(&mut self.arena);
}
}
}
unsafe impl Send for ParsedProjectFile {}
unsafe impl Sync for ParsedProjectFile {}
impl ProjectAnalyzer {
pub fn new() -> Self {
Self {
cache: None,
on_file_done: None,
psr4: None,
stubs_loaded: std::sync::atomic::AtomicBool::new(false),
find_dead_code: false,
php_version: None,
stub_files: Vec::new(),
stub_dirs: Vec::new(),
salsa: std::sync::Mutex::new((MirDb::default(), HashMap::new())),
}
}
pub fn with_cache(cache_dir: &Path) -> Self {
Self {
cache: Some(AnalysisCache::open(cache_dir)),
on_file_done: None,
psr4: None,
stubs_loaded: std::sync::atomic::AtomicBool::new(false),
find_dead_code: false,
php_version: None,
stub_files: Vec::new(),
stub_dirs: Vec::new(),
salsa: std::sync::Mutex::new((MirDb::default(), HashMap::new())),
}
}
pub fn from_composer(
root: &Path,
) -> Result<(Self, crate::composer::Psr4Map), crate::composer::ComposerError> {
let map = crate::composer::Psr4Map::from_composer(root)?;
let psr4 = Arc::new(map.clone());
let analyzer = Self {
cache: None,
on_file_done: None,
psr4: Some(psr4),
stubs_loaded: std::sync::atomic::AtomicBool::new(false),
find_dead_code: false,
php_version: None,
stub_files: Vec::new(),
stub_dirs: Vec::new(),
salsa: std::sync::Mutex::new((MirDb::default(), HashMap::new())),
};
Ok((analyzer, map))
}
pub fn with_php_version(mut self, version: PhpVersion) -> Self {
self.php_version = Some(version);
self
}
fn resolved_php_version(&self) -> PhpVersion {
self.php_version.unwrap_or(PhpVersion::LATEST)
}
fn type_exists(&self, fqcn: &str) -> bool {
let guard = self.salsa.lock().expect("salsa lock poisoned");
crate::db::type_exists_via_db(&guard.0, fqcn)
}
#[doc(hidden)]
pub fn salsa_db_for_test(&self) -> &std::sync::Mutex<(MirDb, HashMap<Arc<str>, SourceFile>)> {
&self.salsa
}
pub fn member_location(
&self,
fqcn: &str,
member_name: &str,
) -> Option<mir_codebase::storage::Location> {
let guard = self.salsa.lock().expect("salsa lock poisoned");
crate::db::member_location_via_db(&guard.0, fqcn, member_name)
}
pub fn symbol_location(&self, symbol: &str) -> Option<mir_codebase::storage::Location> {
let guard = self.salsa.lock().expect("salsa lock poisoned");
let db = &guard.0;
db.lookup_class_node(symbol)
.filter(|n| n.active(db))
.and_then(|n| n.location(db))
.or_else(|| {
db.lookup_function_node(symbol)
.filter(|n| n.active(db))
.and_then(|n| n.location(db))
})
}
pub fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.reference_locations(symbol)
}
pub fn load_stubs(&self) {
if !self
.stubs_loaded
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
let php_version = self.resolved_php_version();
crate::stubs::stub_files()
.par_iter()
.for_each(|(filename, content)| {
let slice =
crate::stubs::stub_slice_from_source(filename, content, Some(php_version));
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.ingest_stub_slice(&slice);
});
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
for slice in crate::stubs::user_stub_slices(&self.stub_files, &self.stub_dirs) {
guard.0.ingest_stub_slice(&slice);
}
}
}
fn collect_and_ingest_source(&self, file: Arc<str>, src: &str) -> FileDefinitions {
let file_defs = {
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, ref mut files) = *guard;
let salsa_file = match files.get(&file) {
Some(&sf) => {
if sf.text(db).as_ref() != src {
sf.set_text(db).to(Arc::from(src));
}
sf
}
None => {
let sf = SourceFile::new(db, file.clone(), Arc::from(src));
files.insert(file.clone(), sf);
sf
}
};
collect_file_definitions(db, salsa_file)
};
{
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.ingest_stub_slice(&file_defs.slice);
}
file_defs
}
pub fn analyze(&self, paths: &[PathBuf]) -> AnalysisResult {
let mut all_issues = Vec::new();
self.load_stubs();
let parsed_files: Vec<ParsedProjectFile> = paths
.par_iter()
.filter_map(|path| match std::fs::read_to_string(path) {
Ok(src) => {
let file = Arc::from(path.to_string_lossy().as_ref());
Some(ParsedProjectFile::new(file, Arc::from(src)))
}
Err(e) => {
eprintln!("Cannot read {}: {}", path.display(), e);
None
}
})
.collect();
let file_data: Vec<(Arc<str>, Arc<str>)> = parsed_files
.iter()
.map(|parsed| (parsed.file.clone(), parsed.source.clone()))
.collect();
if let Some(cache) = &self.cache {
let changed: Vec<String> = file_data
.par_iter()
.filter_map(|(f, src)| {
let h = hash_content(src.as_ref());
if cache.get(f, &h).is_none() {
Some(f.to_string())
} else {
None
}
})
.collect();
if !changed.is_empty() {
cache.evict_with_dependents(&changed);
}
}
{
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, ref mut files) = *guard;
for parsed in &parsed_files {
match files.get(parsed.file.as_ref()) {
Some(&sf) => {
if sf.text(db).as_ref() != parsed.source() {
sf.set_text(db).to(parsed.source.clone());
}
}
None => {
let sf = SourceFile::new(db, parsed.file.clone(), parsed.source.clone());
files.insert(parsed.file.clone(), sf);
}
}
}
}
let file_defs: Vec<FileDefinitions> = parsed_files
.par_iter()
.map(|parsed| {
let parse_result = parsed.parsed();
let mut all_issues: Vec<Issue> = parse_result
.errors
.iter()
.map(|err| {
Issue::new(
mir_issues::IssueKind::ParseError {
message: err.to_string(),
},
mir_issues::Location {
file: parsed.file.clone(),
line: 1,
line_end: 1,
col_start: 0,
col_end: 0,
},
)
})
.collect();
let collector = crate::collector::DefinitionCollector::new_for_slice(
parsed.file.clone(),
parsed.source(),
&parse_result.source_map,
);
let (slice, collector_issues) = collector.collect_slice(&parse_result.program);
all_issues.extend(collector_issues);
FileDefinitions {
slice: Arc::new(slice),
issues: Arc::new(all_issues),
}
})
.collect();
let mut files_with_parse_errors: std::collections::HashSet<Arc<str>> =
std::collections::HashSet::new();
let mut files_needing_inference: std::collections::HashSet<Arc<str>> =
std::collections::HashSet::new();
{
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, _) = *guard;
for defs in file_defs {
for issue in defs.issues.iter() {
if matches!(issue.kind, mir_issues::IssueKind::ParseError { .. }) {
files_with_parse_errors.insert(issue.location.file.clone());
}
}
if stub_slice_needs_inference(&defs.slice) {
if let Some(file) = defs.slice.file.as_ref() {
files_needing_inference.insert(file.clone());
}
}
db.ingest_stub_slice(&defs.slice);
all_issues.extend(Arc::unwrap_or_clone(defs.issues));
}
}
if let Some(psr4) = &self.psr4 {
self.lazy_load_missing_classes(psr4.clone(), &mut all_issues);
}
if let Some(cache) = &self.cache {
let db_snapshot = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.clone()
};
let rev = build_reverse_deps(&db_snapshot);
cache.set_reverse_deps(rev);
}
let analyzed_file_set: std::collections::HashSet<std::sync::Arc<str>> =
file_data.iter().map(|(f, _)| f.clone()).collect();
{
let class_db = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.clone()
};
let class_issues =
crate::class::ClassAnalyzer::with_files(&class_db, analyzed_file_set, &file_data)
.analyze_all();
all_issues.extend(class_issues);
}
let db_priming = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.clone()
};
let filtered_parsed: Vec<_> = parsed_files
.par_iter()
.filter(|parsed| {
!files_with_parse_errors.contains(&parsed.file)
&& files_needing_inference.contains(&parsed.file)
})
.collect();
let (functions, methods) =
run_inference_sweep(db_priming, filtered_parsed, self.resolved_php_version());
{
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.commit_inferred_return_types(functions, methods);
}
let db_main = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.clone()
};
let pass2_results: Vec<(Vec<Issue>, Vec<crate::symbol::ResolvedSymbol>)> = parsed_files
.par_iter()
.filter(|parsed| !files_with_parse_errors.contains(&parsed.file))
.map_with(db_main, |db, parsed| {
let driver =
Pass2Driver::new(&*db as &dyn MirDatabase, self.resolved_php_version());
let result = if let Some(cache) = &self.cache {
let h = hash_content(parsed.source());
if let Some((cached_issues, ref_locs)) = cache.get(&parsed.file, &h) {
db.replay_reference_locations(parsed.file.clone(), &ref_locs);
(cached_issues, Vec::new())
} else {
let parse_result = parsed.parsed();
let (issues, symbols) = driver.analyze_bodies(
&parse_result.program,
parsed.file.clone(),
parsed.source(),
&parse_result.source_map,
);
let ref_locs = extract_reference_locations(&*db, &parsed.file);
cache.put(&parsed.file, h, issues.clone(), ref_locs);
(issues, symbols)
}
} else {
let parse_result = parsed.parsed();
driver.analyze_bodies(
&parse_result.program,
parsed.file.clone(),
parsed.source(),
&parse_result.source_map,
)
};
if let Some(cb) = &self.on_file_done {
cb();
}
result
})
.collect();
let mut all_symbols = Vec::new();
for (issues, symbols) in pass2_results {
all_issues.extend(issues);
all_symbols.extend(symbols);
}
if let Some(psr4) = &self.psr4 {
self.lazy_load_from_body_issues(
psr4.clone(),
&file_data,
&files_with_parse_errors,
&mut all_issues,
&mut all_symbols,
);
}
if let Some(cache) = &self.cache {
cache.flush();
}
if self.find_dead_code {
let salsa = self.salsa.lock().unwrap();
let dead_code_issues = crate::dead_code::DeadCodeAnalyzer::new(&salsa.0).analyze();
drop(salsa);
all_issues.extend(dead_code_issues);
}
AnalysisResult::build(all_issues, std::collections::HashMap::new(), all_symbols)
}
fn lazy_load_missing_classes(
&self,
psr4: Arc<crate::composer::Psr4Map>,
all_issues: &mut Vec<Issue>,
) {
use std::collections::HashSet;
let max_depth = 10;
let mut loaded: HashSet<String> = HashSet::new();
for _ in 0..max_depth {
let mut to_load: Vec<(String, PathBuf)> = Vec::new();
let mut try_queue = |fqcn: &str| {
if !self.type_exists(fqcn) && !loaded.contains(fqcn) {
if let Some(path) = psr4.resolve(fqcn) {
to_load.push((fqcn.to_string(), path));
}
}
};
let mut inheritance_candidates = Vec::new();
let import_candidates = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
let db = &guard.0;
for fqcn in db.active_class_node_fqcns() {
let Some(node) = db.lookup_class_node(&fqcn) else {
continue;
};
if node.is_interface(db) {
for parent in node.extends(db).iter() {
inheritance_candidates.push(parent.to_string());
}
} else if node.is_enum(db) {
for iface in node.interfaces(db).iter() {
inheritance_candidates.push(iface.to_string());
}
} else if node.is_trait(db) {
for used in node.traits(db).iter() {
inheritance_candidates.push(used.to_string());
}
} else {
if let Some(parent) = node.parent(db) {
inheritance_candidates.push(parent.to_string());
}
for iface in node.interfaces(db).iter() {
inheritance_candidates.push(iface.to_string());
}
}
}
db.file_import_snapshots()
.into_iter()
.flat_map(|(_, imports)| imports.into_values())
.collect::<Vec<_>>()
};
for fqcn in inheritance_candidates {
try_queue(&fqcn);
}
for fqcn in import_candidates {
try_queue(&fqcn);
}
if to_load.is_empty() {
break;
}
for (fqcn, path) in to_load {
loaded.insert(fqcn);
if let Ok(src) = std::fs::read_to_string(&path) {
let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
let defs = self.collect_and_ingest_source(file, &src);
all_issues.extend(Arc::unwrap_or_clone(defs.issues));
}
}
}
}
fn lazy_load_from_body_issues(
&self,
psr4: Arc<crate::composer::Psr4Map>,
file_data: &[(Arc<str>, Arc<str>)],
files_with_parse_errors: &HashSet<Arc<str>>,
all_issues: &mut Vec<Issue>,
all_symbols: &mut Vec<crate::symbol::ResolvedSymbol>,
) {
use mir_issues::IssueKind;
let max_depth = 5;
let mut loaded: HashSet<String> = HashSet::new();
for _ in 0..max_depth {
let mut to_load: HashMap<String, PathBuf> = HashMap::new();
for issue in all_issues.iter() {
if let IssueKind::UndefinedClass { name } = &issue.kind {
if !self.type_exists(name) && !loaded.contains(name) {
if let Some(path) = psr4.resolve(name) {
to_load.entry(name.clone()).or_insert(path);
}
}
}
}
if to_load.is_empty() {
break;
}
loaded.extend(to_load.keys().cloned());
for path in to_load.values() {
if let Ok(src) = std::fs::read_to_string(path) {
let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
let _ = self.collect_and_ingest_source(file, &src);
}
}
self.lazy_load_missing_classes(psr4.clone(), all_issues);
let files_to_reanalyze: HashSet<Arc<str>> = all_issues
.iter()
.filter_map(|i| {
if let IssueKind::UndefinedClass { name } = &i.kind {
if self.type_exists(name) {
return Some(i.location.file.clone());
}
}
None
})
.collect();
if files_to_reanalyze.is_empty() {
break;
}
all_issues.retain(|i| !files_to_reanalyze.contains(&i.location.file));
all_symbols.retain(|s| !files_to_reanalyze.contains(&s.file));
let db_reanalysis = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.clone()
};
let reanalysis: Vec<(Vec<Issue>, Vec<crate::symbol::ResolvedSymbol>)> = file_data
.par_iter()
.filter(|(f, _)| {
!files_with_parse_errors.contains(f) && files_to_reanalyze.contains(f)
})
.map_with(db_reanalysis, |db, (file, src)| {
let driver = Pass2Driver::new_inference_only(
&*db as &dyn MirDatabase,
self.resolved_php_version(),
);
let arena = bumpalo::Bump::new();
let parsed = php_rs_parser::parse(&arena, src);
driver.analyze_bodies(&parsed.program, file.clone(), src, &parsed.source_map);
let inferred = driver.take_inferred_types();
let mut guard_db = self.salsa.lock().expect("salsa lock poisoned");
guard_db
.0
.commit_inferred_return_types(inferred.functions, inferred.methods);
drop(guard_db);
let driver =
Pass2Driver::new(&*db as &dyn MirDatabase, self.resolved_php_version());
let arena = bumpalo::Bump::new();
let parsed = php_rs_parser::parse(&arena, src);
driver.analyze_bodies(&parsed.program, file.clone(), src, &parsed.source_map)
})
.collect();
for (issues, symbols) in reanalysis {
all_issues.extend(issues);
all_symbols.extend(symbols);
}
}
}
pub fn re_analyze_file(&self, file_path: &str, new_content: &str) -> AnalysisResult {
if let Some(cache) = &self.cache {
let h = hash_content(new_content);
if let Some((issues, ref_locs)) = cache.get(file_path, &h) {
let file: Arc<str> = Arc::from(file_path);
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.replay_reference_locations(file, &ref_locs);
return AnalysisResult::build(issues, HashMap::new(), Vec::new());
}
}
let file: Arc<str> = Arc::from(file_path);
{
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, _) = *guard;
db.remove_file_definitions(file_path);
}
let file_defs = {
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, ref mut files) = *guard;
let salsa_file = match files.get(&file) {
Some(&sf) => {
sf.set_text(db).to(Arc::from(new_content));
sf
}
None => {
let sf = SourceFile::new(db, file.clone(), Arc::from(new_content));
files.insert(file.clone(), sf);
sf
}
};
collect_file_definitions(db, salsa_file)
};
let mut all_issues: Vec<Issue> = Arc::unwrap_or_clone(file_defs.issues.clone());
let symbols = {
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, _) = *guard;
db.ingest_stub_slice(&file_defs.slice);
let arena = bumpalo::Bump::new();
let parsed = php_rs_parser::parse(&arena, new_content);
if parsed.errors.is_empty() {
let db_ref: &dyn MirDatabase = db;
let driver = Pass2Driver::new_inference_only(db_ref, self.resolved_php_version());
driver.analyze_bodies(
&parsed.program,
file.clone(),
new_content,
&parsed.source_map,
);
let inferred = driver.take_inferred_types();
db.commit_inferred_return_types(inferred.functions, inferred.methods);
let db_ref: &dyn MirDatabase = db;
let driver = Pass2Driver::new(db_ref, self.resolved_php_version());
let (body_issues, symbols) = driver.analyze_bodies(
&parsed.program,
file.clone(),
new_content,
&parsed.source_map,
);
all_issues.extend(body_issues);
symbols
} else {
Vec::new()
}
};
if let Some(cache) = &self.cache {
let h = hash_content(new_content);
cache.evict_with_dependents(&[file_path.to_string()]);
let guard = self.salsa.lock().expect("salsa lock poisoned");
let ref_locs = extract_reference_locations(&guard.0, &file);
cache.put(file_path, h, all_issues.clone(), ref_locs);
}
AnalysisResult::build(all_issues, HashMap::new(), symbols)
}
pub fn analyze_source(source: &str) -> AnalysisResult {
let analyzer = ProjectAnalyzer::new();
let file: Arc<str> = Arc::from("<source>");
let mut db = MirDb::default();
for slice in crate::stubs::builtin_stub_slices_for_version(analyzer.resolved_php_version())
{
db.ingest_stub_slice(&slice);
}
let salsa_file = SourceFile::new(&db, file.clone(), Arc::from(source));
let file_defs = collect_file_definitions(&db, salsa_file);
db.ingest_stub_slice(&file_defs.slice);
let mut all_issues = Arc::unwrap_or_clone(file_defs.issues);
if all_issues
.iter()
.any(|issue| matches!(issue.kind, mir_issues::IssueKind::ParseError { .. }))
{
return AnalysisResult::build(all_issues, std::collections::HashMap::new(), Vec::new());
}
let mut type_envs = std::collections::HashMap::new();
let mut all_symbols = Vec::new();
let arena = bumpalo::Bump::new();
let result = php_rs_parser::parse(&arena, source);
let driver = Pass2Driver::new_inference_only(&db, analyzer.resolved_php_version());
driver.analyze_bodies(&result.program, file.clone(), source, &result.source_map);
let inferred = driver.take_inferred_types();
db.commit_inferred_return_types(inferred.functions, inferred.methods);
let driver = Pass2Driver::new(&db, analyzer.resolved_php_version());
all_issues.extend(driver.analyze_bodies_typed(
&result.program,
file.clone(),
source,
&result.source_map,
&mut type_envs,
&mut all_symbols,
));
AnalysisResult::build(all_issues, type_envs, all_symbols)
}
pub fn discover_files(root: &Path) -> Vec<PathBuf> {
if root.is_file() {
return vec![root.to_path_buf()];
}
let mut files = Vec::new();
collect_php_files(root, &mut files);
files
}
pub fn collect_types_only(&self, paths: &[PathBuf]) {
let file_data: Vec<(Arc<str>, Arc<str>)> = paths
.par_iter()
.filter_map(|path| {
let src = std::fs::read_to_string(path).ok()?;
Some((
Arc::from(path.to_string_lossy().as_ref()),
Arc::<str>::from(src),
))
})
.collect();
let source_files: Vec<SourceFile> = {
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, ref mut files) = *guard;
file_data
.iter()
.map(|(file, src)| match files.get(file) {
Some(&sf) => {
if sf.text(db).as_ref() != src.as_ref() {
sf.set_text(db).to(src.clone());
}
sf
}
None => {
let sf = SourceFile::new(db, file.clone(), src.clone());
files.insert(file.clone(), sf);
sf
}
})
.collect()
};
let db_pass1 = {
let guard = self.salsa.lock().expect("salsa lock poisoned");
guard.0.clone()
};
let file_defs: Vec<FileDefinitions> = source_files
.par_iter()
.map_with(db_pass1, |db, salsa_file| {
collect_file_definitions_uncached(&*db, *salsa_file)
})
.collect();
let mut guard = self.salsa.lock().expect("salsa lock poisoned");
let (ref mut db, _) = *guard;
for defs in file_defs {
db.ingest_stub_slice(&defs.slice);
}
drop(guard);
crate::collector::print_collector_stats();
}
}
impl Default for ProjectAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[allow(clippy::type_complexity)]
fn run_inference_sweep(
db_priming: MirDb,
parsed_files: Vec<&ParsedProjectFile>,
php_version: PhpVersion,
) -> (Vec<(Arc<str>, Union)>, Vec<(Arc<str>, Arc<str>, Union)>) {
let functions = Arc::new(std::sync::Mutex::new(Vec::new()));
let methods = Arc::new(std::sync::Mutex::new(Vec::new()));
rayon::in_place_scope(|s| {
for parsed in parsed_files {
let db = db_priming.clone();
let functions = Arc::clone(&functions);
let methods = Arc::clone(&methods);
s.spawn(move |_| {
let driver = Pass2Driver::new_inference_only(&db as &dyn MirDatabase, php_version);
let parse_result = parsed.parsed();
driver.analyze_bodies(
&parse_result.program,
parsed.file.clone(),
parsed.source(),
&parse_result.source_map,
);
let inferred = driver.take_inferred_types();
if let Ok(mut funcs) = functions.lock() {
funcs.extend(inferred.functions);
}
if let Ok(mut meths) = methods.lock() {
meths.extend(inferred.methods);
}
});
}
});
let functions = Arc::try_unwrap(functions)
.map(|mutex| mutex.into_inner().unwrap_or_default())
.unwrap_or_else(|arc| arc.lock().unwrap().clone());
let methods = Arc::try_unwrap(methods)
.map(|mutex| mutex.into_inner().unwrap_or_default())
.unwrap_or_else(|arc| arc.lock().unwrap().clone());
(functions, methods)
}
fn stub_slice_needs_inference(slice: &mir_codebase::storage::StubSlice) -> bool {
slice
.functions
.iter()
.any(|func| func.return_type.is_none())
|| slice.classes.iter().any(|class| {
class
.own_methods
.values()
.any(|method| !method.is_abstract && method.return_type.is_none())
})
|| slice.traits.iter().any(|tr| {
tr.own_methods
.values()
.any(|method| !method.is_abstract && method.return_type.is_none())
})
|| slice.enums.iter().any(|en| {
en.own_methods
.values()
.any(|method| !method.is_abstract && method.return_type.is_none())
})
}
pub(crate) fn collect_php_files(dir: &Path, out: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if entry.file_type().map(|ft| ft.is_symlink()).unwrap_or(false) {
continue;
}
let path = entry.path();
if path.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(
name,
"vendor" | ".git" | "node_modules" | ".cache" | ".pnpm-store"
) {
continue;
}
collect_php_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("php") {
out.push(path);
}
}
}
}
fn build_reverse_deps(db: &dyn crate::db::MirDatabase) -> HashMap<String, HashSet<String>> {
let mut reverse: HashMap<String, HashSet<String>> = HashMap::new();
let mut add_edge = |symbol: &str, dependent_file: &str| {
if let Some(defining_file) = db.symbol_defining_file(symbol) {
let def = defining_file.as_ref().to_string();
if def != dependent_file {
reverse
.entry(def)
.or_default()
.insert(dependent_file.to_string());
}
}
};
for (file, imports) in db.file_import_snapshots() {
let file = file.as_ref().to_string();
for fqcn in imports.values() {
add_edge(fqcn, &file);
}
}
for fqcn in db.active_class_node_fqcns() {
let kind = match crate::db::class_kind_via_db(db, fqcn.as_ref()) {
Some(k) if !k.is_interface && !k.is_trait && !k.is_enum => k,
_ => continue,
};
let _ = kind;
let Some(file) = db
.symbol_defining_file(fqcn.as_ref())
.map(|f| f.as_ref().to_string())
else {
continue;
};
let Some(node) = db.lookup_class_node(fqcn.as_ref()) else {
continue;
};
if let Some(parent) = node.parent(db) {
add_edge(parent.as_ref(), &file);
}
for iface in node.interfaces(db).iter() {
add_edge(iface.as_ref(), &file);
}
for tr in node.traits(db).iter() {
add_edge(tr.as_ref(), &file);
}
}
reverse
}
fn extract_reference_locations(
db: &dyn crate::db::MirDatabase,
file: &Arc<str>,
) -> Vec<(String, u32, u16, u16)> {
db.extract_file_reference_locations(file.as_ref())
.into_iter()
.map(|(sym, line, col_start, col_end)| (sym.to_string(), line, col_start, col_end))
.collect()
}
pub struct AnalysisResult {
pub issues: Vec<Issue>,
pub type_envs: std::collections::HashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
pub symbols: Vec<crate::symbol::ResolvedSymbol>,
symbols_by_file: HashMap<Arc<str>, std::ops::Range<usize>>,
}
impl AnalysisResult {
fn build(
issues: Vec<Issue>,
type_envs: std::collections::HashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
mut symbols: Vec<crate::symbol::ResolvedSymbol>,
) -> Self {
symbols.sort_unstable_by(|a, b| a.file.as_ref().cmp(b.file.as_ref()));
let mut symbols_by_file: HashMap<Arc<str>, std::ops::Range<usize>> = HashMap::new();
let mut i = 0;
while i < symbols.len() {
let file = Arc::clone(&symbols[i].file);
let start = i;
while i < symbols.len() && symbols[i].file == file {
i += 1;
}
symbols_by_file.insert(file, start..i);
}
Self {
issues,
type_envs,
symbols,
symbols_by_file,
}
}
}
impl AnalysisResult {
pub fn error_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == mir_issues::Severity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == mir_issues::Severity::Warning)
.count()
}
pub fn issues_by_file(&self) -> HashMap<std::sync::Arc<str>, Vec<&Issue>> {
let mut map: HashMap<std::sync::Arc<str>, Vec<&Issue>> = HashMap::new();
for issue in &self.issues {
map.entry(issue.location.file.clone())
.or_default()
.push(issue);
}
map
}
pub fn symbol_at(
&self,
file: &str,
byte_offset: u32,
) -> Option<&crate::symbol::ResolvedSymbol> {
let range = self.symbols_by_file.get(file)?;
self.symbols[range.clone()]
.iter()
.filter(|s| s.span.start <= byte_offset && byte_offset < s.span.end)
.min_by_key(|s| s.span.end - s.span.start)
}
}