use php_ast::owned::ExprKind;
use mir_codebase::definitions::AssertionKind;
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::arrays::{
get_shape_path_type, resolve_shape_base_current_type, set_shape_base_narrowed, set_shape_path,
ShapeBase,
};
use super::core::{
extract_any_prop_access, extract_chained_prop_access, extract_class_fqcn_from_expr,
extract_prop_access, extract_static_prop_access, extract_var_name,
narrow_receiver_non_null_on_prop_match, resolve_prop_current_type,
resolve_static_prop_current_type,
};
use super::instanceof_core::{filter_out_instanceof_match, filter_out_intersection_match};
pub(super) fn apply_docblock_assertions(
call: &php_ast::owned::FunctionCallExpr,
ctx: &mut FlowState,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
fn_name: &str,
) -> bool {
let fn_name = fn_name
.strip_prefix('\\')
.map(|s| s.to_string())
.unwrap_or_else(|| fn_name.to_string());
let fn_active = |name: &str| -> bool {
let here = crate::db::Fqcn::from_str(db, name);
crate::db::find_function(db, here).is_some()
};
let resolved_fn_name = {
let qualified = crate::db::resolve_name(db, file, &fn_name);
if fn_active(qualified.as_str()) {
qualified
} else if fn_active(fn_name.as_str()) {
fn_name.clone()
} else {
qualified
}
};
let here = crate::db::Fqcn::from_str(db, resolved_fn_name.as_str());
let Some(f) = crate::db::find_function(db, here) else {
return false;
};
apply_assertions(
&f.assertions,
&f.params,
&f.template_params,
&call.args,
None,
ctx,
is_true,
db,
file,
)
}
pub(super) fn apply_method_docblock_assertions(
call_args: &[php_ast::owned::Arg],
receiver: &php_ast::owned::Expr,
resolved: &crate::call::method::ResolvedMethod,
ctx: &mut FlowState,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) -> bool {
if resolved.assertions.is_empty() {
return false;
}
apply_assertions(
&resolved.assertions,
&resolved.params,
&resolved.template_params,
call_args,
Some(receiver),
ctx,
is_true,
db,
file,
)
}
#[allow(clippy::too_many_arguments)]
fn apply_assertions(
assertions: &[mir_codebase::definitions::Assertion],
params: &[mir_codebase::definitions::DeclaredParam],
template_params: &[mir_codebase::definitions::TemplateParam],
call_args: &[php_ast::owned::Arg],
receiver: Option<&php_ast::owned::Expr>,
ctx: &mut FlowState,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) -> bool {
let expected_kind = if is_true {
AssertionKind::AssertIfTrue
} else {
AssertionKind::AssertIfFalse
};
let template_bindings =
compute_assertion_template_bindings(template_params, params, call_args, ctx, db, file);
let mut applied = false;
for assertion in assertions
.iter()
.filter(|a| a.kind == expected_kind || (is_true && a.kind == AssertionKind::Assert))
{
if apply_one_assertion(
assertion,
params,
call_args,
receiver,
ctx,
template_bindings.as_ref(),
db,
file,
) {
applied = true;
}
}
applied
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_one_assertion(
assertion: &mir_codebase::definitions::Assertion,
params: &[mir_codebase::definitions::DeclaredParam],
call_args: &[php_ast::owned::Arg],
receiver: Option<&php_ast::owned::Expr>,
ctx: &mut FlowState,
template_bindings: Option<&rustc_hash::FxHashMap<mir_types::Name, Type>>,
db: &dyn MirDatabase,
file: &str,
) -> bool {
if assertion.param_key.is_empty() {
if let Some(prop_name) = assertion.param.strip_prefix("this->") {
if !prop_name.contains("->") && !prop_name.contains('[') {
let Some(obj_key) = receiver.and_then(extract_var_name) else {
return false;
};
let ty = match template_bindings {
Some(b) => assertion.ty.substitute_templates(b),
None => assertion.ty.clone(),
};
let ty = if assertion.negated {
let current = resolve_prop_current_type(ctx, &obj_key, prop_name, db, file);
negate_assertion_type(¤t, &ty, db)
} else {
ty
};
let proved_prop_non_null = !ty.is_nullable();
ctx.set_prop_refined(&obj_key, prop_name, ty);
narrow_receiver_non_null_on_prop_match(ctx, &obj_key, proved_prop_non_null);
return true;
}
}
}
let Some(index) = params.iter().position(|p| p.name == assertion.param) else {
return false;
};
let expanded_args: Vec<php_ast::owned::Arg>;
let call_args: &[php_ast::owned::Arg] =
if let Some(v) = expand_literal_spread_call_args(call_args) {
expanded_args = v;
&expanded_args
} else {
call_args
};
let mut applied = false;
let variadic_args: Vec<&php_ast::owned::Arg>;
let args_to_check: &[&php_ast::owned::Arg] = if params[index].is_variadic {
variadic_args = call_args
.iter()
.filter(|a| a.name.is_none() && !a.unpack)
.skip(index)
.collect();
&variadic_args
} else {
variadic_args = arg_for_param_index(params, call_args, index)
.into_iter()
.collect();
&variadic_args
};
for arg in args_to_check {
if !assertion.param_key.is_empty() {
let path = &assertion.param_key;
let base = if let Some(name) = extract_var_name(&arg.value) {
Some(ShapeBase::Var(name))
} else if let Some((obj, prop)) = extract_chained_prop_access(&arg.value) {
Some(ShapeBase::Prop(obj, prop))
} else {
extract_static_prop_access(&arg.value, ctx, db, file)
.map(|(fqcn, prop)| ShapeBase::Static(fqcn, prop))
};
if let Some(base) = base {
let current = resolve_shape_base_current_type(ctx, &base, db, file);
let ty = match template_bindings {
Some(b) => assertion.ty.substitute_templates(b),
None => assertion.ty.clone(),
};
let ty = if assertion.negated {
let current_leaf = get_shape_path_type(¤t, path);
negate_assertion_type(¤t_leaf, &ty, db)
} else {
ty
};
let narrowed = set_shape_path(¤t, path, &ty);
set_shape_base_narrowed(ctx, &base, current, narrowed);
applied = true;
}
continue;
}
if let Some(var_name) = extract_var_name(&arg.value) {
let ty = match template_bindings {
Some(b) => assertion.ty.substitute_templates(b),
None => assertion.ty.clone(),
};
let ty = if assertion.negated {
negate_assertion_type(&ctx.get_var(&var_name), &ty, db)
} else {
ty
};
ctx.set_var(&var_name, ty);
applied = true;
} else if let Some((obj, prop)) = extract_chained_prop_access(&arg.value) {
let ty = match template_bindings {
Some(b) => assertion.ty.substitute_templates(b),
None => assertion.ty.clone(),
};
let ty = if assertion.negated {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
negate_assertion_type(¤t, &ty, db)
} else {
ty
};
let proved_prop_non_null = !ty.is_nullable();
ctx.set_prop_refined(&obj, &prop, ty);
narrow_receiver_non_null_on_prop_match(ctx, &obj, proved_prop_non_null);
applied = true;
} else if let Some((fqcn, prop)) = extract_static_prop_access(&arg.value, ctx, db, file) {
let ty = match template_bindings {
Some(b) => assertion.ty.substitute_templates(b),
None => assertion.ty.clone(),
};
let ty = if assertion.negated {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
negate_assertion_type(¤t, &ty, db)
} else {
ty
};
ctx.set_prop_refined(&fqcn, &prop, ty);
applied = true;
}
}
applied
}
pub(crate) fn compute_assertion_template_bindings(
template_params: &[mir_codebase::definitions::TemplateParam],
params: &[mir_codebase::definitions::DeclaredParam],
call_args: &[php_ast::owned::Arg],
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<rustc_hash::FxHashMap<mir_types::Name, Type>> {
if template_params.is_empty() {
return None;
}
let expanded_args: Vec<php_ast::owned::Arg>;
let call_args: &[php_ast::owned::Arg] =
if let Some(v) = expand_literal_spread_call_args(call_args) {
expanded_args = v;
&expanded_args
} else {
call_args
};
let arg_types: Vec<Type> = call_args
.iter()
.map(|arg| assertion_arg_type(&arg.value, ctx, db, file))
.collect();
let arg_names: Vec<Option<String>> = call_args
.iter()
.map(|arg| arg.name.as_ref().map(crate::parser::name_to_string_owned))
.collect();
Some(
crate::generic::infer_template_bindings(
db,
template_params,
params,
&arg_types,
&arg_names,
)
.0,
)
}
pub(super) fn method_call_receiver_fqcn(
object: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
method_name: &str,
) -> Option<std::sync::Arc<str>> {
let obj_ty = if let Some(obj_var) = extract_var_name(object) {
ctx.get_var(&obj_var)
} else if let Some((obj_var, prop)) = extract_any_prop_access(object) {
resolve_prop_current_type(ctx, &obj_var, &prop, db, file)
} else if let Some((fqcn, prop)) = extract_static_prop_access(object, ctx, db, file) {
resolve_static_prop_current_type(ctx, &fqcn, &prop, db)
} else {
crate::expr::assignment::resolve_chained_receiver_type(object, ctx, db, file)?
};
let non_null_atoms: Vec<&Atomic> = obj_ty
.types
.iter()
.filter(|t| !matches!(t, Atomic::TNull))
.collect();
match non_null_atoms.as_slice() {
[Atomic::TNamedObject { fqcn, .. }]
| [Atomic::TSelf { fqcn }]
| [Atomic::TStaticObject { fqcn }]
| [Atomic::TParent { fqcn }] => Some(std::sync::Arc::from(fqcn.as_ref())),
[Atomic::TIntersection { parts }] => {
parts
.iter()
.flat_map(|p| p.types.iter())
.find_map(|atomic| match atomic {
Atomic::TNamedObject { fqcn, .. } => {
let resolved = crate::db::resolve_name(db, file, fqcn.as_ref());
let resolved: std::sync::Arc<str> = std::sync::Arc::from(resolved.as_str());
crate::db::has_method_in_chain(db, &resolved, method_name)
.then_some(resolved)
}
_ => None,
})
}
_ => None,
}
}
pub(super) fn resolve_static_call_class_fqcn(
class_expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<std::sync::Arc<str>> {
let ExprKind::Identifier(id) = &class_expr.kind else {
return None;
};
let resolved = crate::db::resolve_name(db, file, id.as_ref());
match resolved.as_str() {
"self" | "static" => Some(std::sync::Arc::from(
ctx.self_fqcn.as_deref().or(ctx.static_fqcn.as_deref())?,
)),
"parent" => Some(std::sync::Arc::from(ctx.parent_fqcn.as_deref()?)),
s => Some(std::sync::Arc::from(s)),
}
}
pub(crate) fn negate_assertion_type(current: &Type, asserted: &Type, db: &dyn MirDatabase) -> Type {
if current.is_mixed_not_template() {
return current.clone();
}
let mut result = current.clone();
for atomic in &asserted.types {
result = match atomic {
Atomic::TNull => result.remove_null(),
Atomic::TFalse => result.remove_false(),
Atomic::TTrue => result.remove_true(),
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => filter_out_instanceof_match(&result, fqcn, db),
Atomic::TIntersection { parts } => filter_out_intersection_match(&result, parts, db),
_ => result,
};
}
result
}
fn arg_for_param_index<'a>(
params: &[mir_codebase::definitions::DeclaredParam],
call_args: &'a [php_ast::owned::Arg],
param_index: usize,
) -> Option<&'a php_ast::owned::Arg> {
let param_name = params.get(param_index)?.name.as_ref();
if let Some(arg) = call_args.iter().find(|a| {
a.name
.as_ref()
.is_some_and(|n| crate::parser::name_to_string_owned(n) == param_name)
}) {
return Some(arg);
}
call_args
.iter()
.filter(|a| a.name.is_none())
.nth(param_index)
}
fn expand_literal_spread_call_args(
call_args: &[php_ast::owned::Arg],
) -> Option<Vec<php_ast::owned::Arg>> {
let [sole] = call_args else {
return None;
};
if !sole.unpack {
return None;
}
let ExprKind::Array(elements) = &sole.value.kind else {
return None;
};
let mut expanded = Vec::with_capacity(elements.len());
for el in elements.iter() {
if el.key.is_some() || el.unpack || el.by_ref {
return None;
}
expanded.push(php_ast::owned::Arg {
name: None,
value: el.value.clone(),
unpack: false,
by_ref: false,
span: el.span,
});
}
Some(expanded)
}
fn assertion_arg_type(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Type {
if let Some(var_name) = extract_var_name(expr) {
return ctx.get_var(&var_name);
}
if let Some((obj_var, prop)) = extract_prop_access(expr) {
return resolve_prop_current_type(ctx, &obj_var, &prop, db, file);
}
if let Some(fqcn) = extract_class_fqcn_from_expr(
expr,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
return Type::single(Atomic::TClassString(Some(mir_types::Name::from(
fqcn.as_ref(),
))));
}
Type::mixed()
}