use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use crate::analyzer_db::AnalyzerDb;
use crate::cache::AnalysisCache;
use crate::composer::Psr4Map;
use crate::db::{MirDatabase, MirDbStorage, RefLoc};
use crate::php_version::PhpVersion;
#[derive(Clone)]
pub struct AnalysisSession {
pub(crate) db: Arc<AnalyzerDb>,
pub(crate) cache: Option<Arc<AnalysisCache>>,
pub(crate) psr4: Option<Arc<Psr4Map>>,
resolver: Option<Arc<dyn crate::ClassResolver>>,
pub(crate) php_version: PhpVersion,
pub(crate) user_stub_files: Vec<PathBuf>,
pub(crate) user_stub_dirs: Vec<PathBuf>,
stale_defined_symbols: Arc<RwLock<HashMap<String, HashSet<Arc<str>>>>>,
unresolvable_fqcns: UnresolvableCache,
source_provider: Arc<dyn crate::SourceProvider>,
}
type UnresolvableCache = Arc<RwLock<HashMap<Arc<str>, Option<Arc<str>>>>>;
const UNRESOLVABLE_CACHE_CAP: usize = 10_000;
impl AnalysisSession {
pub fn new(php_version: PhpVersion) -> Self {
let db = Arc::new(AnalyzerDb::new());
db.salsa
.write()
.set_php_version(Arc::from(php_version.to_string().as_str()));
Self {
db,
cache: None,
psr4: None,
resolver: None,
php_version,
user_stub_files: Vec::new(),
user_stub_dirs: Vec::new(),
stale_defined_symbols: Arc::new(RwLock::new(HashMap::default())),
unresolvable_fqcns: Arc::new(RwLock::new(HashMap::default())),
source_provider: Arc::new(crate::FsSourceProvider),
}
}
pub fn with_source_provider(mut self, provider: Arc<dyn crate::SourceProvider>) -> Self {
self.source_provider = provider;
self
}
pub fn with_cache(mut self, cache: Arc<AnalysisCache>) -> Self {
debug_assert_eq!(
self.db.source_file_count(),
0,
"AnalysisSession::with_cache must be called before any file is ingested"
);
let dir = cache.cache_dir().to_path_buf();
self.db = Arc::new(AnalyzerDb::new().with_cache_dir(&dir));
self.db
.salsa
.write()
.set_php_version(Arc::from(self.php_version.to_string().as_str()));
self.cache = Some(cache);
self
}
pub fn with_cache_dir(mut self, cache_dir: &std::path::Path) -> Self {
debug_assert_eq!(
self.db.source_file_count(),
0,
"AnalysisSession::with_cache_dir must be called before any file is ingested"
);
self.db = Arc::new(AnalyzerDb::new().with_cache_dir(cache_dir));
self.db
.salsa
.write()
.set_php_version(Arc::from(self.php_version.to_string().as_str()));
let user_stub_fp =
crate::stubs::user_stub_fingerprint(&self.user_stub_files, &self.user_stub_dirs);
self.cache = Some(Arc::new(AnalysisCache::open(
cache_dir,
self.php_version.cache_byte(),
user_stub_fp,
)));
self
}
pub fn with_psr4(mut self, map: Arc<Psr4Map>) -> Self {
let user_resolver: Arc<dyn crate::ClassResolver> = map.clone();
let resolver: Arc<dyn crate::ClassResolver> = Arc::new(crate::ChainedClassResolver::new(
user_resolver,
Arc::new(crate::StubClassResolver),
));
self.psr4 = Some(map);
self.resolver = Some(resolver.clone());
self.db.salsa.write().set_resolver(Some(resolver));
self
}
pub fn with_class_resolver(mut self, resolver: Arc<dyn crate::ClassResolver>) -> Self {
let wrapped: Arc<dyn crate::ClassResolver> = Arc::new(crate::ChainedClassResolver::new(
resolver,
Arc::new(crate::StubClassResolver),
));
self.db.salsa.write().set_resolver(Some(wrapped.clone()));
self.resolver = Some(wrapped);
self
}
pub fn with_user_stubs(mut self, files: Vec<PathBuf>, dirs: Vec<PathBuf>) -> Self {
self.user_stub_files = files;
self.user_stub_dirs = dirs;
self
}
pub fn php_version(&self) -> PhpVersion {
self.php_version
}
pub fn cache(&self) -> Option<&AnalysisCache> {
self.cache.as_deref()
}
pub fn psr4(&self) -> Option<&Psr4Map> {
self.psr4.as_deref()
}
}
mod incremental;
mod ingest;
mod loading;
mod queries;
mod stubs;
fn file_outgoing_dependencies(db: &dyn MirDatabase, file: &str) -> HashSet<String> {
let mut targets: HashSet<String> = HashSet::default();
if let Some(sf) = db.lookup_source_file(file) {
for target in crate::db::file_structural_deps(db, sf).iter() {
targets.insert(target.as_ref().to_string());
}
}
for symbol_key in db.file_referenced_symbols(file) {
let lookup: &str = match symbol_key.split_once("::") {
Some((class, _)) => class,
None => &symbol_key,
};
if let Some(defining_file) = db.symbol_defining_file(lookup) {
if defining_file.as_ref() != file {
targets.insert(defining_file.as_ref().to_string());
}
}
}
targets
}
fn collect_class_refs_from_ast(program: &php_ast::owned::Program) -> Vec<String> {
use php_ast::ast::BinaryOp;
use php_ast::owned::visitor::{
walk_owned_catch_clause, walk_owned_expr, walk_owned_program, walk_owned_type_hint,
OwnedVisitor,
};
use php_ast::owned::{ExprKind, TypeHintKind};
use std::ops::ControlFlow;
fn owned_name_str(name: &php_ast::owned::Name) -> String {
let joined: String = name
.parts
.iter()
.map(|p| p.as_ref())
.collect::<Vec<&str>>()
.join("\\");
if name.kind == php_ast::ast::NameKind::FullyQualified {
format!("\\{joined}")
} else {
joined
}
}
struct V {
names: std::collections::HashSet<String>,
}
impl OwnedVisitor for V {
fn visit_expr(&mut self, expr: &php_ast::owned::Expr) -> ControlFlow<()> {
match &expr.kind {
ExprKind::New(n) => {
if let ExprKind::Identifier(name) = &n.class.kind {
self.names.insert(name.as_ref().to_string());
}
}
ExprKind::StaticMethodCall(c) => {
if let ExprKind::Identifier(name) = &c.class.kind {
self.names.insert(name.as_ref().to_string());
}
}
ExprKind::StaticPropertyAccess(a) => {
if let ExprKind::Identifier(name) = &a.class.kind {
self.names.insert(name.as_ref().to_string());
}
}
ExprKind::ClassConstAccess(a) => {
if let ExprKind::Identifier(name) = &a.class.kind {
self.names.insert(name.as_ref().to_string());
}
}
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
if let ExprKind::Identifier(name) = &b.right.kind {
self.names.insert(name.as_ref().to_string());
}
}
_ => {}
}
walk_owned_expr(self, expr)
}
fn visit_type_hint(&mut self, hint: &php_ast::owned::TypeHint) -> ControlFlow<()> {
if let TypeHintKind::Named(name) = &hint.kind {
let s = owned_name_str(name);
if !s.is_empty() {
self.names.insert(s);
}
}
walk_owned_type_hint(self, hint)
}
fn visit_catch_clause(&mut self, catch: &php_ast::owned::CatchClause) -> ControlFlow<()> {
for ty in catch.types.iter() {
self.names.insert(owned_name_str(ty));
}
walk_owned_catch_clause(self, catch)
}
}
let mut v = V {
names: std::collections::HashSet::default(),
};
let _ = walk_owned_program(&mut v, program);
v.names.into_iter().collect()
}