use super::*;
impl AnalysisSession {
pub(super) fn lazy_load_missing_classes(
&mut self,
psr4: Arc<crate::composer::Psr4Map>,
php_version: PhpVersion,
all_issues: &mut Vec<Issue>,
) {
let max_depth = 10;
let mut loaded: HashSet<String> = HashSet::default();
let mut scanned: HashSet<Arc<str>> = HashSet::default();
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 candidates: Vec<String> = Vec::new();
let import_candidates = {
let db_owned = self.snapshot_db();
let db = &db_owned;
for fqcn in crate::db::workspace_classes(db).iter() {
if scanned.contains(fqcn.as_str()) {
continue;
}
let here = crate::db::Fqcn::from_str(db, fqcn.as_str());
let Some(class) = crate::db::find_class_like(db, here) else {
continue;
};
scanned.insert(Arc::from(fqcn.as_str()));
collect_class_referenced_fqcns(&class, &mut candidates);
}
db.file_import_snapshots()
.into_iter()
.flat_map(|(_, imports)| {
imports
.values()
.map(|sym| sym.as_str().to_string())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
};
let provider = {
use crate::db::MirDatabase as _;
self.snapshot_db().source_provider()
};
if let Some(provider) = provider.as_ref() {
let mut seen: HashSet<&str> = HashSet::default();
let db = self.snapshot_db();
let paths: Vec<String> = candidates
.iter()
.chain(import_candidates.iter())
.filter(|fqcn| seen.insert(fqcn.as_str()))
.filter(|fqcn| {
!crate::db::class_like_indexed(&db, crate::db::Fqcn::from_str(&db, fqcn))
})
.filter_map(|fqcn| psr4.resolve(fqcn))
.map(|path| path.to_string_lossy().into_owned())
.filter(|path| self.db.lookup_source_file(path).is_none())
.collect();
provider.prefetch(&paths);
}
for fqcn in candidates {
try_queue(&fqcn);
}
for fqcn in import_candidates {
try_queue(&fqcn);
}
if let Some(provider) = provider.as_ref() {
provider.clear_prefetch();
}
if to_load.is_empty() {
break;
}
for (fqcn, _) in &to_load {
loaded.insert(fqcn.clone());
}
let db_template = self.snapshot_db();
let stub_cache = self.db.stub_cache.clone();
let prepared: Vec<Option<(bool, crate::analyzer_db::PreparedIngest)>> = to_load
.par_iter()
.map_with(db_template, |db, (_, path)| {
let Ok(src) = std::fs::read_to_string(path) else {
return None;
};
let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
let is_vendor = crate::db::is_vendor_path(&file);
let prepared = crate::analyzer_db::AnalyzerDb::prepare_ingest(
db,
stub_cache.as_ref(),
file,
&src,
php_version,
);
Some((is_vendor, prepared))
})
.collect();
for entry in prepared {
let Some((is_vendor, prepared)) = entry else {
continue;
};
let collected = self.db.commit_ingest(prepared);
if !is_vendor {
all_issues.append(&mut Arc::unwrap_or_clone(collected.file_defs.issues));
}
}
self.refresh_workspace_index();
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn lazy_load_from_body_issues(
&mut self,
psr4: Arc<crate::composer::Psr4Map>,
php_version: PhpVersion,
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>,
skip_symbols: bool,
) {
use mir_issues::IssueKind;
let max_depth = 5;
let mut loaded: HashSet<String> = HashSet::default();
for _ in 0..max_depth {
let mut to_load: HashMap<String, PathBuf> = HashMap::default();
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, php_version);
}
}
self.refresh_workspace_index();
self.lazy_load_missing_classes(psr4.clone(), php_version, 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 mut db_full = self.db.snapshot_db();
db_full.freeze_workspace_index();
let reanalysis: Vec<(Vec<Issue>, Vec<crate::symbol::ResolvedSymbol>, Vec<RefLoc>)> =
file_data
.par_iter()
.filter(|(f, _)| {
!files_with_parse_errors.contains(f) && files_to_reanalyze.contains(f)
})
.map_with(db_full, |db, (file, src)| {
let driver = BodyAnalyzer::new(&*db as &dyn MirDatabase, php_version);
let parsed =
crate::recursion::parse_versioned(src, php_version.parser_version());
let (issues, symbols) = driver.analyze_bodies(
&parsed.program,
file.clone(),
src,
&parsed.source_map,
);
let pending = db.take_pending_ref_locs();
(issues, symbols, pending)
})
.collect();
let mut reanalysis_ref_locs: Vec<RefLoc> = Vec::new();
for (issues, symbols, ref_locs) in reanalysis {
all_issues.extend(issues);
if !skip_symbols {
all_symbols.extend(symbols);
}
reanalysis_ref_locs.extend(ref_locs);
}
self.db
.salsa
.commit_reference_locations_batch(reanalysis_ref_locs);
}
}
}