use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use crate::analyzer_db::AnalyzerDb;
use crate::cache::AnalysisCache;
use crate::composer::Psr4Map;
use crate::db::{MirDatabase, MirDbStorage, RefLoc};
use crate::php_version::PhpVersion;
pub struct AnalysisSession {
pub(crate) db: AnalyzerDb,
pub(crate) cache: Option<Arc<AnalysisCache>>,
pub(crate) psr4: Option<Arc<Psr4Map>>,
resolver: Option<Arc<dyn crate::ClassResolver>>,
pub(crate) php_version: PhpVersion,
memoize_method_call_results: bool,
forbidden_functions: Vec<String>,
pub(crate) user_stub_files: Vec<PathBuf>,
pub(crate) user_stub_dirs: Vec<PathBuf>,
stale_defined_symbols: HashMap<String, HashSet<Arc<str>>>,
last_ingested_symbols: HashMap<String, HashSet<Arc<str>>>,
last_structural_targets: HashMap<String, HashSet<String>>,
unresolvable_fqcns: HashMap<Arc<str>, Option<Arc<str>>>,
pub(crate) pending_eager_function_files: Option<Vec<PathBuf>>,
prepared_files: PreparedFilesCache,
pub(crate) suppression_maps: SuppressionMapCache,
prepare_generation: u64,
pub(crate) index: Arc<IndexState>,
}
const _: () = {
const fn assert_send<T: Send>() {}
assert_send::<AnalysisSession>();
};
const _: fn() = || {
trait AmbiguousIfSync<A> {
fn some_item() {}
}
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
struct IsSync;
impl<T: ?Sized + Sync> AmbiguousIfSync<IsSync> for T {}
let _ = <AnalysisSession as AmbiguousIfSync<_>>::some_item;
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReferenceIncludes {
Plain,
UseImports,
PlainAndUseImports,
}
type PreparedFilesCache = HashMap<Arc<str>, (Arc<str>, u64)>;
type SuppressionMapCache = HashMap<Arc<str>, (Arc<str>, Arc<crate::suppression::SuppressionMap>)>;
pub(crate) struct BatchReplayFile {
pub(crate) issues: Arc<[mir_issues::Issue]>,
pub(crate) ref_locs: Arc<[crate::cache::CachedRefLoc]>,
pub(crate) symbols: Arc<[crate::symbol::ResolvedSymbol]>,
}
#[derive(PartialEq, Eq)]
pub(crate) struct BatchReplayKey {
php_version: u8,
skip_symbols: bool,
content_hashes: HashMap<Arc<str>, String>,
}
pub(crate) struct BatchReplayState {
key: BatchReplayKey,
pub(crate) files: HashMap<Arc<str>, BatchReplayFile>,
}
const UNRESOLVABLE_CACHE_CAP: usize = 10_000;
pub(crate) struct DeferredRevisionBumps<'a> {
session: &'a mut AnalysisSession,
}
impl std::ops::Deref for DeferredRevisionBumps<'_> {
type Target = AnalysisSession;
fn deref(&self) -> &AnalysisSession {
self.session
}
}
impl std::ops::DerefMut for DeferredRevisionBumps<'_> {
fn deref_mut(&mut self) -> &mut AnalysisSession {
self.session
}
}
impl Drop for DeferredRevisionBumps<'_> {
fn drop(&mut self) {
self.session.db.salsa.resume_revision_bumps();
}
}
impl AnalysisSession {
pub fn new(php_version: PhpVersion) -> Self {
let mut db = AnalyzerDb::new();
db.salsa.set_php_version(Arc::from(php_version.to_string()));
db.salsa
.set_source_provider(Arc::new(crate::FsSourceProvider::new()));
Self {
db,
cache: None,
psr4: None,
resolver: None,
php_version,
memoize_method_call_results: false,
forbidden_functions: Vec::new(),
user_stub_files: Vec::new(),
user_stub_dirs: Vec::new(),
stale_defined_symbols: HashMap::default(),
last_ingested_symbols: HashMap::default(),
last_structural_targets: HashMap::default(),
unresolvable_fqcns: HashMap::default(),
pending_eager_function_files: Some(Vec::new()),
prepared_files: HashMap::default(),
suppression_maps: HashMap::default(),
prepare_generation: 0,
index: Arc::default(),
}
}
pub fn snapshot(&self) -> AnalysisSnapshot {
AnalysisSnapshot {
db: self.db.snapshot_db(),
index: Arc::clone(&self.index),
cache: self.cache.clone(),
php_version: self.php_version,
index_generation: self.index_generation(),
retire_epoch: self.index.retire_epoch(),
}
}
pub(crate) fn db_view(&self) -> DbView<'_> {
DbView::new(self, self.snapshot())
}
pub(crate) fn query_snapshot<T>(
&self,
query: impl FnOnce(&AnalysisSnapshot) -> Result<T, salsa::Cancelled>,
) -> T {
match query(&self.db_view()) {
Ok(out) => out,
Err(cancelled) => snapshot::unwind(cancelled),
}
}
pub fn ref_query_cache_hits(&self) -> u64 {
self.index.ref_queries.hits()
}
pub fn ref_query_cache_locations(&self) -> usize {
self.index.ref_queries.items()
}
pub fn subtype_query_cache_hits(&self) -> u64 {
self.index.subtype_queries.hits()
}
pub fn subtype_query_cache_sites(&self) -> usize {
self.index.subtype_queries.items()
}
pub fn ref_index_lock_count(&self) -> u64 {
self.db.salsa.ref_index_lock_count()
}
pub(crate) fn transient_batch_replay(
&self,
php_version: u8,
skip_symbols: bool,
content_hashes: &HashMap<Arc<str>, String>,
) -> Option<Arc<BatchReplayState>> {
let state = self.index.transient_batch_replay()?;
(state.key.php_version == php_version
&& state.key.skip_symbols == skip_symbols
&& state.key.content_hashes == *content_hashes)
.then_some(state)
}
pub(crate) fn store_transient_batch_replay(
&self,
php_version: u8,
skip_symbols: bool,
content_hashes: &HashMap<Arc<str>, String>,
files: HashMap<Arc<str>, BatchReplayFile>,
) {
self.index.store_transient_batch_replay(BatchReplayState {
key: BatchReplayKey {
php_version,
skip_symbols,
content_hashes: content_hashes.clone(),
},
files,
});
}
pub(crate) fn defer_revision_bumps(&mut self) -> DeferredRevisionBumps<'_> {
self.db.salsa.defer_revision_bumps();
DeferredRevisionBumps { session: self }
}
pub fn class_mention_stats(&self) -> crate::db::ClassMentionStats {
self.db.salsa.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>> {
self.query_snapshot(|snap| snap.files_mentioning_any(files, needles))
}
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.workspace_symbol_index_singleton().is_some()
}
pub fn workspace_index_walks(&self) -> u64 {
self.db.salsa.workspace_index_walks()
}
pub fn work_count(&self, work: crate::db::Work) -> u64 {
self.db.salsa.work_count(work)
}
pub fn with_source_provider(mut self, provider: Arc<dyn crate::SourceProvider>) -> Self {
self.db.salsa.set_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"
);
self.db.attach_cache_dir(cache.cache_dir());
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.attach_cache_dir(cache_dir);
let mut user_stub_fp =
crate::stubs::user_stub_fingerprint(&self.user_stub_files, &self.user_stub_dirs);
if self.memoize_method_call_results {
user_stub_fp ^= 0x6d65_6d6f_697a_6531;
}
for name in &self.forbidden_functions {
user_stub_fp = user_stub_fp.rotate_left(5)
^ name.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |h, b| {
(h ^ u64::from(b)).wrapping_mul(0x100_0000_01b3)
});
}
self.cache = Some(Arc::new(AnalysisCache::open(
cache_dir,
self.php_version.cache_byte(),
user_stub_fp,
)));
self
}
pub fn with_forbidden_functions(mut self, names: Vec<String>) -> Self {
self.db.salsa.set_forbidden_functions(names.clone());
self.forbidden_functions = names;
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.set_resolver(Some(resolver));
self.pending_eager_function_files = 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.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 with_memoize_method_call_results(mut self, enabled: bool) -> Self {
self.memoize_method_call_results = enabled;
self.db.salsa.set_memoize_method_call_results(enabled);
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 index_state;
mod ingest;
mod loading;
mod queries;
mod snapshot;
mod stubs;
use index_state::IndexState;
pub use queries::SubtypeClassSite;
pub use snapshot::AnalysisSnapshot;
use snapshot::DbView;
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 symbol in crate::db::file_structural_symbols(db, sf).iter() {
let lookup = crate::defining_file_lookup_key(symbol);
if let Some(defining_file) = db.symbol_defining_file(lookup) {
if defining_file.as_ref() != file {
targets.insert(defining_file.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_program, 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);
}
crate::recursion::walk_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());
}
}
_ => {}
}
crate::recursion::walk_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()
}