use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use crate::db::MirDatabase;
use parking_lot::{Mutex, RwLock};
use rayon::prelude::*;
use crate::db::MirDbStorage;
use crate::php_version::PhpVersion;
pub(crate) struct MirDbRw(MirDbStorage);
unsafe impl Sync for MirDbRw {}
impl std::ops::Deref for MirDbRw {
type Target = MirDbStorage;
fn deref(&self) -> &MirDbStorage {
&self.0
}
}
impl std::ops::DerefMut for MirDbRw {
fn deref_mut(&mut self) -> &mut MirDbStorage {
&mut self.0
}
}
pub struct AnalyzerDb {
pub(crate) salsa: RwLock<MirDbRw>,
pub(crate) loaded_stubs: Mutex<HashSet<&'static str>>,
pub(crate) user_stubs_loaded: std::sync::atomic::AtomicBool,
pub(crate) stub_cache: Option<Arc<crate::stub_cache::StubSliceCache>>,
}
impl AnalyzerDb {
pub fn new() -> Self {
let mut db = MirDbStorage::default();
db.init_workspace_revision();
Self {
salsa: RwLock::new(MirDbRw(db)),
loaded_stubs: Mutex::new(HashSet::new()),
user_stubs_loaded: std::sync::atomic::AtomicBool::new(false),
stub_cache: None,
}
}
pub fn with_cache_dir(mut self, cache_dir: &std::path::Path) -> Self {
let cache = Arc::new(crate::stub_cache::StubSliceCache::open(cache_dir));
self.salsa.write().set_stub_cache(cache.clone());
self.stub_cache = Some(cache);
self
}
pub fn source_file_count(&self) -> usize {
self.salsa.read().source_file_count()
}
pub fn snapshot_db(&self) -> MirDbStorage {
let guard = self.salsa.read();
(**guard).clone()
}
pub fn ingest_stub_paths(&self, paths: &[&'static str], php_version: PhpVersion) {
let needed: Vec<&'static str> = {
let loaded = self.loaded_stubs.lock();
paths
.iter()
.copied()
.filter(|p| !loaded.contains(p))
.collect()
};
if needed.is_empty() {
return;
}
let slices: Vec<(&'static str, mir_codebase::storage::StubSlice)> = needed
.par_iter()
.filter_map(|&path| {
crate::stubs::stub_content_for_path(path).map(|content| {
let slice =
crate::stubs::stub_slice_from_source(path, content, Some(php_version));
(path, slice)
})
})
.collect();
let mut guard = self.salsa.write();
let mut loaded = self.loaded_stubs.lock();
for (path, _slice) in &slices {
if loaded.insert(*path) {
if let Some(content) = crate::stubs::stub_content_for_path(path) {
guard.upsert_source_file_with_durability(
Arc::from(*path),
Arc::from(content),
salsa::Durability::HIGH,
);
}
}
}
}
pub fn ingest_user_stubs(&self, files: &[PathBuf], dirs: &[PathBuf]) {
if files.is_empty() && dirs.is_empty() {
return;
}
let was_loaded = self
.user_stubs_loaded
.load(std::sync::atomic::Ordering::Relaxed);
if was_loaded {
return;
}
let mut all_paths: Vec<PathBuf> = files.to_vec();
for dir in dirs {
crate::stubs::collect_stub_dir_paths(dir, &mut all_paths);
}
let path_sources: Vec<(PathBuf, String)> = all_paths
.into_iter()
.filter_map(|p| std::fs::read_to_string(&p).ok().map(|s| (p, s)))
.collect();
let mut guard = self.salsa.write();
for (path, source) in &path_sources {
let path_arc: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
guard.upsert_source_file(path_arc.clone(), Arc::from(source.as_str()));
guard.register_user_stub_path(path_arc);
}
self.user_stubs_loaded
.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn collect_and_ingest_file(
&self,
file: Arc<str>,
source: &str,
php_version: PhpVersion,
) -> crate::db::FileDefinitions {
use mir_issues::Issue;
let php_v = php_version.cache_byte();
let content_hash = crate::stub_cache::hash_source(source);
let durability = if file.contains("/vendor/") || file.contains("\\vendor\\") {
salsa::Durability::HIGH
} else {
salsa::Durability::LOW
};
{
let guard = self.salsa.read();
let cached = guard
.parse_cache()
.get(&content_hash)
.map(|r| Arc::clone(&*r));
drop(guard);
if let Some(cached) = cached {
crate::metrics::record_stub_cache_hit();
let slice_arc = if cached.file.as_deref() == Some(&*file) {
cached
} else {
let mut owned = (*cached).clone();
owned.file = Some(file.clone());
Arc::new(owned)
};
let file_defs = crate::db::FileDefinitions {
slice: slice_arc,
issues: Arc::new(Vec::new()),
};
let mut write_guard = self.salsa.write();
write_guard.upsert_source_file_with_durability(
file.clone(),
Arc::from(source),
durability,
);
return file_defs;
}
}
let cache_hit = self.stub_cache.as_ref().and_then(|cache| {
let mut slice = cache.get(&file, &content_hash, php_v)?;
crate::stub_cache::prepare_for_ingest(&mut slice);
Some(slice)
});
if let Some(slice) = cache_hit {
crate::metrics::record_stub_cache_hit();
let slice_arc = Arc::new(slice);
self.salsa
.read()
.prime_parse_cache(content_hash, slice_arc.clone());
let file_defs = crate::db::FileDefinitions {
slice: slice_arc,
issues: Arc::new(Vec::new()),
};
let mut guard = self.salsa.write();
guard.upsert_source_file_with_durability(file.clone(), Arc::from(source), durability);
return file_defs;
}
crate::metrics::record_stub_cache_miss();
let parsed = php_rs_parser::parse(source);
let has_hard_parse_errors = parsed.errors.iter().any(crate::parser::is_hard_parse_error);
let mut all_issues: Vec<Issue> = parsed
.errors
.iter()
.map(|err| crate::parser::parse_error_to_issue(err, &file, source, &parsed.source_map))
.collect();
let collector = crate::collector::DefinitionCollector::new_for_slice(
file.clone(),
source,
&parsed.source_map,
);
let (mut slice, collector_issues) = collector.collect_slice(&parsed.program);
all_issues.extend(collector_issues);
mir_codebase::storage::deduplicate_params_in_slice(&mut slice);
let slice_arc = Arc::new(slice);
if !has_hard_parse_errors {
self.salsa
.read()
.prime_parse_cache(content_hash, Arc::clone(&slice_arc));
if let Some(cache) = &self.stub_cache {
cache.put(&file, &content_hash, php_v, &slice_arc);
}
}
let file_defs = crate::db::FileDefinitions {
slice: slice_arc,
issues: Arc::new(all_issues),
};
{
let mut guard = self.salsa.write();
guard.upsert_source_file_with_durability(file.clone(), Arc::from(source), durability);
}
file_defs
}
}
impl Default for AnalyzerDb {
fn default() -> Self {
Self::new()
}
}