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,
ref_query_cache: RefQueryCache,
ref_query_cache_locations: Arc<std::sync::atomic::AtomicUsize>,
ref_query_cache_hits: Arc<std::sync::atomic::AtomicU64>,
subtype_query_cache: SubtypeQueryCache,
subtype_query_cache_sites: Arc<std::sync::atomic::AtomicUsize>,
subtype_query_cache_hits: Arc<std::sync::atomic::AtomicU64>,
}
#[derive(PartialEq, Eq, Hash)]
struct RefQueryCacheKey {
symbol: String,
include_declaration: bool,
generation: (salsa::Revision, u64),
files_hash: u64,
}
const REF_QUERY_CACHE_LOCATION_CAP: usize = 200_000;
type RefQueryCache =
Arc<RwLock<RevisionedMap<RefQueryCacheKey, Arc<Vec<(Arc<str>, crate::Range)>>>>>;
struct RevisionedMap<K, V> {
generation: Option<(salsa::Revision, u64)>,
map: HashMap<K, V>,
}
impl<K, V> Default for RevisionedMap<K, V> {
fn default() -> Self {
Self {
generation: None,
map: HashMap::default(),
}
}
}
impl<K: std::hash::Hash + Eq, V> RevisionedMap<K, V> {
fn get(&self, key: &K) -> Option<&V> {
self.map.get(key)
}
fn advance_to(&mut self, generation: (salsa::Revision, u64), on_clear: impl FnOnce()) -> bool {
match self.generation {
Some(g) if g == generation => true,
Some(g) if g > generation => false,
_ => {
self.map.clear();
self.generation = Some(generation);
on_clear();
true
}
}
}
}
#[derive(PartialEq, Eq, Hash)]
struct SubtypeQueryCacheKey {
class_fqn: String,
include_trait_users: bool,
generation: (salsa::Revision, u64),
files_hash: u64,
}
const SUBTYPE_QUERY_CACHE_SITE_CAP: usize = 50_000;
type SubtypeQueryCache =
Arc<RwLock<RevisionedMap<SubtypeQueryCacheKey, Arc<Vec<SubtypeClassSite>>>>>;
fn hash_files(files: &[Arc<str>]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = rustc_hash::FxHasher::default();
files.len().hash(&mut hasher);
for f in files {
f.hash(&mut hasher);
}
hasher.finish()
}
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>>>>;
pub(crate) struct RefCachePut {
content_hash: String,
surface_hash: String,
ref_locs: Arc<[crate::cache::CachedRefLoc]>,
}
pub(crate) struct RefCommit {
text: Arc<str>,
out: std::sync::Weak<crate::db::AnalyzeOutput>,
generation: u64,
resolved: bool,
live_analyzed: bool,
}
type CommittedRefs = Arc<RwLock<HashMap<Arc<str>, RefCommit>>>;
const UNRESOLVABLE_CACHE_CAP: usize = 10_000;
pub(crate) struct DeferredRevisionBumps<'s> {
session: &'s AnalysisSession,
depth: Arc<std::sync::atomic::AtomicUsize>,
pending: Arc<std::sync::atomic::AtomicBool>,
}
impl Drop for DeferredRevisionBumps<'_> {
fn drop(&mut self) {
use std::sync::atomic::Ordering;
if self.depth.fetch_sub(1, Ordering::SeqCst) == 1
&& self.pending.swap(false, Ordering::SeqCst)
{
self.session.db.salsa.write().bump_workspace_revision();
}
}
}
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())),
ref_query_cache: Arc::new(RwLock::new(RevisionedMap::default())),
ref_query_cache_locations: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
ref_query_cache_hits: Arc::new(std::sync::atomic::AtomicU64::new(0)),
subtype_query_cache: Arc::new(RwLock::new(RevisionedMap::default())),
subtype_query_cache_sites: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
subtype_query_cache_hits: Arc::new(std::sync::atomic::AtomicU64::new(0)),
}
}
pub fn ref_query_cache_hits(&self) -> u64 {
self.ref_query_cache_hits
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn ref_query_cache_locations(&self) -> usize {
self.ref_query_cache_locations
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn subtype_query_cache_hits(&self) -> u64 {
self.subtype_query_cache_hits
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn subtype_query_cache_sites(&self) -> usize {
self.subtype_query_cache_sites
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn ref_index_lock_count(&self) -> u64 {
self.db.salsa.read().ref_index_lock_count()
}
pub(crate) fn defer_revision_bumps(&self) -> DeferredRevisionBumps<'_> {
let (depth, pending) = self.db.salsa.read().revision_bump_deferral_handles();
depth.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
DeferredRevisionBumps {
session: self,
depth,
pending,
}
}
pub fn class_mention_stats(&self) -> crate::db::ClassMentionStats {
self.db.salsa.read().class_mention_stats()
}
pub fn files_mentioning_class(&self, files: &[Arc<str>], class_name: &str) -> Vec<Arc<str>> {
self.files_mentioning_any(files, &[class_name])
}
pub fn files_mentioning_any(&self, files: &[Arc<str>], needles: &[&str]) -> Vec<Arc<str>> {
use rayon::prelude::*;
if needles.is_empty() {
return files.to_vec();
}
let db = self.snapshot_db();
db.add_literal_mention_names(needles.iter().copied());
let queries: Vec<_> = needles
.iter()
.filter_map(|n| db.prepare_class_mention_query(n))
.collect();
if queries.is_empty() {
return files.to_vec();
}
let Some(scanner) = db.class_mention_scanner() else {
return files.to_vec();
};
files
.par_iter()
.map_with(db, |db, f| -> Option<Arc<str>> {
let sf = db.lookup_source_file(f.as_ref())?;
let text = sf.text(&*db as &dyn MirDatabase).clone();
for q in &queries {
match db.class_mention_answer(f.as_ref(), q, &text) {
Some(true) => return Some(f.clone()),
Some(false) => continue,
None => {
let names = scanner.scan(text.as_ref());
let hit = queries
.iter()
.any(|q2| names.binary_search(&q2.name).is_ok());
db.set_file_class_mentions(f, &text, scanner.epoch(), names);
return hit.then(|| f.clone());
}
}
}
None
})
.flatten()
.collect()
}
pub(crate) fn is_ref_committed(
&self,
file: &str,
current_text: &Arc<str>,
current_gen: u64,
) -> bool {
self.ref_committed.read().get(file).is_some_and(|c| {
Arc::ptr_eq(&c.text, current_text) && (c.resolved || c.generation == current_gen)
})
}
pub(crate) fn ref_commit_stale_by_generation_only(
&self,
file: &str,
current_text: &Arc<str>,
) -> bool {
self.ref_committed
.read()
.get(file)
.is_some_and(|c| c.live_analyzed && Arc::ptr_eq(&c.text, 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(|c| {
Arc::ptr_eq(&c.text, current_text)
&& c.out.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>>,
generation: u64,
resolved: bool,
) {
let commit = RefCommit {
text: text.clone(),
out: out.map(Arc::downgrade).unwrap_or_default(),
generation,
resolved,
live_analyzed: out.is_some(),
};
self.ref_committed.write().insert(file.clone(), commit);
}
pub(crate) fn forget_ref_committed(&self, file: &str) {
self.ref_committed.write().remove(file);
}
pub(crate) fn stage_ref_cache_put(
&self,
db: &dyn crate::db::MirDatabase,
sf: crate::db::SourceFile,
file: &str,
text: &Arc<str>,
out: &Arc<crate::db::AnalyzeOutput>,
) -> Option<RefCachePut> {
let cache = self.cache.as_deref()?;
let content_hash = crate::cache::hash_content(text);
if cache.is_valid(file, &content_hash) {
return None;
}
let parsed = crate::db::parse_file(db, sf);
let surface_hash = crate::cache::surface_fingerprint(text, &parsed.0.program);
let ref_locs: Arc<[crate::cache::CachedRefLoc]> = out
.ref_locs
.iter()
.map(|r| (Arc::clone(&r.symbol_key), r.line, r.col_start, r.col_end))
.collect();
Some(RefCachePut {
content_hash,
surface_hash,
ref_locs,
})
}
pub(crate) fn apply_ref_cache_put(
&self,
file: &str,
out: &Arc<crate::db::AnalyzeOutput>,
put: RefCachePut,
) {
if let Some(cache) = self.cache.as_deref() {
cache.put(
file,
put.content_hash,
put.surface_hash,
out.issues.clone(),
put.ref_locs,
);
}
}
pub fn flush_analysis_cache(&self) {
if let Some(cache) = &self.cache {
cache.flush();
}
}
pub fn workspace_symbol_index_ready(&self) -> bool {
self.db
.salsa
.read()
.workspace_symbol_index_singleton()
.is_some()
}
pub fn workspace_index_walks(&self) -> u64 {
self.db.salsa.read().workspace_index_walks()
}
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(crate) fn ref_committed_keys(&self) -> Vec<Arc<str>> {
self.ref_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()
}