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>>>>>,
last_ingested_symbols: Arc<RwLock<HashMap<String, HashSet<Arc<str>>>>>,
unresolvable_fqcns: UnresolvableCache,
source_provider: Arc<dyn crate::SourceProvider>,
pub(crate) pending_eager_function_files: Arc<parking_lot::Mutex<Option<Vec<PathBuf>>>>,
prepared_files: PreparedFilesCache,
prepare_generation: Arc<std::sync::atomic::AtomicU64>,
ref_committed: CommittedRefs,
defs_committed: CommittedTexts,
}
type UnresolvableCache = Arc<RwLock<HashMap<Arc<str>, Option<Arc<str>>>>>;
type PreparedFilesCache = Arc<RwLock<HashMap<Arc<str>, (Arc<str>, u64)>>>;
type CommittedTexts = Arc<RwLock<HashMap<Arc<str>, Arc<str>>>>;
type CommittedRefs =
Arc<RwLock<HashMap<Arc<str>, (Arc<str>, std::sync::Weak<crate::db::AnalyzeOutput>)>>>;
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())),
last_ingested_symbols: Arc::new(RwLock::new(HashMap::default())),
unresolvable_fqcns: Arc::new(RwLock::new(HashMap::default())),
source_provider: Arc::new(crate::FsSourceProvider),
pending_eager_function_files: Arc::new(parking_lot::Mutex::new(Some(Vec::new()))),
prepared_files: Arc::new(RwLock::new(HashMap::default())),
prepare_generation: Arc::new(std::sync::atomic::AtomicU64::new(0)),
ref_committed: Arc::new(RwLock::new(HashMap::default())),
defs_committed: Arc::new(RwLock::new(HashMap::default())),
}
}
pub fn ref_index_lock_count(&self) -> u64 {
self.db.salsa.read().ref_index_lock_count()
}
pub(crate) fn is_ref_committed(&self, file: &str, current_text: &Arc<str>) -> bool {
self.ref_committed
.read()
.get(file)
.is_some_and(|(t, _)| Arc::ptr_eq(t, current_text))
}
pub(crate) fn ref_commit_is_current(
&self,
file: &str,
current_text: &Arc<str>,
out: &Arc<crate::db::AnalyzeOutput>,
) -> bool {
self.ref_committed.read().get(file).is_some_and(|(t, w)| {
Arc::ptr_eq(t, current_text) && w.upgrade().is_some_and(|prev| Arc::ptr_eq(&prev, out))
})
}
pub(crate) fn mark_ref_committed(
&self,
file: &Arc<str>,
text: &Arc<str>,
out: Option<&Arc<crate::db::AnalyzeOutput>>,
) {
let weak = out.map(Arc::downgrade).unwrap_or_default();
self.ref_committed
.write()
.insert(file.clone(), (text.clone(), weak));
}
pub(crate) fn forget_ref_committed(&self, file: &str) {
self.ref_committed.write().remove(file);
}
pub(crate) fn is_defs_committed(&self, file: &str, current_text: &Arc<str>) -> bool {
self.defs_committed
.read()
.get(file)
.is_some_and(|t| Arc::ptr_eq(t, current_text))
}
pub(crate) fn mark_defs_committed(&self, file: &Arc<str>, text: &Arc<str>) {
self.defs_committed
.write()
.insert(file.clone(), text.clone());
}
pub(crate) fn forget_defs_committed(&self, file: &str) {
self.defs_committed.write().remove(file);
}
pub(crate) fn defs_committed_keys(&self) -> Vec<Arc<str>> {
self.defs_committed.read().keys().cloned().collect()
}
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.clone());
self.resolver = Some(resolver.clone());
self.db.salsa.write().set_resolver(Some(resolver));
*self.pending_eager_function_files.lock() = Some(map.vendor_eager_files());
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;
pub use queries::SubtypeClassSite;
fn file_outgoing_dependencies(
db: &dyn MirDatabase,
file: &str,
include_body_ref_edges: bool,
) -> 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());
}
}
if !include_body_ref_edges {
return targets;
}
for symbol_key in db.file_referenced_symbols(file) {
let lookup = crate::defining_file_lookup_key(&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_class_member, walk_owned_expr, walk_owned_program, walk_owned_stmt, OwnedVisitor,
};
use php_ast::owned::{ClassMemberKind, ExprKind};
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
}
}
fn collect_from_type(ty: &mir_types::Type, out: &mut std::collections::HashSet<String>) {
for atomic in ty.types.iter() {
if let mir_types::Atomic::TNamedObject { fqcn, type_params } = atomic {
out.insert(fqcn.as_ref().to_string());
for tp in type_params.iter() {
collect_from_type(tp, out);
}
}
}
}
fn collect_from_docblock(text: &str, out: &mut std::collections::HashSet<String>) {
let parsed = crate::parser::DocblockParser::parse(text);
for (_, ty) in &parsed.params {
collect_from_type(ty, out);
}
if let Some(ret) = &parsed.return_type {
collect_from_type(ret, out);
}
if let Some(var) = &parsed.var_type {
collect_from_type(var, out);
}
for ext in &parsed.extends {
collect_from_type(ext, out);
}
for impl_ty in &parsed.implements {
collect_from_type(impl_ty, out);
}
}
struct V {
names: std::collections::HashSet<String>,
}
impl OwnedVisitor for V {
fn visit_stmt(&mut self, stmt: &php_ast::owned::Stmt) -> ControlFlow<()> {
if let Some(doc) = stmt.leading_doc_comment() {
collect_from_docblock(&doc.text, &mut self.names);
}
walk_owned_stmt(self, stmt)
}
fn visit_class_member(&mut self, member: &php_ast::owned::ClassMember) -> ControlFlow<()> {
match &member.kind {
ClassMemberKind::Method(m) => {
if let Some(doc) = &m.doc_comment {
collect_from_docblock(&doc.text, &mut self.names);
}
}
ClassMemberKind::Property(p) => {
if let Some(doc) = &p.doc_comment {
collect_from_docblock(&doc.text, &mut self.names);
}
}
_ => {}
}
walk_owned_class_member(self, member)
}
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_name(&mut self, name: &php_ast::owned::Name) -> ControlFlow<()> {
let s = owned_name_str(name);
if !s.is_empty() {
self.names.insert(s);
}
ControlFlow::Continue(())
}
}
let mut v = V {
names: std::collections::HashSet::default(),
};
let _ = walk_owned_program(&mut v, program);
v.names.into_iter().collect()
}