use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::ControlFlow;
use std::sync::Arc;
use mir_types::Type;
use php_ast::owned::visitor::{walk_owned_expr, OwnedVisitor};
use php_ast::owned::{CallableCreateKind, Expr, ExprKind};
use crate::db::{MirDatabase, SourceFile};
use crate::parser::type_from_hint_owned;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) enum CalleeKey {
Function(Arc<str>),
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CallArgReturn {
pub callee: CalleeKey,
pub arg_position: u16,
pub return_type: Type,
}
fn resolve_plain_function_name(db: &dyn MirDatabase, file: &str, fn_name: &str) -> String {
let imports = db.file_imports(file);
let qualified = if let Some(imported) = imports.get(&mir_types::Name::new(fn_name)) {
imported.as_str().to_string()
} else if fn_name.contains('\\') {
crate::db::resolve_name(db, file, fn_name)
} else if let Some(ns) = db.file_namespace(file) {
format!("{ns}\\{fn_name}")
} else {
fn_name.to_string()
};
let exists = |name: &str| -> bool {
crate::db::find_function(db, crate::db::Fqcn::from_str(db, name)).is_some()
};
if exists(&qualified) {
qualified
} else if exists(fn_name) {
fn_name.to_string()
} else {
qualified
}
}
fn resolve_concrete_callback_return(db: &dyn MirDatabase, file: &str, expr: &Expr) -> Option<Type> {
match &expr.kind {
ExprKind::Closure(c) => {
let hint = c.return_type.as_ref()?;
let ty = type_from_hint_owned(hint, None);
Some(crate::stmt::resolve_union_for_file(ty, db, file))
}
ExprKind::ArrowFunction(a) => {
let hint = a.return_type.as_ref()?;
let ty = type_from_hint_owned(hint, None);
Some(crate::stmt::resolve_union_for_file(ty, db, file))
}
ExprKind::CallableCreate(cc) => {
let CallableCreateKind::Function(target) = &cc.kind else {
return None;
};
let ExprKind::Identifier(name) = &target.kind else {
return None;
};
let fqn = resolve_plain_function_name(db, file, name.as_ref());
let f = crate::db::find_function(db, crate::db::Fqcn::from_str(db, &fqn))?;
f.return_type.as_deref().cloned()
}
ExprKind::String(s) if !s.is_empty() => {
let fqn = resolve_plain_function_name(db, file, s.as_ref());
let f = crate::db::find_function(db, crate::db::Fqcn::from_str(db, &fqn))?;
f.return_type.as_deref().cloned()
}
_ => None,
}
}
struct OpaqueCallScanner<'a> {
db: &'a dyn MirDatabase,
file: &'a str,
out: Vec<CallArgReturn>,
}
impl OwnedVisitor for OpaqueCallScanner<'_> {
fn visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
let fqn = resolve_plain_function_name(self.db, self.file, name.as_ref());
let callee = CalleeKey::Function(Arc::from(fqn.as_str()));
let mut position = 0u16;
for arg in call.args.iter() {
if arg.name.is_none() && !arg.unpack {
if let Some(return_type) = arg
.value
.as_ref()
.and_then(|v| resolve_concrete_callback_return(self.db, self.file, v))
{
self.out.push(CallArgReturn {
callee: callee.clone(),
arg_position: position,
return_type,
});
}
position += 1;
}
}
}
}
walk_owned_expr(self, expr)
}
}
#[salsa::tracked]
pub(crate) fn file_callable_call_args(
db: &dyn MirDatabase,
file: SourceFile,
) -> Arc<[CallArgReturn]> {
let path = file.path(db);
let text = file.text(db);
let parsed = php_rs_parser::parse(text);
let mut scanner = OpaqueCallScanner {
db,
file: path.as_ref(),
out: Vec::new(),
};
let _ = scanner.visit_program(&parsed.program);
Arc::from(scanner.out)
}
thread_local! {
static OPAQUE_CB_IN_PROGRESS: RefCell<HashSet<(CalleeKey, u16)>> = RefCell::new(HashSet::new());
}
struct OpaqueCbGuard(CalleeKey, u16);
impl Drop for OpaqueCbGuard {
fn drop(&mut self) {
OPAQUE_CB_IN_PROGRESS.with(|s| {
s.borrow_mut().remove(&(self.0.clone(), self.1));
});
}
}
pub(crate) fn opaque_callback_return_type(
db: &dyn MirDatabase,
callee: &CalleeKey,
arg_position: u16,
) -> Option<Type> {
let key = (callee.clone(), arg_position);
let already_active = OPAQUE_CB_IN_PROGRESS.with(|s| s.borrow().contains(&key));
if already_active {
return None;
}
OPAQUE_CB_IN_PROGRESS.with(|s| {
s.borrow_mut().insert(key.clone());
});
let _guard = OpaqueCbGuard(callee.clone(), arg_position);
let mut acc: Option<Type> = None;
for file in db.all_source_files() {
for rec in file_callable_call_args(db, file).iter() {
if &rec.callee == callee && rec.arg_position == arg_position {
match &mut acc {
None => acc = Some(rec.return_type.clone()),
Some(t) => t.merge_with(&rec.return_type),
}
}
}
}
acc
}