use php_ast::ast::{AssignOp, BinaryOp, UnaryPrefixOp};
use php_ast::owned::ExprKind;
use mir_codebase::definitions::AssertionKind;
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
fn narrow_from_static_or_class_const_comparison(
ctx: &mut FlowState,
b: &php_ast::owned::BinaryExpr,
effective_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
if let ExprKind::StaticPropertyAccess(_) = &b.right.kind {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
if let Some(var_name) = extract_var_name(&b.left) {
narrow_var_to_literal_enum_case(
db,
ctx,
&var_name,
&enum_fqcn,
&case_name,
effective_true,
);
}
}
else if let Some((fqcn, prop)) = extract_static_prop_access(&b.right, ctx, db, file) {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.left,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_literal_enum_case(
db,
ctx,
&fqcn,
&prop,
(&enum_fqcn, &case_name),
effective_true,
);
} else if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(class_fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_class_string(
ctx,
&fqcn,
&prop,
&class_fqcn,
effective_true,
db,
);
}
}
}
} else if let ExprKind::StaticPropertyAccess(_) = &b.left.kind {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.left,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
if let Some(var_name) = extract_var_name(&b.right) {
narrow_var_to_literal_enum_case(
db,
ctx,
&var_name,
&enum_fqcn,
&case_name,
effective_true,
);
}
}
else if let Some((fqcn, prop)) = extract_static_prop_access(&b.left, ctx, db, file) {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_literal_enum_case(
db,
ctx,
&fqcn,
&prop,
(&enum_fqcn, &case_name),
effective_true,
);
} else if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(class_fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_class_string(
ctx,
&fqcn,
&prop,
&class_fqcn,
effective_true,
db,
);
}
}
}
}
else if let ExprKind::ClassConstAccess(_) = &b.right.kind {
if let Some(var_name) = extract_var_name(&b.left) {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_var_to_literal_enum_case(
db,
ctx,
&var_name,
&enum_fqcn,
&case_name,
effective_true,
);
} else if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_var_to_class_string(ctx, &var_name, &fqcn, effective_true, db);
}
}
}
else if let Some((obj_var, prop)) = extract_any_prop_access(&b.left) {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_prop_to_literal_enum_case(
db,
ctx,
&obj_var,
&prop,
file,
(&enum_fqcn, &case_name),
effective_true,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, effective_true);
} else if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_prop_to_class_string(
ctx,
&obj_var,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, effective_true);
}
}
}
else if let Some(target) = extract_get_class_arg(&b.left) {
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_get_class_static_prop_arg(&b.left, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
}
else if let Some(target) = extract_get_debug_type_arg(&b.left) {
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
match &target {
ScalarArgTarget::Var(obj_var_name) => narrow_var_to_specific_class(
ctx,
obj_var_name,
&fqcn,
effective_true,
db,
),
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
obj,
prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, obj, effective_true);
}
}
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_get_debug_type_static_prop_arg(&b.left, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
}
else if let Some(target) = extract_get_parent_class_arg(&b.left) {
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_from_get_parent_class_literal(
ctx,
&target,
&fqcn,
effective_true,
db,
file,
);
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_get_parent_class_static_prop_arg(&b.left, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_is_subclass_of(
ctx,
&fqcn_recv,
&prop,
&fqcn,
db,
effective_true,
);
}
}
}
else if let Some(target) = extract_dynamic_class_const_var(&b.left) {
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_dynamic_class_const_static_prop_var(&b.left, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.right.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
}
else if let Some(target) = extract_dynamic_class_const_var(&b.right) {
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_dynamic_class_const_static_prop_var(&b.right, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
}
} else if let ExprKind::ClassConstAccess(_) = &b.left.kind {
if let Some(var_name) = extract_var_name(&b.right) {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.left,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_var_to_literal_enum_case(
db,
ctx,
&var_name,
&enum_fqcn,
&case_name,
effective_true,
);
} else if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_var_to_class_string(ctx, &var_name, &fqcn, effective_true, db);
}
}
}
else if let Some((obj_var, prop)) = extract_any_prop_access(&b.right) {
if let Some((enum_fqcn, case_name)) = extract_enum_case(
&b.left,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_prop_to_literal_enum_case(
db,
ctx,
&obj_var,
&prop,
file,
(&enum_fqcn, &case_name),
effective_true,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, effective_true);
} else if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_prop_to_class_string(
ctx,
&obj_var,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, effective_true);
}
}
}
else if let Some(target) = extract_get_class_arg(&b.right) {
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_get_class_static_prop_arg(&b.right, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
}
else if let Some(target) = extract_get_debug_type_arg(&b.right) {
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
match &target {
ScalarArgTarget::Var(obj_var_name) => narrow_var_to_specific_class(
ctx,
obj_var_name,
&fqcn,
effective_true,
db,
),
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
obj,
prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, obj, effective_true);
}
}
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_get_debug_type_static_prop_arg(&b.right, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
}
else if let Some(target) = extract_get_parent_class_arg(&b.right) {
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_from_get_parent_class_literal(
ctx,
&target,
&fqcn,
effective_true,
db,
file,
);
}
}
}
else if let Some((fqcn_recv, prop)) =
extract_get_parent_class_static_prop_arg(&b.right, ctx, db, file)
{
if let ExprKind::ClassConstAccess(cca) = &b.left.kind {
if let Some(fqcn) = extract_class_const_fqcn(
cca,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_is_subclass_of(
ctx,
&fqcn_recv,
&prop,
&fqcn,
db,
effective_true,
);
}
}
}
}
}
pub fn narrow_from_condition(
expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
match &expr.kind {
ExprKind::Parenthesized(inner) => {
narrow_from_condition(inner, ctx, is_true, db, file);
}
ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::BooleanNot => {
narrow_from_condition(&u.operand, ctx, !is_true, db, file);
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanAnd || b.op == BinaryOp::LogicalAnd => {
if is_true {
narrow_from_condition(&b.left, ctx, true, db, file);
narrow_from_condition(&b.right, ctx, true, db, file);
promote_assignment_effects(&b.left, ctx, db, file);
promote_assignment_effects(&b.right, ctx, db, file);
}
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
if !is_true {
narrow_from_condition(&b.left, ctx, false, db, file);
narrow_from_condition(&b.right, ctx, false, db, file);
promote_assignment_effects(&b.left, ctx, db, file);
promote_assignment_effects(&b.right, ctx, db, file);
} else {
narrow_or_instanceof_true(&b.left, &b.right, ctx, db, file);
narrow_or_isset_true(&b.left, &b.right, ctx, db, file);
}
}
ExprKind::Binary(b) if b.op == BinaryOp::Identical || b.op == BinaryOp::NotIdentical => {
let is_identical = b.op == BinaryOp::Identical;
let effective_true = if is_identical { is_true } else { !is_true };
narrow_count_or_strlen_equality(ctx, db, file, &b.left, &b.right, b.op, is_true);
if let Some(nc) = extract_null_coalesce(&b.left) {
if let Some(var_name) = extract_var_name(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.right) {
let current = ctx.get_var(&var_name);
ctx.set_var(&var_name, current.remove_null());
}
} else if let Some((obj, prop)) = extract_any_prop_access(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.right) {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
narrow_receiver_non_null_on_prop_match(ctx, &obj, !effective_true);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&nc.left, ctx, db, file)
{
if !effective_true && same_literal(&nc.right, &b.right) {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
} else if let Some(nc) = extract_null_coalesce(&b.right) {
if let Some(var_name) = extract_var_name(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.left) {
let current = ctx.get_var(&var_name);
ctx.set_var(&var_name, current.remove_null());
}
} else if let Some((obj, prop)) = extract_any_prop_access(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.left) {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
narrow_receiver_non_null_on_prop_match(ctx, &obj, !effective_true);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&nc.left, ctx, db, file)
{
if !effective_true && same_literal(&nc.right, &b.left) {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
}
else if matches!(b.right.kind, ExprKind::Null) {
if let Some(target) = extract_array_key_first_or_last_arg(&b.left) {
match target {
ScalarArgTarget::Var(arr_var) => {
narrow_array_key_first_or_last_null(ctx, &arr_var, effective_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_array_key_first_or_last_null(
ctx,
&obj,
&prop,
db,
file,
effective_true,
)
}
}
} else if let Some((fqcn, prop)) =
extract_array_key_first_or_last_static_prop_arg(&b.left, ctx, db, file)
{
narrow_static_prop_array_key_first_or_last_null(
ctx,
&fqcn,
&prop,
db,
effective_true,
);
} else if let Some(name) = extract_var_name(&b.left) {
narrow_var_null(ctx, &name, effective_true);
} else if let Some((obj, prop)) = extract_nullsafe_prop_access(&b.left) {
narrow_nullsafe_prop_null(ctx, &obj, &prop, db, file, effective_true);
} else if let ExprKind::NullsafeMethodCall(mc) = &b.left.kind {
narrow_nullsafe_method_call_null(ctx, mc, db, effective_true);
} else if let Some((obj, prop)) = extract_prop_access(&b.left) {
narrow_prop_null(ctx, &obj, &prop, db, file, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_null(ctx, &fqcn, &prop, db, effective_true);
}
} else if matches!(b.left.kind, ExprKind::Null) {
if let Some(target) = extract_array_key_first_or_last_arg(&b.right) {
match target {
ScalarArgTarget::Var(arr_var) => {
narrow_array_key_first_or_last_null(ctx, &arr_var, effective_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_array_key_first_or_last_null(
ctx,
&obj,
&prop,
db,
file,
effective_true,
)
}
}
} else if let Some((fqcn, prop)) =
extract_array_key_first_or_last_static_prop_arg(&b.right, ctx, db, file)
{
narrow_static_prop_array_key_first_or_last_null(
ctx,
&fqcn,
&prop,
db,
effective_true,
);
} else if let Some(name) = extract_var_name(&b.right) {
narrow_var_null(ctx, &name, effective_true);
} else if let Some((obj, prop)) = extract_nullsafe_prop_access(&b.right) {
narrow_nullsafe_prop_null(ctx, &obj, &prop, db, file, effective_true);
} else if let ExprKind::NullsafeMethodCall(mc) = &b.right.kind {
narrow_nullsafe_method_call_null(ctx, mc, db, effective_true);
} else if let Some((obj, prop)) = extract_prop_access(&b.right) {
narrow_prop_null(ctx, &obj, &prop, db, file, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_null(ctx, &fqcn, &prop, db, effective_true);
}
}
else if matches!(b.right.kind, ExprKind::Bool(true)) {
if let Some(name) = extract_var_name(&b.left) {
narrow_var_bool(ctx, &name, true, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_bool(ctx, &obj, &prop, db, file, true, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_bool(ctx, &fqcn, &prop, db, true, effective_true);
} else {
narrow_from_condition(&b.left, ctx, effective_true, db, file);
}
} else if matches!(b.right.kind, ExprKind::Bool(false)) {
if let Some(name) = extract_var_name(&b.left) {
narrow_var_bool(ctx, &name, false, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_bool(ctx, &obj, &prop, db, file, false, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_bool(ctx, &fqcn, &prop, db, false, effective_true);
} else {
narrow_from_false_comparable_call(&b.left, ctx, db, file, effective_true);
narrow_from_condition(&b.left, ctx, !effective_true, db, file);
}
}
else if matches!(b.left.kind, ExprKind::Bool(true)) {
if let Some(name) = extract_var_name(&b.right) {
narrow_var_bool(ctx, &name, true, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_bool(ctx, &obj, &prop, db, file, true, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_bool(ctx, &fqcn, &prop, db, true, effective_true);
} else {
narrow_from_condition(&b.right, ctx, effective_true, db, file);
}
} else if matches!(b.left.kind, ExprKind::Bool(false)) {
if let Some(name) = extract_var_name(&b.right) {
narrow_var_bool(ctx, &name, false, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_bool(ctx, &obj, &prop, db, file, false, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_bool(ctx, &fqcn, &prop, db, false, effective_true);
} else {
narrow_from_false_comparable_call(&b.right, ctx, db, file, effective_true);
narrow_from_condition(&b.right, ctx, !effective_true, db, file);
}
}
else if let ExprKind::String(class_name_str) = &b.right.kind {
if let Some(target) = extract_get_class_arg(&b.left) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_get_class_static_prop_arg(&b.left, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
} else if let Some(target) = extract_gettype_arg(&b.left) {
narrow_from_gettype_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_debug_type_arg(&b.left) {
narrow_from_get_debug_type_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some((fqcn, prop)) =
extract_gettype_static_prop_arg(&b.left, ctx, db, file)
{
narrow_static_prop_from_gettype_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
);
} else if let Some((fqcn, prop)) =
extract_get_debug_type_static_prop_arg(&b.left, ctx, db, file)
{
narrow_static_prop_from_get_debug_type_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_parent_class_arg(&b.left) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_from_get_parent_class_literal(
ctx,
&target,
&fqcn,
effective_true,
db,
file,
);
} else if let Some((fqcn_recv, prop)) =
extract_get_parent_class_static_prop_arg(&b.left, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_is_subclass_of(
ctx,
&fqcn_recv,
&prop,
&fqcn,
db,
effective_true,
);
} else if let Some(target) = extract_dynamic_class_const_var(&b.left) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_dynamic_class_const_static_prop_var(&b.left, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
} else if let Some(name) = extract_var_name(&b.left) {
narrow_var_literal_string(ctx, &name, class_name_str, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_literal_string(
ctx,
&obj,
&prop,
db,
file,
class_name_str,
effective_true,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_literal_string(
ctx,
&fqcn,
&prop,
db,
class_name_str,
effective_true,
);
}
} else if let ExprKind::String(class_name_str) = &b.left.kind {
if let Some(target) = extract_get_class_arg(&b.right) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_get_class_static_prop_arg(&b.right, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
} else if let Some(target) = extract_gettype_arg(&b.right) {
narrow_from_gettype_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_debug_type_arg(&b.right) {
narrow_from_get_debug_type_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some((fqcn, prop)) =
extract_gettype_static_prop_arg(&b.right, ctx, db, file)
{
narrow_static_prop_from_gettype_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
);
} else if let Some((fqcn, prop)) =
extract_get_debug_type_static_prop_arg(&b.right, ctx, db, file)
{
narrow_static_prop_from_get_debug_type_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_parent_class_arg(&b.right) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_from_get_parent_class_literal(
ctx,
&target,
&fqcn,
effective_true,
db,
file,
);
} else if let Some((fqcn_recv, prop)) =
extract_get_parent_class_static_prop_arg(&b.right, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_is_subclass_of(
ctx,
&fqcn_recv,
&prop,
&fqcn,
db,
effective_true,
);
} else if let Some(target) = extract_dynamic_class_const_var(&b.right) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_dynamic_class_const_static_prop_var(&b.right, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
} else if let Some(name) = extract_var_name(&b.right) {
narrow_var_literal_string(ctx, &name, class_name_str, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_literal_string(
ctx,
&obj,
&prop,
db,
file,
class_name_str,
effective_true,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_literal_string(
ctx,
&fqcn,
&prop,
db,
class_name_str,
effective_true,
);
}
}
else if let ExprKind::Int(n) = &b.right.kind {
if let Some(name) = extract_var_name(&b.left) {
narrow_var_literal_int(ctx, &name, *n, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_literal_int(ctx, &obj, &prop, db, file, *n, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_literal_int(ctx, &fqcn, &prop, db, *n, effective_true);
}
} else if let ExprKind::Int(n) = &b.left.kind {
if let Some(name) = extract_var_name(&b.right) {
narrow_var_literal_int(ctx, &name, *n, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_literal_int(ctx, &obj, &prop, db, file, *n, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_literal_int(ctx, &fqcn, &prop, db, *n, effective_true);
}
}
else if let ExprKind::Array(elems) = &b.right.kind {
if elems.is_empty() {
if let Some(var_name) = extract_var_name(&b.left) {
let current = ctx.get_var(&var_name);
let narrowed = if effective_true {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_array_empty(ctx, &obj, &prop, db, file, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_array_empty(ctx, &fqcn, &prop, db, effective_true);
}
}
} else if let ExprKind::Array(elems) = &b.left.kind {
if elems.is_empty() {
if let Some(var_name) = extract_var_name(&b.right) {
let current = ctx.get_var(&var_name);
let narrowed = if effective_true {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_array_empty(ctx, &obj, &prop, db, file, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_array_empty(ctx, &fqcn, &prop, db, effective_true);
}
}
}
else if matches!(b.right.kind, ExprKind::StaticPropertyAccess(_))
|| matches!(b.left.kind, ExprKind::StaticPropertyAccess(_))
|| matches!(b.right.kind, ExprKind::ClassConstAccess(_))
|| matches!(b.left.kind, ExprKind::ClassConstAccess(_))
{
narrow_from_static_or_class_const_comparison(ctx, b, effective_true, db, file);
}
}
ExprKind::Binary(b)
if matches!(
b.op,
BinaryOp::Less
| BinaryOp::LessOrEqual
| BinaryOp::Greater
| BinaryOp::GreaterOrEqual
) =>
{
if let Some(var_name) = extract_var_name(&b.left) {
if let Some(n) = extract_int_literal(&b.right) {
narrow_var_int_comparison(ctx, &var_name, b.op, n, is_true);
}
} else if let Some(var_name) = extract_var_name(&b.right) {
if let Some(n) = extract_int_literal(&b.left) {
narrow_var_int_comparison(ctx, &var_name, flip_comparison_op(b.op), n, is_true);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
if let Some(n) = extract_int_literal(&b.right) {
narrow_prop_int_comparison(ctx, &obj, &prop, db, file, b.op, n, is_true);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
if let Some(n) = extract_int_literal(&b.left) {
narrow_prop_int_comparison(
ctx,
&obj,
&prop,
db,
file,
flip_comparison_op(b.op),
n,
is_true,
);
}
} else if let Some((fqcn, prop)) = extract_static_prop_access(&b.left, ctx, db, file) {
if let Some(n) = extract_int_literal(&b.right) {
narrow_static_prop_int_comparison(ctx, &fqcn, &prop, db, b.op, n, is_true);
}
} else if let Some((fqcn, prop)) = extract_static_prop_access(&b.right, ctx, db, file) {
if let Some(n) = extract_int_literal(&b.left) {
narrow_static_prop_int_comparison(
ctx,
&fqcn,
&prop,
db,
flip_comparison_op(b.op),
n,
is_true,
);
}
}
let count_call_on_left = extract_count_arg(&b.left).is_some()
|| extract_count_static_prop_arg(&b.left, ctx, db, file).is_some();
let (count_expr, count_cmp_op, count_lit) = if count_call_on_left {
(&b.left, b.op, &b.right)
} else {
(&b.right, flip_comparison_op(b.op), &b.left)
};
if let (Some(target), Some(n)) = (
extract_count_arg(count_expr),
extract_int_literal(count_lit),
) {
match target {
ScalarArgTarget::Var(arr_var) => {
narrow_array_count_comparison(ctx, &arr_var, count_cmp_op, n, is_true)
}
ScalarArgTarget::Prop(obj, prop) => narrow_prop_array_count_comparison(
ctx,
&obj,
&prop,
db,
file,
count_cmp_op,
n,
is_true,
),
}
} else if let (Some((fqcn, prop)), Some(n)) = (
extract_count_static_prop_arg(count_expr, ctx, db, file),
extract_int_literal(count_lit),
) {
narrow_static_prop_array_count_comparison(
ctx,
&fqcn,
&prop,
db,
count_cmp_op,
n,
is_true,
);
}
let strlen_call_on_left = extract_strlen_arg(&b.left).is_some()
|| extract_strlen_static_prop_arg(&b.left, ctx, db, file).is_some();
let (strlen_expr, strlen_cmp_op, strlen_lit) = if strlen_call_on_left {
(&b.left, b.op, &b.right)
} else {
(&b.right, flip_comparison_op(b.op), &b.left)
};
if let (Some(target), Some(n)) = (
extract_strlen_arg(strlen_expr),
extract_int_literal(strlen_lit),
) {
match target {
ScalarArgTarget::Var(str_var) => {
narrow_string_strlen_comparison(ctx, &str_var, strlen_cmp_op, n, is_true)
}
ScalarArgTarget::Prop(obj, prop) => narrow_prop_string_strlen_comparison(
ctx,
&obj,
&prop,
db,
file,
strlen_cmp_op,
n,
is_true,
),
}
} else if let (Some((fqcn, prop)), Some(n)) = (
extract_strlen_static_prop_arg(strlen_expr, ctx, db, file),
extract_int_literal(strlen_lit),
) {
narrow_static_prop_string_strlen_comparison(
ctx,
&fqcn,
&prop,
db,
strlen_cmp_op,
n,
is_true,
);
}
}
ExprKind::Binary(b) if b.op == BinaryOp::Equal || b.op == BinaryOp::NotEqual => {
let is_equal = b.op == BinaryOp::Equal;
let effective_true = if is_equal { is_true } else { !is_true };
narrow_count_or_strlen_equality(ctx, db, file, &b.left, &b.right, b.op, is_true);
if let Some(nc) = extract_null_coalesce(&b.left) {
if let Some(var_name) = extract_var_name(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.right) {
let current = ctx.get_var(&var_name);
ctx.set_var(&var_name, current.remove_null());
}
} else if let Some((obj, prop)) = extract_any_prop_access(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.right) {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
narrow_receiver_non_null_on_prop_match(ctx, &obj, !effective_true);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&nc.left, ctx, db, file)
{
if !effective_true && same_literal(&nc.right, &b.right) {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
} else if let Some(nc) = extract_null_coalesce(&b.right) {
if let Some(var_name) = extract_var_name(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.left) {
let current = ctx.get_var(&var_name);
ctx.set_var(&var_name, current.remove_null());
}
} else if let Some((obj, prop)) = extract_any_prop_access(&nc.left) {
if !effective_true && same_literal(&nc.right, &b.left) {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
narrow_receiver_non_null_on_prop_match(ctx, &obj, !effective_true);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&nc.left, ctx, db, file)
{
if !effective_true && same_literal(&nc.right, &b.left) {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
} else if matches!(b.right.kind, ExprKind::Null) {
if let Some(name) = extract_var_name(&b.left) {
narrow_var_loose_null(ctx, &name, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_loose_null(ctx, &obj, &prop, db, file, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_loose_null(ctx, &fqcn, &prop, db, effective_true);
}
} else if matches!(b.left.kind, ExprKind::Null) {
if let Some(name) = extract_var_name(&b.right) {
narrow_var_loose_null(ctx, &name, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_loose_null(ctx, &obj, &prop, db, file, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_loose_null(ctx, &fqcn, &prop, db, effective_true);
}
}
else if let ExprKind::Bool(value) = &b.right.kind {
if let Some(name) = extract_var_name(&b.left) {
narrow_var_loose_bool(ctx, &name, *value == effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_loose_bool(ctx, &obj, &prop, db, file, *value == effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, *value == effective_true);
} else {
if !value {
narrow_from_false_comparable_call(&b.left, ctx, db, file, effective_true);
}
narrow_from_condition(&b.left, ctx, *value == effective_true, db, file);
}
} else if let ExprKind::Bool(value) = &b.left.kind {
if let Some(name) = extract_var_name(&b.right) {
narrow_var_loose_bool(ctx, &name, *value == effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_loose_bool(ctx, &obj, &prop, db, file, *value == effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, *value == effective_true);
} else {
if !value {
narrow_from_false_comparable_call(&b.right, ctx, db, file, effective_true);
}
narrow_from_condition(&b.right, ctx, *value == effective_true, db, file);
}
}
else if let ExprKind::Array(elems) = &b.right.kind {
if elems.is_empty() {
if let Some(var_name) = extract_var_name(&b.left) {
let current = ctx.get_var(&var_name);
let narrowed = if effective_true {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_array_empty(ctx, &obj, &prop, db, file, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_array_empty(ctx, &fqcn, &prop, db, effective_true);
}
}
} else if let ExprKind::Array(elems) = &b.left.kind {
if elems.is_empty() {
if let Some(var_name) = extract_var_name(&b.right) {
let current = ctx.get_var(&var_name);
let narrowed = if effective_true {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_array_empty(ctx, &obj, &prop, db, file, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_array_empty(ctx, &fqcn, &prop, db, effective_true);
}
}
}
else if let ExprKind::String(class_name_str) = &b.right.kind {
if let Some(target) = extract_get_class_arg(&b.left) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_get_class_static_prop_arg(&b.left, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
} else if let Some(target) = extract_gettype_arg(&b.left) {
narrow_from_gettype_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_debug_type_arg(&b.left) {
narrow_from_get_debug_type_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some((fqcn, prop)) =
extract_gettype_static_prop_arg(&b.left, ctx, db, file)
{
narrow_static_prop_from_gettype_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
);
} else if let Some((fqcn, prop)) =
extract_get_debug_type_static_prop_arg(&b.left, ctx, db, file)
{
narrow_static_prop_from_get_debug_type_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_parent_class_arg(&b.left) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_from_get_parent_class_literal(
ctx,
&target,
&fqcn,
effective_true,
db,
file,
);
} else if let Some((fqcn_recv, prop)) =
extract_get_parent_class_static_prop_arg(&b.left, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_is_subclass_of(
ctx,
&fqcn_recv,
&prop,
&fqcn,
db,
effective_true,
);
} else if let Some(target) = extract_dynamic_class_const_var(&b.left) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_dynamic_class_const_static_prop_var(&b.left, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
} else if let ExprKind::String(class_name_str) = &b.left.kind {
if let Some(target) = extract_get_class_arg(&b.right) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_get_class_static_prop_arg(&b.right, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
} else if let Some(target) = extract_gettype_arg(&b.right) {
narrow_from_gettype_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_debug_type_arg(&b.right) {
narrow_from_get_debug_type_literal(
ctx,
&target,
class_name_str,
effective_true,
db,
file,
);
} else if let Some((fqcn, prop)) =
extract_gettype_static_prop_arg(&b.right, ctx, db, file)
{
narrow_static_prop_from_gettype_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
);
} else if let Some((fqcn, prop)) =
extract_get_debug_type_static_prop_arg(&b.right, ctx, db, file)
{
narrow_static_prop_from_get_debug_type_literal(
ctx,
&fqcn,
&prop,
class_name_str,
effective_true,
db,
file,
);
} else if let Some(target) = extract_get_parent_class_arg(&b.right) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_from_get_parent_class_literal(
ctx,
&target,
&fqcn,
effective_true,
db,
file,
);
} else if let Some((fqcn_recv, prop)) =
extract_get_parent_class_static_prop_arg(&b.right, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_is_subclass_of(
ctx,
&fqcn_recv,
&prop,
&fqcn,
db,
effective_true,
);
} else if let Some(target) = extract_dynamic_class_const_var(&b.right) {
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
match target {
ScalarArgTarget::Var(name) => {
narrow_var_to_specific_class(ctx, &name, &fqcn, effective_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(
ctx,
&obj,
&prop,
&fqcn,
effective_true,
db,
file,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
}
}
} else if let Some((fqcn_recv, prop)) =
extract_dynamic_class_const_static_prop_var(&b.right, ctx, db, file)
{
let fqcn = crate::db::resolve_name(db, file, class_name_str.as_ref());
narrow_static_prop_to_specific_class(
ctx,
&fqcn_recv,
&prop,
&fqcn,
effective_true,
db,
);
}
}
else if let ExprKind::Int(n) = &b.right.kind {
if let Some(name) = extract_var_name(&b.left) {
narrow_var_loose_int(ctx, &name, *n, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.left) {
narrow_prop_loose_int(ctx, &obj, &prop, db, file, *n, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.left, ctx, db, file)
{
narrow_static_prop_loose_int(ctx, &fqcn, &prop, db, *n, effective_true);
}
} else if let ExprKind::Int(n) = &b.left.kind {
if let Some(name) = extract_var_name(&b.right) {
narrow_var_loose_int(ctx, &name, *n, effective_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&b.right) {
narrow_prop_loose_int(ctx, &obj, &prop, db, file, *n, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&b.right, ctx, db, file)
{
narrow_static_prop_loose_int(ctx, &fqcn, &prop, db, *n, effective_true);
}
}
else if matches!(b.right.kind, ExprKind::StaticPropertyAccess(_))
|| matches!(b.left.kind, ExprKind::StaticPropertyAccess(_))
|| matches!(b.right.kind, ExprKind::ClassConstAccess(_))
|| matches!(b.left.kind, ExprKind::ClassConstAccess(_))
{
narrow_from_static_or_class_const_comparison(ctx, b, effective_true, db, file);
}
}
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
if let Some(var_name) = extract_var_name(&b.left) {
if let Some(raw_name) = extract_class_name(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
) {
let class_name = crate::db::resolve_name(db, file, &raw_name);
let current = ctx.get_var(&var_name);
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(
¤t,
&class_name,
db,
&ctx.template_param_names,
)
} else {
filter_out_instanceof_match(¤t, &class_name, db)
};
set_narrowed(ctx, &var_name, ¤t, narrowed, true);
}
} else if let Some((obj, prop)) = extract_nullsafe_prop_access(&b.left) {
if let Some(raw_name) = extract_class_name(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
) {
let class_name = crate::db::resolve_name(db, file, &raw_name);
narrow_prop_instanceof(ctx, &obj, &prop, &class_name, db, file, is_true);
if is_true {
narrow_var_null(ctx, &obj, false);
}
}
} else if let Some((obj, prop)) = extract_prop_access(&b.left) {
if let Some(raw_name) = extract_class_name(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
) {
let class_name = crate::db::resolve_name(db, file, &raw_name);
narrow_prop_instanceof(ctx, &obj, &prop, &class_name, db, file, is_true);
if is_true {
narrow_var_null(ctx, &obj, false);
}
}
} else if let Some((fqcn, prop)) = extract_static_prop_access(&b.left, ctx, db, file) {
if let Some(raw_name) = extract_class_name(
&b.right,
ctx.self_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
) {
let class_name = crate::db::resolve_name(db, file, &raw_name);
narrow_static_prop_instanceof(ctx, &fqcn, &prop, &class_name, db, is_true);
}
}
}
ExprKind::FunctionCall(call) => {
let fn_name_opt: Option<&str> = match &call.name.kind {
ExprKind::Identifier(name) => Some(name.as_ref()),
_ => None,
};
if let Some(fn_name) = fn_name_opt {
let bare = fn_name.trim_start_matches('\\');
if matches!(
bare.to_ascii_lowercase().as_str(),
"class_exists" | "interface_exists" | "trait_exists" | "enum_exists"
) {
if is_true {
if let Some(arg_expr) = call.args.first() {
if let Some(fqcn) = extract_class_fqcn_from_expr(
&arg_expr.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
ctx.class_exists_guards.insert(fqcn);
} else if let Some(var_name) = extract_var_name(&arg_expr.value) {
let current = ctx.get_var(&var_name);
let narrowed = if bare.eq_ignore_ascii_case("interface_exists") {
current.narrow_to_interface_string()
} else {
current.narrow_to_class_string()
};
set_narrowed(ctx, &var_name, ¤t, narrowed, true);
} else if let Some((obj, prop)) =
extract_any_prop_access(&arg_expr.value)
{
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
if !current.is_mixed() {
let narrowed = if bare.eq_ignore_ascii_case("interface_exists")
{
current.narrow_to_interface_string()
} else {
current.narrow_to_class_string()
};
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
}
} else if let Some((fqcn_recv, prop)) =
extract_static_prop_access(&arg_expr.value, ctx, db, file)
{
let current =
resolve_static_prop_current_type(ctx, &fqcn_recv, &prop, db);
if !current.is_mixed() {
let narrowed = if bare.eq_ignore_ascii_case("interface_exists")
{
current.narrow_to_interface_string()
} else {
current.narrow_to_class_string()
};
apply_prop_narrowed(
ctx, &fqcn_recv, &prop, current, narrowed, true,
);
}
}
}
}
} else if bare.eq_ignore_ascii_case("defined") {
if is_true {
if let Some(arg) = call.args.first() {
if let ExprKind::String(name) = &arg.value.kind {
let name = name.as_ref().trim_start_matches('\\');
if !name.is_empty() {
ctx.defined_guards.insert(std::sync::Arc::from(name));
}
}
}
}
} else if bare.eq_ignore_ascii_case("function_exists") {
if is_true {
if let Some(arg) = call.args.first() {
if let ExprKind::String(name) = &arg.value.kind {
let name = name.as_ref().trim_start_matches('\\');
if !name.is_empty() {
ctx.function_exists_guards
.insert(std::sync::Arc::from(name));
}
}
}
}
} else if bare.eq_ignore_ascii_case("extension_loaded") {
if is_true {
if let Some(arg) = call.args.first() {
if let ExprKind::String(ext) = &arg.value.kind {
if !ext.is_empty() {
ctx.extension_loaded_guards
.insert(std::sync::Arc::from(ext.as_ref()));
}
}
}
}
} else if bare.eq_ignore_ascii_case("assert") {
if let Some(arg_expr) = call.args.first() {
narrow_from_condition(&arg_expr.value, ctx, is_true, db, file);
}
} else if bare.eq_ignore_ascii_case("method_exists")
|| bare.eq_ignore_ascii_case("property_exists")
{
if let Some(arg_expr) = call.args.first() {
if let Some(var_name) = extract_var_name(&arg_expr.value) {
narrow_from_type_fn(ctx, bare, &var_name, db, is_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&arg_expr.value) {
narrow_prop_from_type_fn(ctx, bare, &obj, &prop, db, file, is_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&arg_expr.value, ctx, db, file)
{
narrow_static_prop_from_type_fn(ctx, bare, &fqcn, &prop, db, is_true);
}
if is_true && bare.eq_ignore_ascii_case("method_exists") {
if let Some(expr_key) =
extract_expr_guard_key(&arg_expr.value, ctx, db, file)
{
if let Some(method_arg) = call.args.get(1) {
if let ExprKind::String(method_name) = &method_arg.value.kind {
let method_lc = std::sync::Arc::from(
crate::util::php_ident_lowercase(method_name).as_str(),
);
ctx.method_exists_guards.insert((expr_key, method_lc));
}
}
}
}
}
} else if matches!(
bare.to_ascii_lowercase().as_str(),
"array_key_exists" | "key_exists"
) {
if let (Some(key_arg), Some(arr_arg)) = (call.args.first(), call.args.get(1)) {
let literal_key = match &key_arg.value.kind {
ExprKind::String(s) => Some(mir_types::atomic::ArrayKey::String(
std::sync::Arc::from(s.as_ref()),
)),
ExprKind::Int(i) => Some(mir_types::atomic::ArrayKey::Int(*i)),
_ => {
let key_ty = if let Some(name) = extract_var_name(&key_arg.value) {
Some(ctx.get_var(&name))
} else if let Some((obj, prop)) =
extract_any_prop_access(&key_arg.value)
{
Some(resolve_prop_current_type(ctx, &obj, &prop, db, file))
} else {
extract_static_prop_access(&key_arg.value, ctx, db, file).map(
|(fqcn, prop)| {
resolve_static_prop_current_type(ctx, &fqcn, &prop, db)
},
)
};
key_ty.and_then(|ty| match ty.types.as_slice() {
[Atomic::TLiteralString(s)] => {
Some(mir_types::atomic::ArrayKey::String(s.clone()))
}
[Atomic::TLiteralInt(i)] => {
Some(mir_types::atomic::ArrayKey::Int(*i))
}
_ => None,
})
}
};
if let Some(key) = literal_key {
if is_true {
if let Some(var_name) = extract_var_name(&arr_arg.value) {
let current = ctx.get_var(&var_name);
let narrowed = add_key_to_sealed_shapes(¤t, &key);
if narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) =
extract_any_prop_access(&arr_arg.value)
{
narrow_prop_array_key_exists(ctx, &obj, &prop, &key, db, file);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&arr_arg.value, ctx, db, file)
{
narrow_static_prop_array_key_exists(
ctx, &fqcn, &prop, &key, db,
);
} else if let Some((base, path)) =
collect_array_access_path(&arr_arg.value, ctx, db, file)
{
let current =
resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) =
narrow_shape_path_key_exists(¤t, &path, &key)
{
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some((target, is_parents)),
) = (
&key,
extract_class_implements_or_parents_arg(&arr_arg.value),
) {
let fqcn = crate::db::resolve_name(db, file, iface_name);
match &target {
ScalarArgTarget::Var(var_name) => {
let current = ctx.get_var(var_name);
let narrowed = if is_parents {
narrow_strict_subclass_of(
¤t,
&fqcn,
db,
&ctx.template_param_names,
)
} else {
narrow_instanceof_preserving_subtypes(
¤t,
&fqcn,
db,
&ctx.template_param_names,
)
};
set_narrowed(ctx, var_name, ¤t, narrowed, true);
}
ScalarArgTarget::Prop(obj, prop) => {
if is_parents {
narrow_prop_is_subclass_of(
ctx, obj, prop, &fqcn, db, file, true,
);
} else {
narrow_prop_instanceof(
ctx, obj, prop, &fqcn, db, file, true,
);
}
narrow_receiver_non_null_on_prop_match(ctx, obj, true);
}
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some(((static_fqcn, prop), is_parents)),
) = (
&key,
extract_class_implements_or_parents_static_prop_arg(
&arr_arg.value,
ctx,
db,
file,
),
) {
let fqcn = crate::db::resolve_name(db, file, iface_name);
if is_parents {
narrow_static_prop_is_subclass_of(
ctx,
&static_fqcn,
&prop,
&fqcn,
db,
true,
);
} else {
narrow_static_prop_instanceof(
ctx,
&static_fqcn,
&prop,
&fqcn,
db,
true,
);
}
}
} else {
if let Some(var_name) = extract_var_name(&arr_arg.value) {
let current = ctx.get_var(&var_name);
let narrowed = remove_key_from_sealed_shapes(¤t, &key);
set_narrowed(ctx, &var_name, ¤t, narrowed, true);
} else if let Some((obj, prop)) =
extract_any_prop_access(&arr_arg.value)
{
let current =
resolve_prop_current_type(ctx, &obj, &prop, db, file);
if !current.is_mixed() {
let narrowed =
remove_key_from_sealed_shapes(¤t, &key);
apply_prop_narrowed(
ctx, &obj, &prop, current, narrowed, true,
);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&arr_arg.value, ctx, db, file)
{
let current =
resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
if !current.is_mixed() {
let narrowed =
remove_key_from_sealed_shapes(¤t, &key);
apply_prop_narrowed(
ctx, &fqcn, &prop, current, narrowed, true,
);
}
} else if let Some((base, path)) =
collect_array_access_path(&arr_arg.value, ctx, db, file)
{
let current =
resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) =
narrow_shape_path_key_exists_false(¤t, &path, &key)
{
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some((target, is_parents)),
) = (
&key,
extract_class_implements_or_parents_arg(&arr_arg.value),
) {
if !is_parents {
let fqcn = crate::db::resolve_name(db, file, iface_name);
match &target {
ScalarArgTarget::Var(var_name) => {
let current = ctx.get_var(var_name);
let narrowed = filter_out_instanceof_match(
¤t, &fqcn, db,
);
set_narrowed(
ctx, var_name, ¤t, narrowed, true,
);
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_instanceof(
ctx, obj, prop, &fqcn, db, file, false,
);
}
}
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some(((static_fqcn, prop), is_parents)),
) = (
&key,
extract_class_implements_or_parents_static_prop_arg(
&arr_arg.value,
ctx,
db,
file,
),
) {
if !is_parents {
let fqcn = crate::db::resolve_name(db, file, iface_name);
narrow_static_prop_instanceof(
ctx,
&static_fqcn,
&prop,
&fqcn,
db,
false,
);
}
}
}
}
}
} else if matches!(
bare.to_ascii_lowercase().as_str(),
"str_contains" | "str_starts_with" | "str_ends_with"
) {
if is_true {
if let (Some(haystack_arg), Some(needle_arg)) =
(call.args.first(), call.args.get(1))
{
let needle_non_empty =
expr_is_nonempty_string_literal(&needle_arg.value, ctx, db, file);
if needle_non_empty {
match ScalarArgTarget::extract(&haystack_arg.value) {
Some(ScalarArgTarget::Var(var_name)) => {
let current = ctx.get_var(&var_name);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
if narrowed != current {
ctx.set_var(&var_name, narrowed);
}
}
}
Some(ScalarArgTarget::Prop(obj, prop)) => {
let current =
resolve_prop_current_type(ctx, &obj, &prop, db, file);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
apply_prop_narrowed(
ctx, &obj, &prop, current, narrowed, false,
);
}
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
}
None => {
if let Some((fqcn, prop)) = extract_static_prop_access(
&haystack_arg.value,
ctx,
db,
file,
) {
let current = resolve_static_prop_current_type(
ctx, &fqcn, &prop, db,
);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
apply_prop_narrowed(
ctx, &fqcn, &prop, current, narrowed, false,
);
}
}
}
}
}
}
}
} else if bare.eq_ignore_ascii_case("in_array") {
let strict = call
.args
.get(2)
.map(|a| is_truthy_bool_literal(&a.value))
.unwrap_or(false);
if let (Some(needle_arg), Some(haystack_arg)) =
(call.args.first(), call.args.get(1))
{
if let Some(var_name) = extract_var_name(&needle_arg.value) {
if let Some(haystack_ty) =
extract_haystack_type(&haystack_arg.value, ctx, db, file)
{
let current = ctx.get_var(&var_name);
let loose_safe = strict
|| in_array_loose_narrowing_is_safe(¤t, &haystack_ty);
if !current.is_mixed() && is_true && loose_safe {
let narrowed =
narrow_to_haystack_values(¤t, &haystack_ty);
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if !current.is_mixed() && !is_true && loose_safe {
let all_literals = !current.types.is_empty()
&& current.types.iter().all(|a| {
matches!(
a,
Atomic::TLiteralString(_) | Atomic::TLiteralInt(_)
)
});
if all_literals {
let narrowed = current
.filter(|a| !haystack_ty.types.iter().any(|h| h == a));
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
}
}
}
} else if let Some((obj, prop)) = extract_any_prop_access(&needle_arg.value)
{
if let Some(haystack_ty) =
extract_haystack_type(&haystack_arg.value, ctx, db, file)
{
if is_true {
let haystack_admits_null_loosely =
haystack_ty.types.iter().any(|a| {
matches!(a, Atomic::TLiteralInt(0))
|| matches!(a, Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0")
});
if strict || !haystack_admits_null_loosely {
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
}
}
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
let loose_safe = strict
|| in_array_loose_narrowing_is_safe(¤t, &haystack_ty);
if !current.is_mixed() && is_true && loose_safe {
let narrowed =
narrow_to_haystack_values(¤t, &haystack_ty);
if !narrowed.is_empty() {
apply_prop_narrowed(
ctx, &obj, &prop, current, narrowed, false,
);
}
} else if !current.is_mixed() && !is_true && loose_safe {
let all_literals = !current.types.is_empty()
&& current.types.iter().all(|a| {
matches!(
a,
Atomic::TLiteralString(_) | Atomic::TLiteralInt(_)
)
});
if all_literals {
let narrowed = current
.filter(|a| !haystack_ty.types.iter().any(|h| h == a));
if !narrowed.is_empty() {
apply_prop_narrowed(
ctx, &obj, &prop, current, narrowed, false,
);
}
}
}
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&needle_arg.value, ctx, db, file)
{
if let Some(haystack_ty) =
extract_haystack_type(&haystack_arg.value, ctx, db, file)
{
let current =
resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let loose_safe = strict
|| in_array_loose_narrowing_is_safe(¤t, &haystack_ty);
if !current.is_mixed() && is_true && loose_safe {
let narrowed =
narrow_to_haystack_values(¤t, &haystack_ty);
if !narrowed.is_empty() {
apply_prop_narrowed(
ctx, &fqcn, &prop, current, narrowed, false,
);
}
} else if !current.is_mixed() && !is_true && loose_safe {
let all_literals = !current.types.is_empty()
&& current.types.iter().all(|a| {
matches!(
a,
Atomic::TLiteralString(_) | Atomic::TLiteralInt(_)
)
});
if all_literals {
let narrowed = current
.filter(|a| !haystack_ty.types.iter().any(|h| h == a));
if !narrowed.is_empty() {
apply_prop_narrowed(
ctx, &fqcn, &prop, current, narrowed, false,
);
}
}
}
}
}
}
} else if bare.eq_ignore_ascii_case("is_a") {
if let (Some(obj_arg), Some(class_arg)) = (call.args.first(), call.args.get(1))
{
if let Some(var_name) = extract_var_name(&obj_arg.value) {
if let Some(class_name) = extract_class_fqcn_from_expr(
&class_arg.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
let allow_string = call
.args
.get(2)
.map(|a| is_truthy_bool_literal(&a.value))
.unwrap_or(false);
let current = ctx.get_var(&var_name);
if allow_string {
let narrowed = if is_true {
let (mut result, obj_part) =
partition_is_a_string_like(¤t, &class_name, db);
if !obj_part.is_empty() || current.is_mixed() {
let obj_src = if obj_part.is_empty() {
¤t
} else {
&obj_part
};
let obj_narrowed =
narrow_instanceof_preserving_subtypes(
obj_src,
&class_name,
db,
&ctx.template_param_names,
);
for atom in obj_narrowed.types.iter() {
result.add_type(atom.clone());
}
}
result
} else {
filter_out_is_a_string_match(¤t, &class_name, db)
};
set_narrowed(ctx, &var_name, ¤t, narrowed, false);
} else {
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(
¤t,
&class_name,
db,
&ctx.template_param_names,
)
} else {
filter_out_instanceof_match(¤t, &class_name, db)
};
set_narrowed(ctx, &var_name, ¤t, narrowed, true);
}
}
} else if let Some((obj, prop)) = extract_any_prop_access(&obj_arg.value) {
if let Some(class_name) = extract_class_fqcn_from_expr(
&class_arg.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
let allow_string = call
.args
.get(2)
.map(|a| is_truthy_bool_literal(&a.value))
.unwrap_or(false);
narrow_prop_is_a(
ctx,
&obj,
&prop,
&class_name,
allow_string,
db,
file,
is_true,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, is_true);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&obj_arg.value, ctx, db, file)
{
if let Some(class_name) = extract_class_fqcn_from_expr(
&class_arg.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
let allow_string = call
.args
.get(2)
.map(|a| is_truthy_bool_literal(&a.value))
.unwrap_or(false);
narrow_static_prop_is_a(
ctx,
&fqcn,
&prop,
&class_name,
allow_string,
db,
is_true,
);
}
}
}
} else if bare.eq_ignore_ascii_case("is_subclass_of") {
if let (Some(obj_arg), Some(class_arg)) = (call.args.first(), call.args.get(1))
{
if let Some(var_name) = extract_var_name(&obj_arg.value) {
if let Some(class_name) = extract_class_fqcn_from_expr(
&class_arg.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
let current = ctx.get_var(&var_name);
if is_true {
let narrowed = narrow_strict_subclass_of(
¤t,
&class_name,
db,
&ctx.template_param_names,
);
set_narrowed(ctx, &var_name, ¤t, narrowed, false);
}
}
} else if let Some((obj, prop)) = extract_any_prop_access(&obj_arg.value) {
if let Some(class_name) = extract_class_fqcn_from_expr(
&class_arg.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_prop_is_subclass_of(
ctx,
&obj,
&prop,
&class_name,
db,
file,
is_true,
);
narrow_receiver_non_null_on_prop_match(ctx, &obj, is_true);
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&obj_arg.value, ctx, db, file)
{
if let Some(class_name) = extract_class_fqcn_from_expr(
&class_arg.value,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
db,
file,
) {
narrow_static_prop_is_subclass_of(
ctx,
&fqcn,
&prop,
&class_name,
db,
is_true,
);
}
}
}
} else if apply_docblock_assertions(call, ctx, is_true, db, file, fn_name) {
} else if let Some(arg_expr) = call.args.first() {
if let Some(var_name) = extract_var_name(&arg_expr.value) {
narrow_from_type_fn(ctx, bare, &var_name, db, is_true);
} else if let Some((obj, prop)) = extract_any_prop_access(&arg_expr.value) {
narrow_prop_from_type_fn(ctx, bare, &obj, &prop, db, file, is_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&arg_expr.value, ctx, db, file)
{
narrow_static_prop_from_type_fn(ctx, bare, &fqcn, &prop, db, is_true);
}
}
}
}
ExprKind::MethodCall(mc) => {
if let Some(fqcn) = method_call_receiver_fqcn(&mc.object, ctx) {
if let ExprKind::Identifier(name) = &mc.method.kind {
let method_name_lower = crate::util::php_ident_lowercase(name);
if let Some(resolved) =
crate::call::method::resolve_method_from_db(db, &fqcn, &method_name_lower)
{
apply_method_docblock_assertions(
&mc.args, &resolved, ctx, is_true, db, file,
);
}
}
}
}
ExprKind::StaticMethodCall(smc) => {
if let Some(fqcn) = resolve_static_call_class_fqcn(&smc.class, ctx, db, file) {
if let ExprKind::Identifier(name) = &smc.method.kind {
let method_name_lower = crate::util::php_ident_lowercase(name);
if let Some(resolved) =
crate::call::method::resolve_method_from_db(db, &fqcn, &method_name_lower)
{
apply_method_docblock_assertions(
&smc.args, &resolved, ctx, is_true, db, file,
);
}
}
}
}
ExprKind::Isset(vars) => {
for var_expr in vars.iter() {
if let Some(var_name) = extract_var_name(var_expr) {
if is_true {
let current = ctx.get_var(&var_name);
ctx.set_var(&var_name, current.remove_null());
std::sync::Arc::make_mut(&mut ctx.assigned_vars)
.insert(mir_types::Name::from(var_name.as_str()));
} else if ctx.var_is_defined(&var_name) {
narrow_var_null(ctx, &var_name, true);
}
} else if is_true {
if let Some(target) = array_access_base_target(var_expr, ctx, db, file) {
narrow_container_non_null_non_false(ctx, &target, db, file);
}
narrow_isset_shape_key(var_expr, ctx, db, file);
if let Some((obj_var, prop)) = extract_any_prop_access(var_expr) {
let current = resolve_prop_current_type(ctx, &obj_var, &prop, db, file);
if !current.is_mixed() {
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &obj_var, &prop, current, narrowed, true);
}
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(var_expr, ctx, db, file)
{
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
if !current.is_mixed() {
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, true);
}
}
}
}
}
ExprKind::Empty(var_expr) => {
if let Some(var_name) = extract_var_name(var_expr) {
let current = ctx.get_var(&var_name);
let narrowed = if is_true {
current.narrow_to_falsy()
} else {
current.narrow_to_truthy()
};
if !narrowed.is_empty() {
ctx.set_var(&var_name, narrowed);
}
} else {
if !is_true {
if let Some(target) = array_access_base_target(var_expr, ctx, db, file) {
narrow_container_non_null_non_false(ctx, &target, db, file);
}
}
narrow_empty_shape_key(var_expr, ctx, is_true, db, file);
if let Some((obj_var, prop)) = extract_any_prop_access(var_expr) {
narrow_prop_loose_bool(ctx, &obj_var, &prop, db, file, !is_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, !is_true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(var_expr, ctx, db, file)
{
narrow_static_prop_loose_bool(ctx, &fqcn, &prop, db, !is_true);
}
}
}
ExprKind::Assign(a) if matches!(a.op, AssignOp::Assign | AssignOp::Coalesce) => {
if let Some(var_name) = extract_var_name(&a.target) {
let current = ctx.get_var(&var_name);
let mut narrowed = if is_true {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
if is_true {
narrowed.possibly_undefined = false;
}
if !narrowed.is_empty() {
ctx.set_var(&var_name, narrowed);
} else if !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
} else if let Some((obj_var, prop)) = extract_prop_access(&a.target) {
narrow_var_null(ctx, &obj_var, false);
let current = resolve_prop_current_type(ctx, &obj_var, &prop, db, file);
let mut narrowed = if is_true {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
if is_true {
narrowed.possibly_undefined = false;
}
apply_prop_narrowed(ctx, &obj_var, &prop, current, narrowed, true);
} else if let Some((fqcn, prop)) = extract_static_prop_access(&a.target, ctx, db, file)
{
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let mut narrowed = if is_true {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
if is_true {
narrowed.possibly_undefined = false;
}
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, true);
}
}
_ => {
if let Some(var_name) = extract_var_name(expr) {
let current = ctx.get_var(&var_name);
let narrowed = if is_true {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
if !narrowed.is_empty() {
ctx.set_var(&var_name, narrowed);
} else if !current.is_empty()
&& !current.is_mixed()
&& ctx.var_is_defined(&var_name)
{
ctx.diverges = true;
}
} else if let Some((obj_var, prop)) = extract_any_prop_access(expr) {
narrow_prop_loose_bool(ctx, &obj_var, &prop, db, file, is_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, is_true);
} else if let Some((fqcn, prop)) = extract_static_prop_access(expr, ctx, db, file) {
narrow_static_prop_loose_bool(ctx, &fqcn, &prop, db, is_true);
}
}
}
}
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,
ctx,
is_true,
db,
file,
)
}
fn apply_method_docblock_assertions(
call_args: &[php_ast::owned::Arg],
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,
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],
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 = if template_params.is_empty() {
None
} else {
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,
)
};
let mut applied = false;
for assertion in assertions
.iter()
.filter(|a| a.kind == expected_kind || (is_true && a.kind == AssertionKind::Assert))
{
if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
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())
.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 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_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
}
fn method_call_receiver_fqcn(
object: &php_ast::owned::Expr,
ctx: &FlowState,
) -> Option<std::sync::Arc<str>> {
let obj_var = extract_var_name(object)?;
let obj_ty = ctx.get_var(&obj_var);
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())),
_ => None,
}
}
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() || asserted.types.len() != 1 {
return current.clone();
}
match &asserted.types[0] {
Atomic::TNull => current.remove_null(),
Atomic::TFalse => current.remove_false(),
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => filter_out_instanceof_match(current, fqcn, db),
_ => current.clone(),
}
}
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 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()
}
#[allow(clippy::too_many_arguments)]
fn collect_instanceof(
expr: &php_ast::owned::Expr,
var_name: &mut Option<String>,
class_names: &mut Vec<String>,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> bool {
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
if let (Some(vn), Some(cn)) = (
extract_var_name(&b.left),
extract_class_name(&b.right, self_fqcn, parent_fqcn),
) {
let resolved = crate::db::resolve_name(db, file, &cn);
match var_name {
None => {
*var_name = Some(vn);
class_names.push(resolved);
true
}
Some(existing) if existing == &vn => {
class_names.push(resolved);
true
}
_ => false, }
} else {
false
}
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
collect_instanceof(
&b.left,
var_name,
class_names,
db,
file,
self_fqcn,
parent_fqcn,
) && collect_instanceof(
&b.right,
var_name,
class_names,
db,
file,
self_fqcn,
parent_fqcn,
)
}
ExprKind::Parenthesized(inner) => collect_instanceof(
inner,
var_name,
class_names,
db,
file,
self_fqcn,
parent_fqcn,
),
_ => false,
}
}
pub(crate) fn narrow_instanceof_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<String> {
if conditions.len() < 2 {
return None;
}
let self_fqcn = ctx.self_fqcn.as_deref();
let parent_fqcn = ctx.parent_fqcn.as_deref();
let mut var_name: Option<String> = None;
let mut class_names: Vec<String> = vec![];
let all_ok = conditions.iter().all(|cond| {
collect_instanceof(
cond,
&mut var_name,
&mut class_names,
db,
file,
self_fqcn,
parent_fqcn,
)
});
if !all_ok || class_names.len() < 2 {
return None;
}
let vn = var_name?;
let current = ctx.get_var(&vn);
let narrowed =
narrow_or_instanceof_union(¤t, &class_names, db, &ctx.template_param_names);
set_narrowed(ctx, &vn, ¤t, narrowed, true);
Some(vn)
}
fn collect_prop_instanceof(
expr: &php_ast::owned::Expr,
receiver: &mut Option<(String, String)>,
class_names: &mut Vec<String>,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> bool {
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
if let (Some((obj, prop)), Some(cn)) = (
extract_prop_access(&b.left),
extract_class_name(&b.right, self_fqcn, parent_fqcn),
) {
let resolved = crate::db::resolve_name(db, file, &cn);
match receiver {
None => {
*receiver = Some((obj, prop));
class_names.push(resolved);
true
}
Some((existing_obj, existing_prop))
if *existing_obj == obj && *existing_prop == prop =>
{
class_names.push(resolved);
true
}
_ => false, }
} else {
false
}
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
collect_prop_instanceof(
&b.left,
receiver,
class_names,
db,
file,
self_fqcn,
parent_fqcn,
) && collect_prop_instanceof(
&b.right,
receiver,
class_names,
db,
file,
self_fqcn,
parent_fqcn,
)
}
ExprKind::Parenthesized(inner) => collect_prop_instanceof(
inner,
receiver,
class_names,
db,
file,
self_fqcn,
parent_fqcn,
),
_ => false,
}
}
pub(crate) fn narrow_prop_instanceof_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(String, String)> {
if conditions.len() < 2 {
return None;
}
let self_fqcn = ctx.self_fqcn.as_deref();
let parent_fqcn = ctx.parent_fqcn.as_deref();
let mut receiver: Option<(String, String)> = None;
let mut class_names: Vec<String> = vec![];
let all_ok = conditions.iter().all(|cond| {
collect_prop_instanceof(
cond,
&mut receiver,
&mut class_names,
db,
file,
self_fqcn,
parent_fqcn,
)
});
if !all_ok || class_names.len() < 2 {
return None;
}
let (obj_var, prop) = receiver?;
let current = resolve_prop_current_type(ctx, &obj_var, &prop, db, file);
let narrowed =
narrow_or_instanceof_union(¤t, &class_names, db, &ctx.template_param_names);
apply_prop_narrowed(ctx, &obj_var, &prop, current, narrowed, true);
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, true);
Some((obj_var, prop))
}
#[allow(clippy::too_many_arguments)]
fn collect_static_prop_instanceof(
expr: &php_ast::owned::Expr,
receiver: &mut Option<(std::sync::Arc<str>, String)>,
class_names: &mut Vec<String>,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> bool {
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
if let (Some((fqcn, prop)), Some(cn)) = (
extract_static_prop_access_parts(
&b.left,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
),
extract_class_name(&b.right, self_fqcn, parent_fqcn),
) {
let resolved = crate::db::resolve_name(db, file, &cn);
match receiver {
None => {
*receiver = Some((fqcn, prop));
class_names.push(resolved);
true
}
Some((existing_fqcn, existing_prop))
if *existing_fqcn == fqcn && *existing_prop == prop =>
{
class_names.push(resolved);
true
}
_ => false, }
} else {
false
}
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
collect_static_prop_instanceof(
&b.left,
receiver,
class_names,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
) && collect_static_prop_instanceof(
&b.right,
receiver,
class_names,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
)
}
ExprKind::Parenthesized(inner) => collect_static_prop_instanceof(
inner,
receiver,
class_names,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
),
_ => false,
}
}
pub(crate) fn narrow_static_prop_instanceof_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if conditions.len() < 2 {
return None;
}
let self_fqcn = ctx.self_fqcn.as_deref();
let static_fqcn = ctx.static_fqcn.as_deref();
let parent_fqcn = ctx.parent_fqcn.as_deref();
let mut receiver: Option<(std::sync::Arc<str>, String)> = None;
let mut class_names: Vec<String> = vec![];
let all_ok = conditions.iter().all(|cond| {
collect_static_prop_instanceof(
cond,
&mut receiver,
&mut class_names,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
)
});
if !all_ok || class_names.len() < 2 {
return None;
}
let (fqcn, prop) = receiver?;
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let narrowed =
narrow_or_instanceof_union(¤t, &class_names, db, &ctx.template_param_names);
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, true);
Some((fqcn, prop))
}
const NARROWING_TYPE_FNS: &[&str] = &[
"is_string",
"is_int",
"is_integer",
"is_long",
"is_float",
"is_double",
"is_real",
"is_bool",
"is_null",
"is_array",
"array_is_list",
"is_object",
"is_callable",
"is_scalar",
"is_iterable",
"is_countable",
"is_resource",
"is_numeric",
"ctype_alpha",
"ctype_alnum",
"ctype_digit",
"ctype_lower",
"ctype_upper",
"ctype_punct",
"ctype_space",
"ctype_xdigit",
"ctype_print",
"ctype_graph",
"ctype_cntrl",
];
fn extract_type_fn_check(expr: &php_ast::owned::Expr) -> Option<(&str, String)> {
let ExprKind::FunctionCall(call) = &expr.kind else {
return None;
};
let ExprKind::Identifier(name) = &call.name.kind else {
return None;
};
let bare = name.as_ref().trim_start_matches('\\');
let canonical = NARROWING_TYPE_FNS
.iter()
.find(|f| f.eq_ignore_ascii_case(bare))?;
if call.args.len() != 1 {
return None;
}
let var_name = extract_var_name(&call.args[0].value)?;
Some((canonical, var_name))
}
fn extract_type_fn_check_prop(expr: &php_ast::owned::Expr) -> Option<(&str, String, String)> {
let ExprKind::FunctionCall(call) = &expr.kind else {
return None;
};
let ExprKind::Identifier(name) = &call.name.kind else {
return None;
};
let bare = name.as_ref().trim_start_matches('\\');
let canonical = NARROWING_TYPE_FNS
.iter()
.find(|f| f.eq_ignore_ascii_case(bare))?;
if call.args.len() != 1 {
return None;
}
let (obj, prop) = extract_prop_access(&call.args[0].value)?;
Some((canonical, obj, prop))
}
pub(crate) fn narrow_type_fn_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
) -> Option<String> {
if conditions.len() < 2 {
return None;
}
let mut var_name: Option<String> = None;
let mut fn_names: Vec<&str> = Vec::with_capacity(conditions.len());
for cond in conditions {
let (fn_name, vn) = extract_type_fn_check(cond)?;
match &var_name {
None => var_name = Some(vn),
Some(existing) if *existing == vn => {}
_ => return None, }
fn_names.push(fn_name);
}
let vn = var_name?;
let original = ctx.get_var(&vn);
let mut union_ty = Type::empty();
for fn_name in &fn_names {
let mut scratch = ctx.branch();
scratch.set_var(&vn, original.clone());
narrow_from_type_fn(&mut scratch, fn_name, &vn, db, true);
union_ty.merge_with(&scratch.get_var(&vn));
}
if !union_ty.is_empty() {
ctx.set_var(&vn, union_ty);
}
Some(vn)
}
pub(crate) fn narrow_prop_type_fn_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(String, String)> {
if conditions.len() < 2 {
return None;
}
let mut receiver: Option<(String, String)> = None;
let mut fn_names: Vec<&str> = Vec::with_capacity(conditions.len());
for cond in conditions {
let (fn_name, obj, prop) = extract_type_fn_check_prop(cond)?;
match &receiver {
None => receiver = Some((obj, prop)),
Some((existing_obj, existing_prop))
if *existing_obj == obj && *existing_prop == prop => {}
_ => return None, }
fn_names.push(fn_name);
}
let (obj_var, prop) = receiver?;
let original = resolve_prop_current_type(ctx, &obj_var, &prop, db, file);
let mut union_ty = Type::empty();
for fn_name in &fn_names {
let mut scratch = ctx.branch();
scratch.set_prop_refined(&obj_var, &prop, original.clone());
narrow_prop_from_type_fn(&mut scratch, fn_name, &obj_var, &prop, db, file, true);
union_ty.merge_with(&resolve_prop_current_type(
&scratch, &obj_var, &prop, db, file,
));
}
if !union_ty.is_empty() {
apply_prop_narrowed(ctx, &obj_var, &prop, original, union_ty, true);
if !fn_names.iter().any(|f| f.eq_ignore_ascii_case("is_null")) {
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, true);
}
}
Some((obj_var, prop))
}
fn extract_type_fn_check_static_prop(
expr: &php_ast::owned::Expr,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> Option<(&'static str, std::sync::Arc<str>, String)> {
let ExprKind::FunctionCall(call) = &expr.kind else {
return None;
};
let ExprKind::Identifier(name) = &call.name.kind else {
return None;
};
let bare = name.as_ref().trim_start_matches('\\');
let canonical = NARROWING_TYPE_FNS
.iter()
.find(|f| f.eq_ignore_ascii_case(bare))?;
if call.args.len() != 1 {
return None;
}
let (fqcn, prop) = extract_static_prop_access_parts(
&call.args[0].value,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
)?;
Some((canonical, fqcn, prop))
}
pub(crate) fn narrow_static_prop_type_fn_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if conditions.len() < 2 {
return None;
}
let self_fqcn = ctx.self_fqcn.as_deref();
let static_fqcn = ctx.static_fqcn.as_deref();
let parent_fqcn = ctx.parent_fqcn.as_deref();
let mut receiver: Option<(std::sync::Arc<str>, String)> = None;
let mut fn_names: Vec<&str> = Vec::with_capacity(conditions.len());
for cond in conditions {
let (fn_name, fqcn, prop) =
extract_type_fn_check_static_prop(cond, db, file, self_fqcn, static_fqcn, parent_fqcn)?;
match &receiver {
None => receiver = Some((fqcn, prop)),
Some((existing_fqcn, existing_prop))
if *existing_fqcn == fqcn && *existing_prop == prop => {}
_ => return None, }
fn_names.push(fn_name);
}
let (fqcn, prop) = receiver?;
let original = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let mut union_ty = Type::empty();
for fn_name in &fn_names {
let mut scratch = ctx.branch();
scratch.set_prop_refined(&fqcn, &prop, original.clone());
narrow_static_prop_from_type_fn(&mut scratch, fn_name, &fqcn, &prop, db, true);
union_ty.merge_with(&resolve_static_prop_current_type(
&scratch, &fqcn, &prop, db,
));
}
if !union_ty.is_empty() {
apply_prop_narrowed(ctx, &fqcn, &prop, original, union_ty, true);
}
Some((fqcn, prop))
}
fn is_a_or_subclass_of_call_receiver(expr: &php_ast::owned::Expr) -> Option<&php_ast::owned::Expr> {
let ExprKind::FunctionCall(call) = &expr.kind else {
return None;
};
let ExprKind::Identifier(name) = &call.name.kind else {
return None;
};
let bare = name.as_ref().trim_start_matches('\\');
if !(bare.eq_ignore_ascii_case("is_a") || bare.eq_ignore_ascii_case("is_subclass_of")) {
return None;
}
Some(&call.args.first()?.value)
}
fn single_leaf_disjunct_var(expr: &php_ast::owned::Expr) -> Option<String> {
let expr = peel_parens(expr);
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => extract_var_name(&b.left),
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
let l = single_leaf_disjunct_var(&b.left)?;
let r = single_leaf_disjunct_var(&b.right)?;
(l == r).then_some(l)
}
_ => extract_type_fn_check(expr)
.map(|(_, vn)| vn)
.or_else(|| extract_var_name(is_a_or_subclass_of_call_receiver(expr)?)),
}
}
pub(crate) fn narrow_mixed_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<String> {
if conditions.len() < 2 {
return None;
}
let mut var_name: Option<String> = None;
for cond in conditions {
let vn = single_leaf_disjunct_var(cond)?;
match &var_name {
None => var_name = Some(vn),
Some(existing) if *existing == vn => {}
_ => return None, }
}
let vn = var_name?;
let original = ctx.get_var(&vn);
let mut union_ty = Type::empty();
for cond in conditions {
let mut scratch = ctx.branch();
scratch.set_var(&vn, original.clone());
narrow_from_condition(cond, &mut scratch, true, db, file);
union_ty.merge_with(&scratch.get_var(&vn));
}
if !union_ty.is_empty() {
ctx.set_var(&vn, union_ty);
}
Some(vn)
}
fn single_leaf_disjunct_prop(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
let expr = peel_parens(expr);
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => extract_prop_access(&b.left),
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
let l = single_leaf_disjunct_prop(&b.left)?;
let r = single_leaf_disjunct_prop(&b.right)?;
(l == r).then_some(l)
}
_ => extract_type_fn_check_prop(expr)
.map(|(_, obj, prop)| (obj, prop))
.or_else(|| extract_prop_access(is_a_or_subclass_of_call_receiver(expr)?)),
}
}
fn disjunct_contains_is_null_prop_leaf(expr: &php_ast::owned::Expr) -> bool {
let expr = peel_parens(expr);
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
disjunct_contains_is_null_prop_leaf(&b.left)
|| disjunct_contains_is_null_prop_leaf(&b.right)
}
_ => extract_type_fn_check_prop(expr)
.is_some_and(|(fn_name, ..)| fn_name.eq_ignore_ascii_case("is_null")),
}
}
pub(crate) fn narrow_mixed_prop_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(String, String)> {
if conditions.len() < 2 {
return None;
}
let mut receiver: Option<(String, String)> = None;
for cond in conditions {
let (obj, prop) = single_leaf_disjunct_prop(cond)?;
match &receiver {
None => receiver = Some((obj, prop)),
Some((existing_obj, existing_prop))
if *existing_obj == obj && *existing_prop == prop => {}
_ => return None, }
}
let (obj_var, prop) = receiver?;
let original = resolve_prop_current_type(ctx, &obj_var, &prop, db, file);
let mut union_ty = Type::empty();
for cond in conditions {
let mut scratch = ctx.branch();
scratch.set_prop_refined(&obj_var, &prop, original.clone());
narrow_from_condition(cond, &mut scratch, true, db, file);
union_ty.merge_with(&resolve_prop_current_type(
&scratch, &obj_var, &prop, db, file,
));
}
if !union_ty.is_empty() {
apply_prop_narrowed(ctx, &obj_var, &prop, original, union_ty, true);
if !conditions
.iter()
.any(|c| disjunct_contains_is_null_prop_leaf(c))
{
narrow_receiver_non_null_on_prop_match(ctx, &obj_var, true);
}
}
Some((obj_var, prop))
}
#[allow(clippy::too_many_arguments)]
fn single_leaf_disjunct_static_prop(
expr: &php_ast::owned::Expr,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> Option<(std::sync::Arc<str>, String)> {
let expr = peel_parens(expr);
match &expr.kind {
ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
extract_static_prop_access_parts(&b.left, db, file, self_fqcn, static_fqcn, parent_fqcn)
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
let l = single_leaf_disjunct_static_prop(
&b.left,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
)?;
let r = single_leaf_disjunct_static_prop(
&b.right,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
)?;
(l == r).then_some(l)
}
_ => extract_type_fn_check_static_prop(expr, db, file, self_fqcn, static_fqcn, parent_fqcn)
.map(|(_, fqcn, prop)| (fqcn, prop))
.or_else(|| {
extract_static_prop_access_parts(
is_a_or_subclass_of_call_receiver(expr)?,
db,
file,
self_fqcn,
static_fqcn,
parent_fqcn,
)
}),
}
}
pub(crate) fn narrow_mixed_static_prop_disjuncts(
conditions: &[&php_ast::owned::Expr],
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if conditions.len() < 2 {
return None;
}
let self_fqcn = ctx.self_fqcn.as_deref();
let static_fqcn = ctx.static_fqcn.as_deref();
let parent_fqcn = ctx.parent_fqcn.as_deref();
let mut receiver: Option<(std::sync::Arc<str>, String)> = None;
for cond in conditions {
let (fqcn, prop) =
single_leaf_disjunct_static_prop(cond, db, file, self_fqcn, static_fqcn, parent_fqcn)?;
match &receiver {
None => receiver = Some((fqcn, prop)),
Some((existing_fqcn, existing_prop))
if *existing_fqcn == fqcn && *existing_prop == prop => {}
_ => return None, }
}
let (fqcn, prop) = receiver?;
let original = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let mut union_ty = Type::empty();
for cond in conditions {
let mut scratch = ctx.branch();
scratch.set_prop_refined(&fqcn, &prop, original.clone());
narrow_from_condition(cond, &mut scratch, true, db, file);
union_ty.merge_with(&resolve_static_prop_current_type(
&scratch, &fqcn, &prop, db,
));
}
if !union_ty.is_empty() {
apply_prop_narrowed(ctx, &fqcn, &prop, original, union_ty, true);
}
Some((fqcn, prop))
}
fn narrow_or_instanceof_true(
left: &php_ast::owned::Expr,
right: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) {
if narrow_instanceof_disjuncts(&[left, right], ctx, db, file).is_none()
&& narrow_type_fn_disjuncts(&[left, right], ctx, db).is_none()
&& narrow_prop_instanceof_disjuncts(&[left, right], ctx, db, file).is_none()
&& narrow_prop_type_fn_disjuncts(&[left, right], ctx, db, file).is_none()
&& narrow_static_prop_instanceof_disjuncts(&[left, right], ctx, db, file).is_none()
&& narrow_static_prop_type_fn_disjuncts(&[left, right], ctx, db, file).is_none()
&& narrow_mixed_disjuncts(&[left, right], ctx, db, file).is_none()
&& narrow_mixed_prop_disjuncts(&[left, right], ctx, db, file).is_none()
{
narrow_mixed_static_prop_disjuncts(&[left, right], ctx, db, file);
}
}
fn narrow_or_isset_true(
left: &php_ast::owned::Expr,
right: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) {
if let ExprKind::UnaryPrefix(u) = &left.kind {
if u.op == UnaryPrefixOp::BooleanNot {
if let ExprKind::Isset(_) = &u.operand.kind {
let pre = ctx.branch();
let mut not_set_branch = ctx.branch();
narrow_from_condition(left, &mut not_set_branch, true, db, file);
let mut set_branch = ctx.branch();
narrow_from_condition(left, &mut set_branch, false, db, file);
if !set_branch.diverges {
narrow_from_condition(right, &mut set_branch, true, db, file);
}
*ctx = FlowState::merge_branches(&pre, set_branch, Some(not_set_branch));
}
}
}
}
fn project_type_params_onto_subclass(
db: &dyn MirDatabase,
atom_fqcn: &str,
atom_type_params: &[Type],
class_name: &str,
) -> std::sync::Arc<[Type]> {
let Some(class_own_tps) = crate::db::class_template_params(db, class_name) else {
return mir_types::union::empty_type_params();
};
if class_own_tps.is_empty() {
return mir_types::union::empty_type_params();
}
let Some(atom_own_tps) = crate::db::class_template_params(db, atom_fqcn) else {
return mir_types::union::empty_type_params();
};
let here = crate::db::Fqcn::from_str(db, class_name);
let Some(class) = crate::db::find_class_like(db, here) else {
return mir_types::union::empty_type_params();
};
let explicit_args: Option<&[Type]> = if class
.parent()
.is_some_and(|p| p.as_ref().eq_ignore_ascii_case(atom_fqcn))
{
Some(class.extends_type_args())
} else {
class
.implements_type_args()
.iter()
.chain(class.interface_extends_type_args())
.find(|(iface, _)| iface.as_ref().eq_ignore_ascii_case(atom_fqcn))
.map(|(_, args)| args.as_slice())
};
let mut result = vec![Type::mixed(); class_own_tps.len()];
let mut any_bound = false;
if let Some(args) = explicit_args.filter(|a| !a.is_empty()) {
for (idx, given_ty) in atom_type_params.iter().enumerate() {
let Some(arg_expr) = args.get(idx) else {
continue;
};
let Some(bare_name) = bare_named_type(arg_expr) else {
continue;
};
if let Some(pos) = class_own_tps
.iter()
.position(|tp| tp.name.as_str() == bare_name)
{
result[pos] = given_ty.clone();
any_bound = true;
}
}
} else if class_own_tps.len() == atom_own_tps.len() {
result = atom_type_params.to_vec();
any_bound = !result.is_empty();
}
if any_bound {
mir_types::union::vec_to_type_params(result)
} else {
mir_types::union::empty_type_params()
}
}
fn bare_named_type(ty: &Type) -> Option<&str> {
if ty.types.len() != 1 {
return None;
}
match &ty.types[0] {
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty() && !fqcn.contains('\\') =>
{
Some(fqcn.as_ref())
}
Atomic::TTemplateParam { name, .. } => Some(name.as_ref()),
_ => None,
}
}
fn narrow_instanceof_preserving_subtypes(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
template_param_names: &rustc_hash::FxHashSet<mir_types::Name>,
) -> Type {
let narrowed_ty = Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: mir_types::union::empty_type_params(),
};
if current.is_empty() || current.is_mixed_not_template() {
return Type::single(narrowed_ty);
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
match atomic {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if named_object_matches_instanceof(fqcn, class_name, db) =>
{
result.add_type(atomic.clone());
}
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty()
&& !fqcn.contains('\\')
&& template_param_names.contains(fqcn) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TTemplateParam { .. } => {
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TObject | Atomic::TMixed => result.add_type(narrowed_ty.clone()),
Atomic::TIntersection { parts } => {
let already_covered = parts.iter().any(|p| {
p.types.iter().any(|a| {
matches!(a, Atomic::TNamedObject { fqcn, .. }
if named_object_matches_instanceof(fqcn, class_name, db))
})
});
if already_covered {
result.add_type(atomic.clone());
} else {
let projected_atom = parts.iter().find_map(|p| {
p.types.iter().find_map(|a| match a {
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& named_object_matches_instanceof(class_name, fqcn, db) =>
{
Some(Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: project_type_params_onto_subclass(
db,
fqcn,
type_params,
class_name,
),
})
}
_ => None,
})
});
let mut new_parts: Vec<Type> = parts.iter().cloned().collect();
new_parts.push(Type::single(
projected_atom.unwrap_or_else(|| narrowed_ty.clone()),
));
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(new_parts),
});
}
}
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& named_object_matches_instanceof(class_name, fqcn, db) =>
{
let projected =
project_type_params_onto_subclass(db, fqcn, type_params, class_name);
result.add_type(Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: projected,
});
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if named_object_matches_instanceof(class_name, fqcn, db) =>
{
result.add_type(narrowed_ty.clone());
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if classes_can_coexist(fqcn, class_name, db) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TClosure { .. } if class_name.eq_ignore_ascii_case("Closure") => {
result.add_type(atomic.clone());
}
_ => {}
}
}
result
}
fn narrow_or_instanceof_union(
current: &Type,
class_names: &[String],
db: &dyn MirDatabase,
template_param_names: &rustc_hash::FxHashSet<mir_types::Name>,
) -> Type {
let class_atom = |cn: &str| Atomic::TNamedObject {
fqcn: cn.into(),
type_params: mir_types::union::empty_type_params(),
};
if current.is_empty() || current.is_mixed_not_template() {
let mut out = Type::empty();
for cn in class_names {
out.add_type(class_atom(cn));
}
return out;
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
match atomic {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if class_names
.iter()
.any(|cn| named_object_matches_instanceof(fqcn, cn, db)) =>
{
result.add_type(atomic.clone());
}
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty()
&& !fqcn.contains('\\')
&& template_param_names.contains(fqcn) =>
{
let mut classes = Type::empty();
for cn in class_names {
classes.add_type(class_atom(cn));
}
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![Type::single(atomic.clone()), classes]),
});
}
Atomic::TTemplateParam { .. } => {
let mut classes = Type::empty();
for cn in class_names {
classes.add_type(class_atom(cn));
}
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![Type::single(atomic.clone()), classes]),
});
}
Atomic::TObject | Atomic::TMixed => {
for cn in class_names {
result.add_type(class_atom(cn));
}
}
Atomic::TClosure { .. }
if class_names
.iter()
.any(|cn| cn.eq_ignore_ascii_case("Closure")) =>
{
result.add_type(atomic.clone());
}
Atomic::TIntersection { parts } => {
let mut remaining = Type::empty();
for cn in class_names {
let already_covered = parts.iter().any(|p| {
p.types.iter().any(|a| {
matches!(a, Atomic::TNamedObject { fqcn, .. }
if named_object_matches_instanceof(fqcn, cn, db))
})
});
if !already_covered {
let projected_atom = parts.iter().find_map(|p| {
p.types.iter().find_map(|a| match a {
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& named_object_matches_instanceof(cn, fqcn, db) =>
{
Some(Atomic::TNamedObject {
fqcn: cn.as_str().into(),
type_params: project_type_params_onto_subclass(
db,
fqcn,
type_params,
cn,
),
})
}
_ => None,
})
});
remaining.add_type(projected_atom.unwrap_or_else(|| class_atom(cn)));
}
}
if remaining.is_empty() {
result.add_type(atomic.clone());
} else {
let mut new_parts: Vec<Type> = parts.iter().cloned().collect();
new_parts.push(remaining);
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(new_parts),
});
}
}
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& class_names
.iter()
.any(|cn| named_object_matches_instanceof(cn, fqcn, db)) =>
{
for cn in class_names {
if named_object_matches_instanceof(cn, fqcn, db) {
let projected =
project_type_params_onto_subclass(db, fqcn, type_params, cn);
result.add_type(Atomic::TNamedObject {
fqcn: cn.as_str().into(),
type_params: projected,
});
}
}
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if class_names
.iter()
.any(|cn| named_object_matches_instanceof(cn, fqcn, db)) =>
{
for cn in class_names {
if named_object_matches_instanceof(cn, fqcn, db) {
result.add_type(class_atom(cn));
}
}
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => {
let mut classes = Type::empty();
for cn in class_names {
if classes_can_coexist(fqcn, cn, db) {
classes.add_type(class_atom(cn));
}
}
if !classes.is_empty() {
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![Type::single(atomic.clone()), classes]),
});
}
}
_ => {}
}
}
result
}
fn classes_can_coexist(a: &str, b: &str, db: &dyn MirDatabase) -> bool {
crate::db::class_kind(db, a).is_some_and(|k| k.is_interface)
|| crate::db::class_kind(db, b).is_some_and(|k| k.is_interface)
}
fn filter_out_instanceof_match(current: &Type, class_name: &str, db: &dyn MirDatabase) -> Type {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => !named_object_matches_instanceof(fqcn, class_name, db),
Atomic::TClosure { .. } => !class_name.eq_ignore_ascii_case("Closure"),
Atomic::TIntersection { parts } => !parts.iter().any(|part| {
part.types.iter().any(|inner| match inner {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => named_object_matches_instanceof(fqcn, class_name, db),
_ => false,
})
}),
_ => true,
})
}
fn named_object_matches_instanceof(fqcn: &str, class_name: &str, db: &dyn MirDatabase) -> bool {
fqcn == class_name || crate::db::extends_or_implements(db, fqcn, class_name)
}
fn partition_is_a_string_like(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
) -> (Type, Type) {
let mut string_part = Type::empty();
string_part.possibly_undefined = current.possibly_undefined;
string_part.from_docblock = current.from_docblock;
let mut obj_part = Type::empty();
for atom in ¤t.types {
if let Atomic::TClassString(Some(name)) = atom {
if named_object_matches_instanceof(name, class_name, db)
|| classes_can_coexist(name, class_name, db)
{
string_part.add_type(atom.clone());
}
} else if atom.is_string() {
string_part.add_type(atom.clone());
} else {
obj_part.add_type(atom.clone());
}
}
(string_part, obj_part)
}
fn filter_out_is_a_string_match(current: &Type, class_name: &str, db: &dyn MirDatabase) -> Type {
filter_out_instanceof_match(current, class_name, db).filter(|t| {
!matches!(t, Atomic::TClassString(Some(name)) if named_object_matches_instanceof(name, class_name, db))
})
}
fn narrow_strict_subclass_of(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
template_param_names: &rustc_hash::FxHashSet<mir_types::Name>,
) -> Type {
let narrowed_ty = Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: mir_types::union::empty_type_params(),
};
if current.is_empty() || current.is_mixed_not_template() {
return Type::single(narrowed_ty);
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
match atomic {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if crate::db::extends_or_implements(db, fqcn.as_ref(), class_name)
&& fqcn.as_ref() != class_name =>
{
result.add_type(atomic.clone());
}
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty()
&& !fqcn.contains('\\')
&& template_param_names.contains(fqcn) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TTemplateParam { .. } => {
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TObject | Atomic::TMixed => result.add_type(narrowed_ty.clone()),
Atomic::TIntersection { parts } => {
let already_covered = parts.iter().any(|p| {
p.types.iter().any(|a| {
matches!(a, Atomic::TNamedObject { fqcn, .. }
if crate::db::extends_or_implements(db, fqcn.as_ref(), class_name)
&& fqcn.as_ref() != class_name)
})
});
if already_covered {
result.add_type(atomic.clone());
} else {
let mut new_parts: Vec<Type> = parts.iter().cloned().collect();
new_parts.push(Type::single(narrowed_ty.clone()));
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(new_parts),
});
}
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if classes_can_coexist(fqcn, class_name, db) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
_ => {}
}
}
result
}
fn is_truthy_bool_literal(expr: &php_ast::owned::Expr) -> bool {
matches!(expr.kind, php_ast::owned::ExprKind::Bool(true))
}
fn expr_is_nonempty_string_literal(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> bool {
match &expr.kind {
ExprKind::String(s) => !s.is_empty(),
_ => match ScalarArgTarget::extract(expr) {
Some(ScalarArgTarget::Var(name)) => {
matches!(ctx.get_var(&name).types.as_slice(), [Atomic::TLiteralString(s)] if !s.is_empty())
}
Some(ScalarArgTarget::Prop(obj, prop)) => matches!(
resolve_prop_current_type(ctx, &obj, &prop, db, file)
.types
.as_slice(),
[Atomic::TLiteralString(s)] if !s.is_empty()
),
None => match extract_static_prop_access(expr, ctx, db, file) {
Some((fqcn, prop)) => matches!(
resolve_static_prop_current_type(ctx, &fqcn, &prop, db)
.types
.as_slice(),
[Atomic::TLiteralString(s)] if !s.is_empty()
),
None => false,
},
},
}
}
fn narrow_from_false_comparable_call(
expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
is_false: bool,
) {
let ExprKind::FunctionCall(call) = &expr.kind else {
return;
};
let fn_name_opt: Option<&str> = match &call.name.kind {
ExprKind::Identifier(name) => Some(name.as_ref()),
_ => None,
};
let Some(fn_name) = fn_name_opt else {
return;
};
let bare = fn_name.trim_start_matches('\\');
if matches!(
bare.to_ascii_lowercase().as_str(),
"strpos"
| "stripos"
| "strrpos"
| "strripos"
| "mb_strpos"
| "mb_stripos"
| "mb_strrpos"
| "mb_strripos"
) {
if !is_false {
if let (Some(haystack_arg), Some(needle_arg)) = (call.args.first(), call.args.get(1)) {
let needle_non_empty =
expr_is_nonempty_string_literal(&needle_arg.value, ctx, db, file);
if needle_non_empty {
match ScalarArgTarget::extract(&haystack_arg.value) {
Some(ScalarArgTarget::Var(var_name)) => {
let current = ctx.get_var(&var_name);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
if narrowed != current {
ctx.set_var(&var_name, narrowed);
}
}
}
Some(ScalarArgTarget::Prop(obj, prop)) => {
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
}
}
None => {
if let Some((fqcn, prop)) =
extract_static_prop_access(&haystack_arg.value, ctx, db, file)
{
let current =
resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
apply_prop_narrowed(
ctx, &fqcn, &prop, current, narrowed, false,
);
}
}
}
}
}
}
}
} else if bare.eq_ignore_ascii_case("array_search") {
let strict = call
.args
.get(2)
.map(|a| is_truthy_bool_literal(&a.value))
.unwrap_or(false);
if let (Some(needle_arg), Some(haystack_arg)) = (call.args.first(), call.args.get(1)) {
if let Some(target) = ScalarArgTarget::extract(&needle_arg.value) {
if let Some(haystack_ty) = extract_haystack_type(&haystack_arg.value, ctx, db, file)
{
if !is_false {
if let ScalarArgTarget::Prop(obj, _) = &target {
let haystack_admits_null_loosely =
haystack_ty.types.iter().any(|a| {
matches!(a, Atomic::TLiteralInt(0))
|| matches!(a, Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0")
});
if strict || !haystack_admits_null_loosely {
narrow_receiver_non_null_on_prop_match(ctx, obj, true);
}
}
}
let current = match &target {
ScalarArgTarget::Var(name) => ctx.get_var(name),
ScalarArgTarget::Prop(obj, prop) => {
resolve_prop_current_type(ctx, obj, prop, db, file)
}
};
let loose_safe =
strict || in_array_loose_narrowing_is_safe(¤t, &haystack_ty);
if !current.is_mixed() && loose_safe {
let narrowed = if !is_false {
let narrowed = narrow_to_haystack_values(¤t, &haystack_ty);
(!narrowed.is_empty() && narrowed != current).then_some(narrowed)
} else {
let all_literals = !current.types.is_empty()
&& current.types.iter().all(|a| {
matches!(a, Atomic::TLiteralString(_) | Atomic::TLiteralInt(_))
});
all_literals
.then(|| {
current.filter(|a| !haystack_ty.types.iter().any(|h| h == a))
})
.filter(|narrowed| !narrowed.is_empty() && *narrowed != current)
};
if let Some(narrowed) = narrowed {
match target {
ScalarArgTarget::Var(name) => ctx.set_var(&name, narrowed),
ScalarArgTarget::Prop(obj, prop) => {
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false)
}
}
}
}
}
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&needle_arg.value, ctx, db, file)
{
if let Some(haystack_ty) = extract_haystack_type(&haystack_arg.value, ctx, db, file)
{
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
let loose_safe =
strict || in_array_loose_narrowing_is_safe(¤t, &haystack_ty);
if !current.is_mixed() && loose_safe {
let narrowed = if !is_false {
let narrowed = narrow_to_haystack_values(¤t, &haystack_ty);
(!narrowed.is_empty() && narrowed != current).then_some(narrowed)
} else {
let all_literals = !current.types.is_empty()
&& current.types.iter().all(|a| {
matches!(a, Atomic::TLiteralString(_) | Atomic::TLiteralInt(_))
});
all_literals
.then(|| {
current.filter(|a| !haystack_ty.types.iter().any(|h| h == a))
})
.filter(|narrowed| !narrowed.is_empty() && *narrowed != current)
};
if let Some(narrowed) = narrowed {
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
}
}
}
}
}
fn set_narrowed(
ctx: &mut FlowState,
name: &str,
current: &Type,
narrowed: Type,
mark_diverges: bool,
) {
if !narrowed.is_empty() {
ctx.set_var(name, narrowed);
} else if mark_diverges && !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
}
pub(crate) fn resolve_prop_current_type(
ctx: &FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
) -> Type {
if let Some(refined) = ctx.get_prop_refined(obj_var, prop) {
return refined.clone();
}
let obj_ty = ctx.get_var(obj_var);
let mut prop_ty = mir_types::Type::mixed();
'outer: for atomic in &obj_ty.types {
if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop) {
if let Some(ty) = p_def.ty.as_deref() {
prop_ty = ty.clone();
break 'outer;
}
}
} else if let mir_types::Atomic::TSelf { fqcn }
| mir_types::Atomic::TStaticObject { fqcn }
| mir_types::Atomic::TParent { fqcn } = atomic
{
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop) {
if let Some(ty) = p_def.ty.as_deref() {
prop_ty = ty.clone();
break 'outer;
}
}
}
}
if prop_ty.is_mixed() && obj_var == "this" {
if let Some(fqcn) = ctx.self_fqcn.as_ref() {
let resolved = crate::db::resolve_name(db, file, fqcn.as_ref());
let here = crate::db::Fqcn::from_str(db, &resolved);
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop) {
if let Some(ty) = p_def.ty.as_deref() {
prop_ty = ty.clone();
}
}
}
}
prop_ty
}
fn narrow_prop_null(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_null: bool,
) {
if !ctx.get_var(obj_var).is_nullable() {
narrow_prop_null_with_divergence(ctx, obj_var, prop, db, file, is_null, true);
return;
}
narrow_nullsafe_prop_null(ctx, obj_var, prop, db, file, is_null);
}
fn narrow_prop_null_with_divergence(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_null: bool,
mark_diverges: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let narrowed = if is_null {
current.narrow_to_null()
} else {
current.remove_null()
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn narrow_nullsafe_prop_null(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_null: bool,
) {
narrow_prop_null_with_divergence(ctx, obj_var, prop, db, file, is_null, !is_null);
if !is_null {
narrow_var_null(ctx, obj_var, false);
}
}
fn narrow_nullsafe_method_call_null(
ctx: &mut FlowState,
mc: &php_ast::owned::MethodCallExpr,
db: &dyn MirDatabase,
is_null: bool,
) {
let Some(obj_var) = extract_var_name(&mc.object) else {
return;
};
let ExprKind::Identifier(method_name) = &mc.method.kind else {
return;
};
let obj_ty = ctx.get_var(&obj_var);
let non_null_atoms: Vec<&Atomic> = obj_ty
.types
.iter()
.filter(|t| !matches!(t, Atomic::TNull))
.collect();
let fqcn = match non_null_atoms.as_slice() {
[Atomic::TNamedObject { fqcn, .. }] => *fqcn,
_ => return,
};
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
let Some((_, method)) = crate::db::find_method_in_chain(db, here, method_name.as_ref()) else {
return;
};
let Some(return_ty) = method.return_type.as_deref() else {
return;
};
if return_ty.is_mixed()
|| return_ty.contains(|t| matches!(t, Atomic::TNull | Atomic::TTemplateParam { .. }))
{
return;
}
narrow_var_null(ctx, &obj_var, is_null);
}
pub(crate) fn resolve_static_prop_current_type(
ctx: &FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
) -> Type {
if let Some(refined) = ctx.get_prop_refined(fqcn, prop) {
return refined.clone();
}
let here = crate::db::Fqcn::from_str(db, fqcn);
crate::db::find_property_in_chain(db, here, prop)
.and_then(|(_, p)| p.ty.as_deref().cloned())
.unwrap_or_else(mir_types::Type::mixed)
}
fn narrow_static_prop_null(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
is_null: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let narrowed = if is_null {
current.narrow_to_null()
} else {
current.remove_null()
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
fn narrow_static_prop_loose_null(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
is_null: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let narrowed = if is_null {
current.narrow_to_falsy()
} else {
current.remove_null()
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
fn narrow_static_prop_literal_string(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
value: &str,
is_value: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let narrowed = literal_string_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_literal_int(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
value: i64,
is_value: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let narrowed = literal_int_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_loose_int(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
value: i64,
is_value: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.types.is_empty()
|| !current
.types
.iter()
.all(|a| atom_safe_for_loose_int_narrowing(a, value))
{
return;
}
let narrowed = literal_int_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_bool(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
value: bool,
is_value: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let narrowed = bool_narrow_type(¤t, value, is_value);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
fn narrow_static_prop_loose_bool(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
want_truthy: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let narrowed = if want_truthy {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
fn narrow_static_prop_instanceof(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
is_true: bool,
) {
let current = if let Some(refined) = ctx.get_prop_refined(fqcn, prop) {
refined.clone()
} else {
let here = crate::db::Fqcn::from_str(db, fqcn);
crate::db::find_property_in_chain(db, here, prop)
.and_then(|(_, p)| p.ty.as_deref().cloned())
.unwrap_or_else(mir_types::Type::mixed)
};
if current.is_mixed_not_template() {
return;
}
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(¤t, class_name, db, &ctx.template_param_names)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
if !narrowed.is_empty() {
if narrowed != current {
ctx.set_prop_refined(fqcn, prop, narrowed);
}
} else if !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
}
pub(crate) fn apply_prop_narrowed(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
current: Type,
narrowed: Type,
mark_diverges: bool,
) {
if !narrowed.is_empty() {
if narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
} else if mark_diverges && !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
}
fn narrow_prop_array_empty(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_empty: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_empty {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
fn narrow_static_prop_array_empty(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
is_empty: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let narrowed = if is_empty {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
fn narrow_prop_instanceof(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed_not_template() {
return;
}
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(¤t, class_name, db, &ctx.template_param_names)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
let mark_diverges = is_true || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_is_a(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
class_name: &str,
allow_string: bool,
db: &dyn MirDatabase,
is_true: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed_not_template() {
return;
}
if allow_string {
let narrowed = if is_true {
let (mut result, obj_part) = partition_is_a_string_like(¤t, class_name, db);
if !obj_part.is_empty() || current.is_mixed() {
let obj_src = if obj_part.is_empty() {
¤t
} else {
&obj_part
};
let obj_narrowed = narrow_instanceof_preserving_subtypes(
obj_src,
class_name,
db,
&ctx.template_param_names,
);
for atom in obj_narrowed.types.iter() {
result.add_type(atom.clone());
}
}
result
} else {
filter_out_is_a_string_match(¤t, class_name, db)
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
} else {
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(
¤t,
class_name,
db,
&ctx.template_param_names,
)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
}
#[allow(clippy::too_many_arguments)]
fn narrow_prop_is_a(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
class_name: &str,
allow_string: bool,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed_not_template() {
return;
}
if allow_string {
let narrowed = if is_true {
let (mut result, obj_part) = partition_is_a_string_like(¤t, class_name, db);
if !obj_part.is_empty() || current.is_mixed() {
let obj_src = if obj_part.is_empty() {
¤t
} else {
&obj_part
};
let obj_narrowed = narrow_instanceof_preserving_subtypes(
obj_src,
class_name,
db,
&ctx.template_param_names,
);
for atom in obj_narrowed.types.iter() {
result.add_type(atom.clone());
}
}
result
} else {
filter_out_is_a_string_match(¤t, class_name, db)
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
} else {
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(
¤t,
class_name,
db,
&ctx.template_param_names,
)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
let mark_diverges = is_true || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
}
fn narrow_static_prop_is_subclass_of(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
is_true: bool,
) {
if !is_true {
return;
}
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed_not_template() {
return;
}
let narrowed = narrow_strict_subclass_of(¤t, class_name, db, &ctx.template_param_names);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
fn narrow_prop_is_subclass_of(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
if !is_true {
return;
}
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed_not_template() {
return;
}
let narrowed = narrow_strict_subclass_of(¤t, class_name, db, &ctx.template_param_names);
if !narrowed.is_empty() && narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
}
fn narrow_static_prop_array_key_exists(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
key: &mir_types::atomic::ArrayKey,
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let narrowed = add_key_to_sealed_shapes(¤t, key);
if narrowed != current {
ctx.set_prop_refined(fqcn, prop, narrowed);
}
}
fn narrow_prop_array_key_exists(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
key: &mir_types::atomic::ArrayKey,
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let narrowed = add_key_to_sealed_shapes(¤t, key);
if narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
}
fn add_key_to_sealed_shapes(
ty: &mir_types::Type,
key: &mir_types::atomic::ArrayKey,
) -> mir_types::Type {
use mir_types::atomic::{ArrayKey, KeyedProperty};
let is_real_union = ty.types.len() > 1;
let mut changed = false;
let mut result = mir_types::Type::empty();
for a in &ty.types {
if let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = a
{
if !properties.contains_key(key) {
changed = true;
if *is_open || !is_real_union {
let mut new_props = properties.clone();
let stays_list = *is_list
&& matches!(key, ArrayKey::Int(n) if *n == properties.len() as i64);
new_props.insert(
key.clone(),
KeyedProperty {
ty: mir_types::Type::mixed(),
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: stays_list,
});
}
continue;
}
if let Some(prop) = properties.get(key) {
if prop.optional {
changed = true;
let mut new_props = properties.clone();
if let Some(new_prop) = new_props.get_mut(key) {
new_prop.optional = false;
}
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
continue;
}
}
}
result.add_type(a.clone());
}
if !changed {
return ty.clone();
}
if result.types.is_empty() {
return ty.clone();
}
result.from_docblock = ty.from_docblock;
result
}
fn remove_key_from_sealed_shapes(
ty: &mir_types::Type,
key: &mir_types::atomic::ArrayKey,
) -> mir_types::Type {
if ty.types.len() <= 1 {
return ty.clone();
}
let mut changed = false;
let mut result = mir_types::Type::empty();
for a in &ty.types {
if let Atomic::TKeyedArray { properties, .. } = a {
if let Some(prop) = properties.get(key) {
if !prop.optional {
changed = true;
continue;
}
}
}
result.add_type(a.clone());
}
if !changed {
return ty.clone();
}
if result.types.is_empty() {
return ty.clone();
}
result.from_docblock = ty.from_docblock;
result
}
fn extract_int_literal(expr: &php_ast::owned::Expr) -> Option<i64> {
let e = peel_parens(expr);
match &e.kind {
ExprKind::Int(n) => Some(*n),
ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::Negate => {
if let ExprKind::Int(n) = &u.operand.kind {
n.checked_neg()
} else {
None
}
}
_ => None,
}
}
fn flip_comparison_op(op: BinaryOp) -> BinaryOp {
match op {
BinaryOp::Less => BinaryOp::Greater,
BinaryOp::LessOrEqual => BinaryOp::GreaterOrEqual,
BinaryOp::Greater => BinaryOp::Less,
BinaryOp::GreaterOrEqual => BinaryOp::LessOrEqual,
other => other,
}
}
const IMPOSSIBLE_BOUNDS: (Option<i64>, Option<i64>) = (Some(1), Some(0));
fn int_comparison_bounds(
op: BinaryOp,
n: i64,
is_true: bool,
) -> Option<(Option<i64>, Option<i64>)> {
match (op, is_true) {
(BinaryOp::Less, true) | (BinaryOp::GreaterOrEqual, false) => Some(
n.checked_sub(1)
.map_or(IMPOSSIBLE_BOUNDS, |hi| (None, Some(hi))),
),
(BinaryOp::LessOrEqual, true) | (BinaryOp::Greater, false) => Some((None, Some(n))),
(BinaryOp::Greater, true) | (BinaryOp::LessOrEqual, false) => Some(
n.checked_add(1)
.map_or(IMPOSSIBLE_BOUNDS, |lo| (Some(lo), None)),
),
(BinaryOp::GreaterOrEqual, true) | (BinaryOp::Less, false) => Some((Some(n), None)),
_ => None,
}
}
fn int_comparison_excludes_null(op: BinaryOp, n: i64, is_true: bool) -> bool {
match (op, is_true) {
(BinaryOp::Greater, true) | (BinaryOp::LessOrEqual, false) => true,
(BinaryOp::Greater, false) | (BinaryOp::LessOrEqual, true) => false,
(BinaryOp::Less, true) | (BinaryOp::GreaterOrEqual, false) => n == 0,
(BinaryOp::Less, false) | (BinaryOp::GreaterOrEqual, true) => n != 0,
_ => false,
}
}
fn narrow_var_int_comparison(ctx: &mut FlowState, name: &str, op: BinaryOp, n: i64, is_true: bool) {
let Some((min, max)) = int_comparison_bounds(op, n, is_true) else {
return;
};
let current = ctx.get_var(name);
let mut narrowed = narrow_type_to_int_range(¤t, min, max);
if int_comparison_excludes_null(op, n, is_true) {
narrowed = narrowed.remove_null();
}
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
set_narrowed(ctx, name, ¤t, narrowed, mark_diverges);
}
#[allow(clippy::too_many_arguments)]
fn narrow_prop_int_comparison(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some((min, max)) = int_comparison_bounds(op, n, is_true) else {
return;
};
narrow_receiver_non_null_on_prop_match(
ctx,
obj_var,
int_comparison_excludes_null(op, n, is_true),
);
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let mut narrowed = narrow_type_to_int_range(¤t, min, max);
if int_comparison_excludes_null(op, n, is_true) {
narrowed = narrowed.remove_null();
}
let mark_diverges =
crate::contradiction::is_closed_precise(¤t) && !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_int_comparison(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some((min, max)) = int_comparison_bounds(op, n, is_true) else {
return;
};
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let mut narrowed = narrow_type_to_int_range(¤t, min, max);
if int_comparison_excludes_null(op, n, is_true) {
narrowed = narrowed.remove_null();
}
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, mark_diverges);
}
fn narrow_type_to_int_range(ty: &Type, min: Option<i64>, max: Option<i64>) -> Type {
let in_bounds = |v: i64| min.is_none_or(|lo| v >= lo) && max.is_none_or(|hi| v <= hi);
let mut result = Type::empty();
result.from_docblock = ty.from_docblock;
for atomic in &ty.types {
match atomic {
Atomic::TInt => {
intersect_int_range_into(&mut result, None, None, min, max);
}
Atomic::TPositiveInt => {
intersect_int_range_into(&mut result, Some(1), None, min, max);
}
Atomic::TNonNegativeInt => {
intersect_int_range_into(&mut result, Some(0), None, min, max);
}
Atomic::TNegativeInt => {
intersect_int_range_into(&mut result, None, Some(-1), min, max);
}
Atomic::TIntRange {
min: cur_min,
max: cur_max,
} => {
intersect_int_range_into(&mut result, *cur_min, *cur_max, min, max);
}
Atomic::TLiteralInt(v) => {
if in_bounds(*v) {
result.add_type(atomic.clone());
}
}
_ => {
result.add_type(atomic.clone());
}
}
}
result
}
fn intersect_int_range_into(
out: &mut Type,
existing_min: Option<i64>,
existing_max: Option<i64>,
narrow_min: Option<i64>,
narrow_max: Option<i64>,
) {
let new_min = match (existing_min, narrow_min) {
(Some(a), Some(b)) => Some(a.max(b)),
(None, v) | (v, None) => v,
};
let new_max = match (existing_max, narrow_max) {
(Some(a), Some(b)) => Some(a.min(b)),
(None, v) | (v, None) => v,
};
if let (Some(lo), Some(hi)) = (new_min, new_max) {
if lo > hi {
return; }
}
out.add_type(Atomic::TIntRange {
min: new_min,
max: new_max,
});
}
fn narrow_string_to_non_empty(ty: &Type) -> Type {
let mut result = Type::empty();
result.from_docblock = ty.from_docblock;
for t in &ty.types {
match t {
Atomic::TString => result.add_type(Atomic::TNonEmptyString),
Atomic::TLiteralString(s) if s.as_ref().is_empty() => {}
_ => result.add_type(t.clone()),
}
}
result
}
fn narrow_string_to_empty(ty: &Type) -> Type {
ty.filter(|t| match t {
Atomic::TNonEmptyString
| Atomic::TNumericString
| Atomic::TClassString(_)
| Atomic::TCallableString => false,
Atomic::TLiteralString(s) => s.as_ref().is_empty(),
_ => true,
})
}
fn narrow_var_null(ctx: &mut FlowState, name: &str, is_null: bool) {
let current = ctx.get_var(name);
let narrowed = if is_null {
current.narrow_to_null()
} else {
current.remove_null()
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
fn narrow_var_loose_null(ctx: &mut FlowState, name: &str, is_null: bool) {
let current = ctx.get_var(name);
let narrowed = if is_null {
current.narrow_to_falsy()
} else {
current.remove_null()
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
fn narrow_prop_loose_null(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_null: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let narrowed = if is_null {
current.narrow_to_falsy()
} else {
current.remove_null()
};
let receiver_nullable = ctx.get_var(obj_var).is_nullable();
let mark_diverges = !receiver_nullable || !is_null;
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
if !is_null && receiver_nullable {
narrow_var_null(ctx, obj_var, false);
}
}
pub(crate) fn narrow_receiver_non_null_on_prop_match(
ctx: &mut FlowState,
obj_var: &str,
proved_match: bool,
) {
if proved_match {
narrow_var_null(ctx, obj_var, false);
}
}
fn narrow_var_loose_bool(ctx: &mut FlowState, name: &str, want_truthy: bool) {
let current = ctx.get_var(name);
let narrowed = if want_truthy {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
set_narrowed(ctx, name, ¤t, narrowed, false);
}
fn narrow_prop_loose_bool(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
want_truthy: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let narrowed = if want_truthy {
current.narrow_to_truthy()
} else {
current.narrow_to_falsy()
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
fn narrow_var_bool(ctx: &mut FlowState, name: &str, value: bool, is_value: bool) {
let current = ctx.get_var(name);
let narrowed = bool_narrow_type(¤t, value, is_value);
set_narrowed(ctx, name, ¤t, narrowed, false);
}
fn narrow_prop_bool(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
value: bool,
is_value: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let narrowed = bool_narrow_type(¤t, value, is_value);
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
fn bool_narrow_type(current: &Type, value: bool, is_value: bool) -> Type {
let mut narrowed = Type::empty();
narrowed.from_docblock = current.from_docblock;
for t in ¤t.types {
let keep = match t {
Atomic::TBool => {
if is_value {
let lit = if value { Atomic::TTrue } else { Atomic::TFalse };
narrowed.add_type(lit);
} else {
let lit = if value { Atomic::TFalse } else { Atomic::TTrue };
narrowed.add_type(lit);
}
false }
Atomic::TTrue => is_value == value,
Atomic::TFalse => is_value != value,
Atomic::TMixed | Atomic::TScalar => {
if is_value {
let lit = if value { Atomic::TTrue } else { Atomic::TFalse };
narrowed.add_type(lit);
} else {
narrowed.add_type(t.clone());
}
false }
_ => !is_value, };
if keep {
narrowed.add_type(t.clone());
}
}
narrowed
}
fn narrow_from_type_fn(
ctx: &mut FlowState,
fn_name: &str,
var_name: &str,
db: &dyn MirDatabase,
is_true: bool,
) {
let current = ctx.get_var(var_name);
let Some(narrowed) = type_fn_narrowed(¤t, fn_name, db, is_true) else {
return;
};
set_narrowed(ctx, var_name, ¤t, narrowed, true);
}
fn narrow_prop_from_type_fn(
ctx: &mut FlowState,
fn_name: &str,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let Some(narrowed) = type_fn_narrowed(¤t, fn_name, db, is_true) else {
return;
};
let is_null_check = crate::util::php_ident_lowercase(fn_name) == "is_null";
let receiver_nullable = ctx.get_var(obj_var).is_nullable();
let proved_prop_non_null = is_true != is_null_check;
let mark_diverges = !receiver_nullable || proved_prop_non_null;
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
narrow_receiver_non_null_on_prop_match(ctx, obj_var, proved_prop_non_null);
}
fn narrow_static_prop_from_type_fn(
ctx: &mut FlowState,
fn_name: &str,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
is_true: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let Some(narrowed) = type_fn_narrowed(¤t, fn_name, db, is_true) else {
return;
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
fn type_fn_narrowed(
current: &Type,
fn_name: &str,
db: &dyn MirDatabase,
is_true: bool,
) -> Option<Type> {
Some(match crate::util::php_ident_lowercase(fn_name).as_str() {
"is_string" => {
if is_true {
current.narrow_to_string()
} else {
current.filter(|t| !t.is_string())
}
}
"is_int" | "is_integer" | "is_long" => {
if is_true {
current.narrow_to_int()
} else {
current.filter(|t| !t.is_int())
}
}
"is_float" | "is_double" | "is_real" => {
if is_true {
current.narrow_to_float()
} else {
current.filter(|t| {
!matches!(
t,
Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
)
})
}
}
"is_bool" => {
if is_true {
current.narrow_to_bool()
} else {
current.filter(|t| !matches!(t, Atomic::TBool | Atomic::TTrue | Atomic::TFalse))
}
}
"is_null" => {
if is_true {
current.narrow_to_null()
} else {
current.remove_null()
}
}
"is_array" => {
if is_true {
current.narrow_to_array()
} else {
current.filter(|t| !t.is_array())
}
}
"array_is_list" => {
if is_true {
current.narrow_to_list()
} else {
current.filter(|t| {
!matches!(
t,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
})
}
}
"is_object" => {
if is_true {
current.narrow_to_object()
} else {
current.filter(|t| !t.is_object())
}
}
"is_callable" => {
if is_true {
current.narrow_to_callable()
} else {
current.filter(|t| !t.is_callable())
}
}
"is_scalar" => {
if is_true {
current.narrow_to_scalar()
} else {
current.filter(|t| {
!t.is_string()
&& !t.is_int()
&& !matches!(
t,
Atomic::TFloat
| Atomic::TIntegralFloat
| Atomic::TLiteralFloat(..)
| Atomic::TBool
| Atomic::TTrue
| Atomic::TFalse
| Atomic::TScalar
| Atomic::TNumeric
)
})
}
}
"is_iterable" => {
if is_true {
current.narrow_to_iterable()
} else {
current
.filter(|t| !atom_excluded_from_is_iterable_or_countable(t, "Traversable", db))
}
}
"is_countable" => {
if is_true {
current.narrow_to_countable()
} else {
current.filter(|t| !atom_excluded_from_is_iterable_or_countable(t, "Countable", db))
}
}
"is_resource" => {
if is_true {
current.narrow_to_resource()
} else {
current.clone()
}
}
"is_numeric" => {
if is_true {
let mut narrowed_parts = Type::empty();
for t in ¤t.types {
match t {
Atomic::TInt
| Atomic::TIntRange { .. }
| Atomic::TPositiveInt
| Atomic::TNonNegativeInt
| Atomic::TNegativeInt
| Atomic::TLiteralInt(_)
| Atomic::TFloat
| Atomic::TIntegralFloat
| Atomic::TLiteralFloat(..)
| Atomic::TNumeric
| Atomic::TNumericString => {
narrowed_parts.add_type(t.clone());
}
Atomic::TString | Atomic::TNonEmptyString => {
narrowed_parts.add_type(Atomic::TNumericString);
}
Atomic::TLiteralString(s) if is_numeric_string(s) => {
narrowed_parts.add_type(t.clone());
}
Atomic::TScalar | Atomic::TMixed => {
narrowed_parts.add_type(Atomic::TInt);
narrowed_parts.add_type(Atomic::TFloat);
narrowed_parts.add_type(Atomic::TNumericString);
}
_ => {} }
}
narrowed_parts
} else {
current.filter(|t| {
!matches!(
t,
Atomic::TInt
| Atomic::TIntRange { .. }
| Atomic::TPositiveInt
| Atomic::TNonNegativeInt
| Atomic::TNegativeInt
| Atomic::TFloat
| Atomic::TIntegralFloat
| Atomic::TNumeric
| Atomic::TNumericString
| Atomic::TLiteralInt(_)
| Atomic::TLiteralFloat(..)
) && !matches!(t, Atomic::TLiteralString(s) if is_numeric_string(s))
})
}
}
"ctype_alpha" | "ctype_alnum" | "ctype_digit" | "ctype_lower" | "ctype_upper"
| "ctype_punct" | "ctype_space" | "ctype_xdigit" | "ctype_print" | "ctype_graph"
| "ctype_cntrl" => {
if is_true {
narrow_string_to_non_empty(current)
} else {
current.clone()
}
}
"method_exists" | "property_exists" => {
let mut result = Type::empty();
result.from_docblock = current.from_docblock;
for t in ¤t.types {
if t.is_object() || t.is_string() {
result.add_type(t.clone());
} else if matches!(t, Atomic::TMixed) {
result.add_type(Atomic::TObject);
} else if matches!(t, Atomic::TScalar) {
result.add_type(Atomic::TString);
}
}
result
}
_ => return None,
})
}
fn atom_excluded_from_is_iterable_or_countable(
t: &Atomic,
interface: &str,
db: &dyn MirDatabase,
) -> bool {
if t.is_array() {
return true;
}
if let Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } = t
{
return crate::db::extends_or_implements(db, fqcn, interface);
}
false
}
fn narrow_var_literal_string(ctx: &mut FlowState, name: &str, value: &str, is_value: bool) {
let current = ctx.get_var(name);
let narrowed = literal_string_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
set_narrowed(ctx, name, ¤t, narrowed, mark_diverges);
}
fn narrow_prop_literal_string(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
value: &str,
is_value: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = literal_string_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn literal_string_narrow_type(current: &Type, value: &str, is_value: bool) -> Type {
if is_value {
let lit: std::sync::Arc<str> = std::sync::Arc::from(value);
let mut result = Type::empty();
result.from_docblock = current.from_docblock;
for t in ¤t.types {
match t {
Atomic::TLiteralString(s) if s.as_ref() == value => {
result.add_type(t.clone());
}
Atomic::TString | Atomic::TScalar | Atomic::TMixed => {
result.add_type(Atomic::TLiteralString(lit.clone()));
}
Atomic::TNonEmptyString if !value.is_empty() => {
result.add_type(Atomic::TLiteralString(lit.clone()));
}
Atomic::TNumericString if is_numeric_string(value) => {
result.add_type(Atomic::TLiteralString(lit.clone()));
}
Atomic::TCallableString
| Atomic::TClassString(_)
| Atomic::TInterfaceString(_)
| Atomic::TEnumString
| Atomic::TTraitString => {
result.add_type(Atomic::TLiteralString(lit.clone()));
}
_ => {} }
}
result
} else {
current.filter(|t| !matches!(t, Atomic::TLiteralString(s) if s.as_ref() == value))
}
}
fn narrow_var_literal_int(ctx: &mut FlowState, name: &str, value: i64, is_value: bool) {
let current = ctx.get_var(name);
let narrowed = literal_int_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
set_narrowed(ctx, name, ¤t, narrowed, mark_diverges);
}
fn atom_safe_for_loose_int_narrowing(a: &Atomic, value: i64) -> bool {
a.is_int() || (matches!(a, Atomic::TNull) && value != 0)
}
fn narrow_var_loose_int(ctx: &mut FlowState, name: &str, value: i64, is_value: bool) {
let current = ctx.get_var(name);
if current.types.is_empty()
|| !current
.types
.iter()
.all(|a| atom_safe_for_loose_int_narrowing(a, value))
{
return;
}
let narrowed = literal_int_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
set_narrowed(ctx, name, ¤t, narrowed, mark_diverges);
}
fn narrow_prop_literal_int(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
value: i64,
is_value: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = literal_int_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn narrow_prop_loose_int(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
value: i64,
is_value: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.types.is_empty()
|| !current
.types
.iter()
.all(|a| atom_safe_for_loose_int_narrowing(a, value))
{
return;
}
let narrowed = literal_int_narrow_type(¤t, value, is_value);
let mark_diverges = crate::contradiction::is_closed_precise(¤t);
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn literal_int_narrow_type(current: &Type, value: i64, is_value: bool) -> Type {
if is_value {
let int_contains = |min: Option<i64>, max: Option<i64>| {
min.is_none_or(|lo| value >= lo) && max.is_none_or(|hi| value <= hi)
};
let mut result = Type::empty();
result.from_docblock = current.from_docblock;
for t in ¤t.types {
match t {
Atomic::TLiteralInt(n) if *n == value => {
result.add_type(t.clone());
}
Atomic::TInt | Atomic::TScalar | Atomic::TNumeric | Atomic::TMixed => {
result.add_type(Atomic::TLiteralInt(value));
}
Atomic::TIntRange { min, max } if int_contains(*min, *max) => {
result.add_type(Atomic::TLiteralInt(value));
}
Atomic::TPositiveInt if int_contains(Some(1), None) => {
result.add_type(Atomic::TLiteralInt(value));
}
Atomic::TNonNegativeInt if int_contains(Some(0), None) => {
result.add_type(Atomic::TLiteralInt(value));
}
Atomic::TNegativeInt if int_contains(None, Some(-1)) => {
result.add_type(Atomic::TLiteralInt(value));
}
_ => {}
}
}
result
} else {
let tighten = |min: Option<i64>, max: Option<i64>| {
let (new_min, new_max) = if min == Some(value) {
match value.checked_add(1) {
Some(v) => (Some(v), max),
None => (Some(i64::MAX), Some(i64::MIN)),
}
} else if max == Some(value) {
match value.checked_sub(1) {
Some(v) => (min, Some(v)),
None => (Some(i64::MAX), Some(i64::MIN)),
}
} else {
return None; };
let atom = match (new_min, new_max) {
(Some(1), None) => Atomic::TPositiveInt,
(Some(0), None) => Atomic::TNonNegativeInt,
(None, Some(-1)) => Atomic::TNegativeInt,
(None, None) => Atomic::TInt,
(min, max) => Atomic::TIntRange { min, max },
};
Some(atom)
};
let mut result = Type::empty();
result.from_docblock = current.from_docblock;
for t in ¤t.types {
match t {
Atomic::TLiteralInt(n) if *n == value => {} Atomic::TIntRange { min, max } => {
if let Some(tightened) = tighten(*min, *max) {
let is_empty_range = matches!(
&tightened,
Atomic::TIntRange { min: Some(lo), max: Some(hi) } if lo > hi
);
if !is_empty_range {
result.add_type(tightened);
}
} else {
result.add_type(t.clone());
}
}
Atomic::TPositiveInt => {
if let Some(tightened) = tighten(Some(1), None) {
result.add_type(tightened);
} else {
result.add_type(t.clone());
}
}
Atomic::TNonNegativeInt => {
if let Some(tightened) = tighten(Some(0), None) {
result.add_type(tightened);
} else {
result.add_type(t.clone());
}
}
Atomic::TNegativeInt => {
if let Some(tightened) = tighten(None, Some(-1)) {
result.add_type(tightened);
} else {
result.add_type(t.clone());
}
}
_ => result.add_type(t.clone()),
}
}
result
}
}
fn expand_enum_to_cases(db: &dyn MirDatabase, ty: &Type, enum_fqcn: &str) -> Type {
if !ty
.types
.iter()
.any(|a| matches!(a, Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == enum_fqcn))
{
return ty.clone();
}
let Some(crate::db::ClassLike::Enum(e)) =
crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, enum_fqcn))
else {
return ty.clone();
};
let mut result = Type::empty();
result.possibly_undefined = ty.possibly_undefined;
result.from_docblock = ty.from_docblock;
for atomic in &ty.types {
match atomic {
Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == enum_fqcn => {
for case_name in e.cases.keys() {
result.add_type(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.as_ref().into(),
});
}
}
other => result.add_type(other.clone()),
}
}
result
}
fn narrow_var_to_literal_enum_case(
db: &dyn MirDatabase,
ctx: &mut FlowState,
name: &str,
enum_fqcn: &str,
case_name: &str,
is_case: bool,
) {
let current = ctx.get_var(name);
let narrowed = if is_case {
Type::single(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.into(),
})
} else {
expand_enum_to_cases(db, ¤t, enum_fqcn).filter(|t| {
!matches!(t, Atomic::TLiteralEnumCase { enum_fqcn: fqcn, case_name: c }
if fqcn.as_ref() == enum_fqcn && c.as_ref() == case_name)
})
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
fn narrow_prop_to_literal_enum_case(
db: &dyn MirDatabase,
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
file: &str,
(enum_fqcn, case_name): (&str, &str),
is_case: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_case {
Type::single(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.into(),
})
} else {
expand_enum_to_cases(db, ¤t, enum_fqcn).filter(|t| {
!matches!(t, Atomic::TLiteralEnumCase { enum_fqcn: fqcn, case_name: c }
if fqcn.as_ref() == enum_fqcn && c.as_ref() == case_name)
})
};
let mark_diverges = is_case || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_to_literal_enum_case(
db: &dyn MirDatabase,
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
(enum_fqcn, case_name): (&str, &str),
is_case: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let narrowed = if is_case {
Type::single(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.into(),
})
} else {
expand_enum_to_cases(db, ¤t, enum_fqcn).filter(|t| {
!matches!(t, Atomic::TLiteralEnumCase { enum_fqcn: fqcn, case_name: c }
if fqcn.as_ref() == enum_fqcn && c.as_ref() == case_name)
})
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
fn narrow_static_prop_to_class_string(
ctx: &mut FlowState,
fqcn_key: &str,
prop: &str,
fqcn: &str,
is_class: bool,
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn_key, prop, db);
let narrowed = if is_class {
Type::single(Atomic::TClassString(Some(mir_types::Name::from(fqcn))))
} else {
current.filter(|t| {
!matches!(t, Atomic::TClassString(Some(f)) if f.as_ref() == fqcn && crate::db::is_final(db, fqcn))
})
};
apply_prop_narrowed(ctx, fqcn_key, prop, current, narrowed, true);
}
fn narrow_var_to_class_string(
ctx: &mut FlowState,
name: &str,
fqcn: &str,
is_class: bool,
db: &dyn MirDatabase,
) {
let current = ctx.get_var(name);
let narrowed = if is_class {
Type::single(Atomic::TClassString(Some(mir_types::Name::from(fqcn))))
} else {
current.filter(|t| {
!matches!(t, Atomic::TClassString(Some(f)) if f.as_ref() == fqcn && crate::db::is_final(db, fqcn))
})
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
fn narrow_prop_to_class_string(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
fqcn: &str,
is_class: bool,
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_class {
Type::single(Atomic::TClassString(Some(mir_types::Name::from(fqcn))))
} else {
current.filter(|t| {
!matches!(t, Atomic::TClassString(Some(f)) if f.as_ref() == fqcn && crate::db::is_final(db, fqcn))
})
};
let mark_diverges = is_class || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn type_params_for_exact_class(
current: &Type,
target_fqcn: &str,
db: &dyn MirDatabase,
) -> std::sync::Arc<[Type]> {
for t in ¤t.types {
if let Atomic::TNamedObject { fqcn, type_params } = t {
if type_params.is_empty() {
continue;
}
if fqcn.as_ref() == target_fqcn {
return type_params.clone();
}
if named_object_matches_instanceof(target_fqcn, fqcn, db) {
return project_type_params_onto_subclass(db, fqcn, type_params, target_fqcn);
}
}
}
mir_types::union::empty_type_params()
}
fn narrow_var_to_specific_class(
ctx: &mut FlowState,
name: &str,
fqcn: &str,
is_exact_class: bool,
db: &dyn MirDatabase,
) {
let current = ctx.get_var(name);
let narrowed = if is_exact_class {
Type::single(Atomic::TNamedObject {
fqcn: fqcn.into(),
type_params: type_params_for_exact_class(¤t, fqcn, db),
})
} else {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn: obj_fqcn, .. }
| Atomic::TSelf { fqcn: obj_fqcn }
| Atomic::TStaticObject { fqcn: obj_fqcn }
| Atomic::TParent { fqcn: obj_fqcn } => {
obj_fqcn.as_ref() != fqcn || !crate::db::is_final(db, fqcn)
}
_ => true,
})
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
fn narrow_prop_to_specific_class(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
fqcn: &str,
is_exact_class: bool,
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_exact_class {
Type::single(Atomic::TNamedObject {
fqcn: fqcn.into(),
type_params: type_params_for_exact_class(¤t, fqcn, db),
})
} else {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn: obj_fqcn, .. }
| Atomic::TSelf { fqcn: obj_fqcn }
| Atomic::TStaticObject { fqcn: obj_fqcn }
| Atomic::TParent { fqcn: obj_fqcn } => {
obj_fqcn.as_ref() != fqcn || !crate::db::is_final(db, fqcn)
}
_ => true,
})
};
let mark_diverges = is_exact_class || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
fn narrow_static_prop_to_specific_class(
ctx: &mut FlowState,
fqcn_receiver: &str,
prop: &str,
fqcn: &str,
is_exact_class: bool,
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn_receiver, prop, db);
let narrowed = if is_exact_class {
Type::single(Atomic::TNamedObject {
fqcn: fqcn.into(),
type_params: type_params_for_exact_class(¤t, fqcn, db),
})
} else {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn: obj_fqcn, .. }
| Atomic::TSelf { fqcn: obj_fqcn }
| Atomic::TStaticObject { fqcn: obj_fqcn }
| Atomic::TParent { fqcn: obj_fqcn } => {
obj_fqcn.as_ref() != fqcn || !crate::db::is_final(db, fqcn)
}
_ => true,
})
};
apply_prop_narrowed(ctx, fqcn_receiver, prop, current, narrowed, true);
}
pub(crate) fn extract_class_fqcn_from_expr(
expr: &php_ast::owned::Expr,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
db: &dyn MirDatabase,
file: &str,
) -> Option<std::sync::Arc<str>> {
let expr = peel_parens(expr);
match &expr.kind {
ExprKind::ClassConstAccess(cca) => {
if let ExprKind::Identifier(id) = &cca.class.kind {
let member = match &cca.member.kind {
ExprKind::Identifier(s) => s.as_ref(),
_ => return None,
};
if member.eq_ignore_ascii_case("class") {
match id.to_ascii_lowercase().as_str() {
"self" | "static" => {
let fqcn = if id.eq_ignore_ascii_case("static") {
static_fqcn.or(self_fqcn)
} else {
self_fqcn
};
return fqcn.map(std::sync::Arc::from);
}
"parent" => return parent_fqcn.map(std::sync::Arc::from),
_ => {
let resolved = crate::db::resolve_name(db, file, id.as_ref());
return Some(std::sync::Arc::from(resolved.as_str()));
}
}
}
}
None
}
ExprKind::String(s) => {
let name = s.as_ref().trim_start_matches('\\');
if !name.is_empty() {
Some(std::sync::Arc::from(name))
} else {
None
}
}
_ => None,
}
}
pub(crate) fn extract_prop_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
match &expr.kind {
ExprKind::PropertyAccess(pa) => {
let obj = extract_var_name(&pa.object)?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref().to_string(),
_ => return None,
};
Some((obj, prop))
}
ExprKind::Parenthesized(inner) => extract_prop_access(inner),
_ => None,
}
}
fn extract_nullsafe_prop_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
match &expr.kind {
ExprKind::NullsafePropertyAccess(pa) => {
let obj = extract_var_name(&pa.object)?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref().to_string(),
_ => return None,
};
Some((obj, prop))
}
ExprKind::Parenthesized(inner) => extract_nullsafe_prop_access(inner),
_ => None,
}
}
fn extract_any_prop_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
extract_nullsafe_prop_access(expr).or_else(|| extract_prop_access(expr))
}
pub(crate) fn extract_static_prop_access(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
extract_static_prop_access_parts(
expr,
db,
file,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
)
}
fn extract_static_prop_access_parts(
expr: &php_ast::owned::Expr,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> Option<(std::sync::Arc<str>, String)> {
match &expr.kind {
ExprKind::StaticPropertyAccess(spa) => {
let id = match &spa.class.kind {
ExprKind::Identifier(id) => id,
_ => return None,
};
let resolved = crate::db::resolve_name(db, file, id.as_ref());
let fqcn = match resolved.as_str() {
"self" | "static" => std::sync::Arc::from(self_fqcn.or(static_fqcn)?),
"parent" => std::sync::Arc::from(parent_fqcn?),
s => std::sync::Arc::from(s),
};
let prop = match &spa.member.kind {
ExprKind::Variable(name) | ExprKind::Identifier(name) => {
name.trim_start_matches('$').to_string()
}
_ => return None,
};
Some((fqcn, prop))
}
ExprKind::Parenthesized(inner) => {
extract_static_prop_access_parts(inner, db, file, self_fqcn, static_fqcn, parent_fqcn)
}
_ => None,
}
}
fn extract_var_name(expr: &php_ast::owned::Expr) -> Option<String> {
match &expr.kind {
ExprKind::Variable(name) => Some(name.trim_start_matches('$').to_string()),
ExprKind::Parenthesized(inner) => extract_var_name(inner),
ExprKind::Assign(a) if matches!(a.op, AssignOp::Assign) => extract_var_name(&a.target),
_ => None,
}
}
pub(crate) fn extract_expr_guard_key(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<std::sync::Arc<str>> {
match &expr.kind {
ExprKind::Variable(name) => Some(std::sync::Arc::from(name.trim_start_matches('$'))),
ExprKind::Parenthesized(inner) => extract_expr_guard_key(inner, ctx, db, file),
ExprKind::PropertyAccess(pa) => {
let base = extract_var_name(&pa.object)?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref(),
ExprKind::Variable(s) => s.trim_start_matches('$'),
_ => return None,
};
Some(std::sync::Arc::from(format!("{base}->{prop}").as_str()))
}
ExprKind::StaticPropertyAccess(_) => {
let (fqcn, prop) = extract_static_prop_access(expr, ctx, db, file)?;
Some(std::sync::Arc::from(
format!("static:{fqcn}::{prop}").as_str(),
))
}
ExprKind::ClassConstAccess(cca) => {
let ExprKind::Identifier(member) = &cca.member.kind else {
return None;
};
if !member.eq_ignore_ascii_case("class") {
return None;
}
let ExprKind::Identifier(class_name) = &cca.class.kind else {
return None;
};
let resolved = crate::db::resolve_name(db, file, class_name.as_ref());
Some(std::sync::Arc::from(format!("cls:{resolved}").as_str()))
}
_ => None,
}
}
enum ShapeBase {
Var(String),
Prop(String, String),
Static(std::sync::Arc<str>, String),
}
impl ShapeBase {
fn extract(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<Self> {
match ScalarArgTarget::extract(expr) {
Some(ScalarArgTarget::Var(name)) => Some(ShapeBase::Var(name)),
Some(ScalarArgTarget::Prop(obj, prop)) => Some(ShapeBase::Prop(obj, prop)),
None => extract_static_prop_access(expr, ctx, db, file)
.map(|(fqcn, prop)| ShapeBase::Static(fqcn, prop)),
}
}
}
pub(crate) enum MatchSubject {
Var(String),
Prop(String, String),
Static(std::sync::Arc<str>, String),
}
impl MatchSubject {
pub(crate) fn extract(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<Self> {
if let Some(name) = extract_var_name(expr) {
return Some(MatchSubject::Var(name));
}
if let Some((obj, prop)) = extract_prop_access(expr) {
return Some(MatchSubject::Prop(obj, prop));
}
extract_static_prop_access(expr, ctx, db, file)
.map(|(fqcn, prop)| MatchSubject::Static(fqcn, prop))
}
}
fn array_access_base_target(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<ShapeBase> {
match &expr.kind {
ExprKind::ArrayAccess(aa) => array_access_base_target(&aa.array, ctx, db, file),
ExprKind::Parenthesized(inner) => array_access_base_target(inner, ctx, db, file),
_ => ShapeBase::extract(expr, ctx, db, file),
}
}
fn narrow_container_non_null_non_false(
ctx: &mut FlowState,
target: &ShapeBase,
db: &dyn MirDatabase,
file: &str,
) {
match target {
ShapeBase::Var(name) => {
let current = ctx.get_var(name);
ctx.set_var(name, current.remove_null().remove_false());
}
ShapeBase::Prop(obj, prop) => {
let current = resolve_prop_current_type(ctx, obj, prop, db, file);
if !current.is_mixed() {
let narrowed = current.remove_null().remove_false();
apply_prop_narrowed(ctx, obj, prop, current, narrowed, true);
}
}
ShapeBase::Static(fqcn, prop) => {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if !current.is_mixed() {
let narrowed = current.remove_null().remove_false();
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
}
}
}
fn narrow_isset_shape_key(
var_expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) {
let Some((base, path)) = collect_array_access_path(var_expr, ctx, db, file) else {
return;
};
let current = resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) = narrow_shape_path(¤t, &path) {
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
}
fn collect_array_access_path(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(ShapeBase, Vec<mir_types::atomic::ArrayKey>)> {
let ExprKind::ArrayAccess(aa) = &expr.kind else {
return None;
};
let idx = aa.index.as_ref()?;
let key = match &idx.kind {
ExprKind::String(s) => {
mir_types::atomic::ArrayKey::String(std::sync::Arc::from(s.as_ref()))
}
ExprKind::Int(i) => mir_types::atomic::ArrayKey::Int(*i),
_ => return None,
};
if let Some(base) = ShapeBase::extract(&aa.array, ctx, db, file) {
Some((base, vec![key]))
} else {
let (base, mut path) = collect_array_access_path(&aa.array, ctx, db, file)?;
path.push(key);
Some((base, path))
}
}
fn resolve_shape_base_current_type(
ctx: &mut FlowState,
base: &ShapeBase,
db: &dyn MirDatabase,
file: &str,
) -> Type {
match base {
ShapeBase::Var(name) => ctx.get_var(name),
ShapeBase::Prop(obj, prop) => resolve_prop_current_type(ctx, obj, prop, db, file),
ShapeBase::Static(fqcn, prop) => resolve_static_prop_current_type(ctx, fqcn, prop, db),
}
}
fn set_shape_base_narrowed(ctx: &mut FlowState, base: &ShapeBase, current: Type, narrowed: Type) {
match base {
ShapeBase::Var(name) => ctx.set_var(name, narrowed),
ShapeBase::Prop(obj, prop) => apply_prop_narrowed(ctx, obj, prop, current, narrowed, false),
ShapeBase::Static(fqcn, prop) => {
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false)
}
}
}
fn narrow_shape_path(ty: &Type, path: &[mir_types::atomic::ArrayKey]) -> Option<Type> {
let (key, rest) = path.split_first()?;
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(key) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(key) {
let mut narrowed_ty = prop.ty.remove_null();
if !rest.is_empty() {
if let Some(deeper) = narrow_shape_path(&narrowed_ty, rest) {
narrowed_ty = deeper;
}
}
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
prop.optional = false;
}
changed = true;
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else if *is_open {
result.add_type(atomic.clone());
} else {
changed = true;
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed && !result.types.is_empty() {
Some(result)
} else {
None
}
}
fn narrow_shape_path_key_exists(
ty: &Type,
path: &[mir_types::atomic::ArrayKey],
key: &mir_types::atomic::ArrayKey,
) -> Option<Type> {
let Some((head, rest)) = path.split_first() else {
let narrowed = add_key_to_sealed_shapes(ty, key);
return if narrowed != *ty {
Some(narrowed)
} else {
None
};
};
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(head) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(head) {
if let Some(deeper) = narrow_shape_path_key_exists(&prop.ty, rest, key) {
prop.ty = deeper;
changed = true;
}
if prop.optional {
prop.optional = false;
changed = true;
}
}
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else {
result.add_type(atomic.clone());
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed {
Some(result)
} else {
None
}
}
fn narrow_shape_path_key_exists_false(
ty: &Type,
path: &[mir_types::atomic::ArrayKey],
key: &mir_types::atomic::ArrayKey,
) -> Option<Type> {
let Some((head, rest)) = path.split_first() else {
let narrowed = remove_key_from_sealed_shapes(ty, key);
return if narrowed != *ty {
Some(narrowed)
} else {
None
};
};
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(head) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(head) {
if let Some(deeper) =
narrow_shape_path_key_exists_false(&prop.ty, rest, key)
{
prop.ty = deeper;
changed = true;
}
if prop.optional {
prop.optional = false;
changed = true;
}
}
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else {
result.add_type(atomic.clone());
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed {
Some(result)
} else {
None
}
}
fn narrow_empty_shape_key(
var_expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
let Some((base, path)) = collect_array_access_path(var_expr, ctx, db, file) else {
return;
};
if path.len() > 1 {
if !is_true {
let current = resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) = narrow_not_empty_shape_path(¤t, &path) {
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
}
return;
}
let key = path
.into_iter()
.next()
.expect("path.len() == 1 checked above");
let current = resolve_shape_base_current_type(ctx, &base, db, file);
let mut changed = false;
let mut result = Type::empty();
for atomic in ¤t.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(&key) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(&key) {
if is_true {
let narrowed_ty = prop.ty.narrow_to_falsy();
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
} else {
let narrowed_ty = prop.ty.narrow_to_truthy();
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
prop.optional = false;
}
}
changed = true;
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else if *is_open || is_true {
result.add_type(atomic.clone());
} else {
changed = true;
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed && !result.types.is_empty() {
set_shape_base_narrowed(ctx, &base, current, result);
}
}
fn narrow_not_empty_shape_path(ty: &Type, path: &[mir_types::atomic::ArrayKey]) -> Option<Type> {
let (key, rest) = path.split_first()?;
let is_last = rest.is_empty();
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(key) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(key) {
if is_last {
let narrowed_ty = prop.ty.narrow_to_truthy();
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
} else {
let mut narrowed_ty = prop.ty.remove_null();
if let Some(deeper) = narrow_not_empty_shape_path(&narrowed_ty, rest) {
narrowed_ty = deeper;
}
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
}
prop.optional = false;
}
changed = true;
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else if *is_open {
result.add_type(atomic.clone());
} else {
changed = true;
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed && !result.types.is_empty() {
Some(result)
} else {
None
}
}
fn extract_null_coalesce(expr: &php_ast::owned::Expr) -> Option<&php_ast::owned::NullCoalesceExpr> {
match &expr.kind {
ExprKind::NullCoalesce(nc) => Some(nc),
ExprKind::Parenthesized(inner) => extract_null_coalesce(inner),
_ => None,
}
}
fn same_literal(a: &php_ast::owned::Expr, b: &php_ast::owned::Expr) -> bool {
let a = peel_parens(a);
let b = peel_parens(b);
match (&a.kind, &b.kind) {
(ExprKind::Null, ExprKind::Null) => true,
(ExprKind::Bool(a), ExprKind::Bool(b)) => a == b,
(ExprKind::Int(a), ExprKind::Int(b)) => a == b,
(ExprKind::String(a), ExprKind::String(b)) => a == b,
_ => false,
}
}
fn peel_parens(expr: &php_ast::owned::Expr) -> &php_ast::owned::Expr {
match &expr.kind {
ExprKind::Parenthesized(inner) => peel_parens(inner),
_ => expr,
}
}
fn extract_class_name(
expr: &php_ast::owned::Expr,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> Option<String> {
match &expr.kind {
ExprKind::Identifier(name) => match name.to_ascii_lowercase().as_str() {
"self" | "static" => self_fqcn.map(|s| s.to_string()),
"parent" => parent_fqcn.map(|s| s.to_string()),
_ => Some(name.to_string()),
},
ExprKind::Variable(name) if name.trim_start_matches('$') == "this" => {
self_fqcn.map(|s| s.to_string())
}
ExprKind::Variable(_) => None, _ => None,
}
}
fn extract_enum_case(
expr: &php_ast::owned::Expr,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
db: &dyn MirDatabase,
file: &str,
) -> Option<(String, String)> {
let spa = match &expr.kind {
ExprKind::StaticPropertyAccess(spa) => spa,
ExprKind::ClassConstAccess(cca) => cca,
_ => return None,
};
let enum_short_name = extract_class_name(&spa.class, self_fqcn, parent_fqcn)?;
let enum_fqcn = crate::db::resolve_name(db, file, &enum_short_name);
let ExprKind::Identifier(case_name) = &spa.member.kind else {
return None;
};
let is_declared_case = matches!(
crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, &enum_fqcn)),
Some(crate::db::ClassLike::Enum(e)) if e.cases.contains_key(case_name.as_ref())
);
if !is_declared_case {
return None;
}
Some((enum_fqcn, case_name.to_string()))
}
fn extract_class_const_fqcn(
cca: &php_ast::owned::StaticAccessExpr,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
db: &dyn MirDatabase,
file: &str,
) -> Option<String> {
let is_class = matches!(&cca.member.kind, ExprKind::Identifier(n) if n.as_ref() == "class");
if !is_class {
return None;
}
let short = extract_class_name(&cca.class, self_fqcn, parent_fqcn)?;
Some(crate::db::resolve_name(db, file, &short))
}
fn promote_assignment_effects(
expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn crate::db::MirDatabase,
file: &str,
) {
match &expr.kind {
ExprKind::Assign(a) => {
if let Some(var_name) = extract_var_name(&a.target) {
let sym = mir_types::Name::from(var_name.as_str());
if ctx.possibly_assigned_vars.contains(&sym) {
let ty = ctx.get_var(&var_name);
ctx.set_var(&var_name, ty);
std::sync::Arc::make_mut(&mut ctx.possibly_assigned_vars).remove(&sym);
}
}
promote_assignment_effects(&a.value, ctx, db, file);
}
ExprKind::UnaryPrefix(u) => {
promote_assignment_effects(&u.operand, ctx, db, file);
}
ExprKind::FunctionCall(call) => {
if let ExprKind::Identifier(fn_name) = &call.name.kind {
let resolved = crate::db::resolve_name(db, file, fn_name.as_ref());
let here = crate::db::Fqcn::from_str(db, &resolved);
if let Some(func) = crate::db::find_function(db, here) {
for (i, param) in func.params.iter().enumerate() {
if param.is_byref {
let arg = call.args.get(i);
if let Some(arg) = arg {
if let ExprKind::Variable(name) = &arg.value.kind {
let var_name = name.as_ref().trim_start_matches('$');
let sym = mir_types::Name::from(var_name);
if ctx.possibly_assigned_vars.contains(&sym) {
let ty = ctx.get_var(var_name);
ctx.set_var(var_name, ty);
std::sync::Arc::make_mut(&mut ctx.possibly_assigned_vars)
.remove(&sym);
}
}
}
}
}
}
}
for arg in call.args.iter() {
promote_assignment_effects(&arg.value, ctx, db, file);
}
}
ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
promote_assignment_effects(&mc.object, ctx, db, file);
for arg in mc.args.iter() {
promote_assignment_effects(&arg.value, ctx, db, file);
}
}
ExprKind::StaticMethodCall(smc) => {
for arg in smc.args.iter() {
promote_assignment_effects(&arg.value, ctx, db, file);
}
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanAnd || b.op == BinaryOp::LogicalAnd => {
promote_assignment_effects(&b.left, ctx, db, file);
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
promote_assignment_effects(&b.left, ctx, db, file);
}
ExprKind::Binary(b) => {
promote_assignment_effects(&b.left, ctx, db, file);
promote_assignment_effects(&b.right, ctx, db, file);
}
ExprKind::Parenthesized(inner) => {
promote_assignment_effects(inner, ctx, db, file);
}
ExprKind::ArrayAccess(aa) => {
promote_assignment_effects(&aa.array, ctx, db, file);
if let Some(idx) = &aa.index {
promote_assignment_effects(idx, ctx, db, file);
}
}
_ => {}
}
}
fn extract_get_class_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("get_class")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
}
None
}
fn extract_get_class_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("get_class")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
}
None
}
enum ScalarArgTarget {
Var(String),
Prop(String, String),
}
impl ScalarArgTarget {
fn extract(expr: &php_ast::owned::Expr) -> Option<Self> {
if let Some(name) = extract_var_name(expr) {
return Some(ScalarArgTarget::Var(name));
}
extract_prop_access(expr).map(|(obj, prop)| ScalarArgTarget::Prop(obj, prop))
}
}
fn extract_gettype_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("gettype")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
}
None
}
fn extract_get_debug_type_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("get_debug_type")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
}
None
}
fn extract_gettype_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("gettype")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
}
None
}
fn extract_get_debug_type_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("get_debug_type")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
}
None
}
fn extract_get_parent_class_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("get_parent_class")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
}
None
}
fn extract_get_parent_class_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
if name
.trim_start_matches('\\')
.eq_ignore_ascii_case("get_parent_class")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
}
None
}
fn extract_class_implements_or_parents_arg(
expr: &php_ast::owned::Expr,
) -> Option<(ScalarArgTarget, bool)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
let bare = name.trim_start_matches('\\');
let is_parents = bare.eq_ignore_ascii_case("class_parents");
if is_parents || bare.eq_ignore_ascii_case("class_implements") {
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value).map(|t| (t, is_parents));
}
}
}
}
None
}
fn extract_class_implements_or_parents_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<((std::sync::Arc<str>, String), bool)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
if let ExprKind::Identifier(name) = &call.name.kind {
let bare = name.trim_start_matches('\\');
let is_parents = bare.eq_ignore_ascii_case("class_parents");
if is_parents || bare.eq_ignore_ascii_case("class_implements") {
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file)
.map(|t| (t, is_parents));
}
}
}
}
None
}
fn narrow_from_get_parent_class_literal(
ctx: &mut FlowState,
target: &ScalarArgTarget,
fqcn: &str,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
match target {
ScalarArgTarget::Var(var_name) => {
if is_true {
let current = ctx.get_var(var_name);
let narrowed =
narrow_strict_subclass_of(¤t, fqcn, db, &ctx.template_param_names);
set_narrowed(ctx, var_name, ¤t, narrowed, false);
}
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_is_subclass_of(ctx, obj, prop, fqcn, db, file, is_true);
narrow_receiver_non_null_on_prop_match(ctx, obj, true);
}
}
}
fn extract_dynamic_class_const_var(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::ClassConstAccess(cca) = &expr.kind {
if matches!(&cca.member.kind, ExprKind::Identifier(n) if n.as_ref() == "class") {
return ScalarArgTarget::extract(&cca.class);
}
}
None
}
fn extract_dynamic_class_const_static_prop_var(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::ClassConstAccess(cca) = &expr.kind {
if matches!(&cca.member.kind, ExprKind::Identifier(n) if n.as_ref() == "class") {
return extract_static_prop_access(&cca.class, ctx, db, file);
}
}
None
}
fn narrow_from_gettype_literal(
ctx: &mut FlowState,
target: &ScalarArgTarget,
literal: &str,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
let type_fn = match literal {
"boolean" => "is_bool",
"integer" => "is_int",
"double" => "is_float",
"string" => "is_string",
"array" => "is_array",
"object" => "is_object",
"NULL" => "is_null",
"resource" | "resource (closed)" => "is_resource",
_ => return,
};
match target {
ScalarArgTarget::Var(var_name) => narrow_from_type_fn(ctx, type_fn, var_name, db, is_true),
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_from_type_fn(ctx, type_fn, obj, prop, db, file, is_true)
}
}
}
fn narrow_from_get_debug_type_literal(
ctx: &mut FlowState,
target: &ScalarArgTarget,
literal: &str,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
let type_fn = match literal {
"null" => Some("is_null"),
"bool" => Some("is_bool"),
"int" => Some("is_int"),
"float" => Some("is_float"),
"string" => Some("is_string"),
"array" => Some("is_array"),
"resource" | "resource (closed)" => Some("is_resource"),
_ => None,
};
if let Some(type_fn) = type_fn {
match target {
ScalarArgTarget::Var(var_name) => {
narrow_from_type_fn(ctx, type_fn, var_name, db, is_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_from_type_fn(ctx, type_fn, obj, prop, db, file, is_true)
}
}
} else {
let fqcn = crate::db::resolve_name(db, file, literal);
match target {
ScalarArgTarget::Var(var_name) => {
narrow_var_to_specific_class(ctx, var_name, &fqcn, is_true, db)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_to_specific_class(ctx, obj, prop, &fqcn, is_true, db, file);
narrow_receiver_non_null_on_prop_match(ctx, obj, is_true);
}
}
}
}
fn narrow_static_prop_from_gettype_literal(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
literal: &str,
is_true: bool,
db: &dyn MirDatabase,
) {
let type_fn = match literal {
"boolean" => "is_bool",
"integer" => "is_int",
"double" => "is_float",
"string" => "is_string",
"array" => "is_array",
"object" => "is_object",
"NULL" => "is_null",
"resource" | "resource (closed)" => "is_resource",
_ => return,
};
narrow_static_prop_from_type_fn(ctx, type_fn, fqcn, prop, db, is_true);
}
fn narrow_static_prop_from_get_debug_type_literal(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
literal: &str,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
let type_fn = match literal {
"null" => Some("is_null"),
"bool" => Some("is_bool"),
"int" => Some("is_int"),
"float" => Some("is_float"),
"string" => Some("is_string"),
"array" => Some("is_array"),
"resource" | "resource (closed)" => Some("is_resource"),
_ => None,
};
if let Some(type_fn) = type_fn {
narrow_static_prop_from_type_fn(ctx, type_fn, fqcn, prop, db, is_true);
} else {
let resolved = crate::db::resolve_name(db, file, literal);
narrow_static_prop_to_specific_class(ctx, fqcn, prop, &resolved, is_true, db);
}
}
trait UnionNarrowExt {
fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type;
}
impl UnionNarrowExt for Type {
fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
let mut result = Type::empty();
result.possibly_undefined = self.possibly_undefined;
result.from_docblock = self.from_docblock;
for atomic in &self.types {
if f(atomic) {
result.types.push(atomic.clone());
}
}
result
}
}
pub(crate) fn is_numeric_string(s: &str) -> bool {
let t = s.trim();
if t.is_empty() {
return false;
}
t.parse::<i64>().is_ok() || t.parse::<f64>().is_ok_and(f64::is_finite)
}
fn extract_count_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("count")
|| bare.eq_ignore_ascii_case("sizeof")
|| bare.eq_ignore_ascii_case("iterator_count")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
None
}
fn extract_count_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("count")
|| bare.eq_ignore_ascii_case("sizeof")
|| bare.eq_ignore_ascii_case("iterator_count")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
None
}
fn extract_array_key_first_or_last_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("array_key_first")
|| bare.eq_ignore_ascii_case("array_key_last")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
None
}
fn extract_array_key_first_or_last_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("array_key_first")
|| bare.eq_ignore_ascii_case("array_key_last")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
None
}
fn narrow_array_key_first_or_last_null(ctx: &mut FlowState, arr_var: &str, is_null: bool) {
let current = ctx.get_var(arr_var);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = if is_null {
non_null.narrow_to_empty_collection()
} else {
non_null.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(arr_var, narrowed);
}
}
fn narrow_prop_array_key_first_or_last_null(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_null: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
narrow_receiver_non_null_on_prop_match(ctx, obj_var, true);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = if is_null {
non_null.narrow_to_empty_collection()
} else {
non_null.narrow_to_non_empty_collection()
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
fn narrow_static_prop_array_key_first_or_last_null(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
is_null: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = if is_null {
non_null.narrow_to_empty_collection()
} else {
non_null.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
}
fn extract_strlen_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("strlen")
|| bare.eq_ignore_ascii_case("mb_strlen")
|| bare.eq_ignore_ascii_case("iconv_strlen")
{
if let Some(arg) = call.args.first() {
return ScalarArgTarget::extract(&arg.value);
}
}
}
None
}
fn extract_strlen_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("strlen")
|| bare.eq_ignore_ascii_case("mb_strlen")
|| bare.eq_ignore_ascii_case("iconv_strlen")
{
if let Some(arg) = call.args.first() {
return extract_static_prop_access(&arg.value, ctx, db, file);
}
}
}
None
}
fn narrow_count_or_strlen_equality(
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
left: &php_ast::owned::Expr,
right: &php_ast::owned::Expr,
op: BinaryOp,
is_true: bool,
) {
let count_on_left = extract_count_arg(left).is_some()
|| extract_count_static_prop_arg(left, ctx, db, file).is_some();
let (count_expr, count_lit) = if count_on_left {
(left, right)
} else {
(right, left)
};
if let (Some(target), Some(n)) = (
extract_count_arg(count_expr),
extract_int_literal(count_lit),
) {
match target {
ScalarArgTarget::Var(arr_var) => {
narrow_array_count_comparison(ctx, &arr_var, op, n, is_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_array_count_comparison(ctx, &obj, &prop, db, file, op, n, is_true)
}
}
return;
} else if let (Some((fqcn, prop)), Some(n)) = (
extract_count_static_prop_arg(count_expr, ctx, db, file),
extract_int_literal(count_lit),
) {
narrow_static_prop_array_count_comparison(ctx, &fqcn, &prop, db, op, n, is_true);
return;
}
let strlen_on_left = extract_strlen_arg(left).is_some()
|| extract_strlen_static_prop_arg(left, ctx, db, file).is_some();
let (strlen_expr, strlen_lit) = if strlen_on_left {
(left, right)
} else {
(right, left)
};
if let (Some(target), Some(n)) = (
extract_strlen_arg(strlen_expr),
extract_int_literal(strlen_lit),
) {
match target {
ScalarArgTarget::Var(str_var) => {
narrow_string_strlen_comparison(ctx, &str_var, op, n, is_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_string_strlen_comparison(ctx, &obj, &prop, db, file, op, n, is_true)
}
}
} else if let (Some((fqcn, prop)), Some(n)) = (
extract_strlen_static_prop_arg(strlen_expr, ctx, db, file),
extract_int_literal(strlen_lit),
) {
narrow_static_prop_string_strlen_comparison(ctx, &fqcn, &prop, db, op, n, is_true);
}
}
fn count_or_strlen_emptiness(op: BinaryOp, n: i64, is_true: bool) -> Option<bool> {
match (op, is_true) {
(BinaryOp::Greater, true) if n >= 0 => Some(true), (BinaryOp::GreaterOrEqual, true) if n >= 1 => Some(true), (BinaryOp::Less, false) if n >= 1 => Some(true), (BinaryOp::LessOrEqual, false) if n >= 0 => Some(true), (BinaryOp::Identical | BinaryOp::Equal, true) if n >= 1 => Some(true),
(BinaryOp::Identical | BinaryOp::Equal, false) if n == 0 => Some(true),
(BinaryOp::NotIdentical | BinaryOp::NotEqual, true) if n == 0 => Some(true),
(BinaryOp::NotIdentical | BinaryOp::NotEqual, false) if n >= 1 => Some(true),
(BinaryOp::Less, true) if n <= 1 => Some(false),
(BinaryOp::LessOrEqual, true) if n <= 0 => Some(false),
(BinaryOp::GreaterOrEqual, false) if n <= 1 => Some(false),
(BinaryOp::Greater, false) if n <= 0 => Some(false),
(BinaryOp::Identical | BinaryOp::Equal, true) if n == 0 => Some(false),
(BinaryOp::NotIdentical | BinaryOp::NotEqual, false) if n == 0 => Some(false),
_ => None,
}
}
fn narrow_array_count_comparison(
ctx: &mut FlowState,
arr_var: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let current = ctx.get_var(arr_var);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = match count_or_strlen_emptiness(op, n, is_true) {
Some(true) => non_null.narrow_to_non_empty_collection(),
Some(false) => non_null.narrow_to_empty_collection(),
None => non_null,
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(arr_var, narrowed);
}
}
fn narrow_string_strlen_comparison(
ctx: &mut FlowState,
str_var: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some(non_empty) = count_or_strlen_emptiness(op, n, is_true) else {
return;
};
let current = ctx.get_var(str_var);
if current.is_mixed() {
return;
}
let base = if non_empty {
current.remove_null()
} else {
current.clone()
};
let narrowed = if non_empty {
narrow_string_to_non_empty(&base)
} else {
narrow_string_to_empty(&base)
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(str_var, narrowed);
}
}
#[allow(clippy::too_many_arguments)]
fn narrow_prop_array_count_comparison(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
narrow_receiver_non_null_on_prop_match(ctx, obj_var, true);
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = match count_or_strlen_emptiness(op, n, is_true) {
Some(true) => non_null.narrow_to_non_empty_collection(),
Some(false) => non_null.narrow_to_empty_collection(),
None => non_null,
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
#[allow(clippy::too_many_arguments)]
fn narrow_prop_string_strlen_comparison(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some(non_empty) = count_or_strlen_emptiness(op, n, is_true) else {
return;
};
if non_empty {
narrow_receiver_non_null_on_prop_match(ctx, obj_var, true);
}
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let base = if non_empty {
current.remove_null()
} else {
current.clone()
};
let narrowed = if non_empty {
narrow_string_to_non_empty(&base)
} else {
narrow_string_to_empty(&base)
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
fn narrow_static_prop_array_count_comparison(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = match count_or_strlen_emptiness(op, n, is_true) {
Some(true) => non_null.narrow_to_non_empty_collection(),
Some(false) => non_null.narrow_to_empty_collection(),
None => non_null,
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
fn narrow_static_prop_string_strlen_comparison(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some(non_empty) = count_or_strlen_emptiness(op, n, is_true) else {
return;
};
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let base = if non_empty {
current.remove_null()
} else {
current.clone()
};
let narrowed = if non_empty {
narrow_string_to_non_empty(&base)
} else {
narrow_string_to_empty(&base)
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
fn haystack_type_from_array_type(var_ty: &Type) -> Option<Type> {
if var_ty.is_mixed() || var_ty.is_empty() {
return None;
}
let mut ty = Type::empty();
for atomic in &var_ty.types {
match atomic {
Atomic::TKeyedArray { properties, .. } => {
for prop in properties.values() {
match &prop.ty.types[..] {
[Atomic::TLiteralString(_)] | [Atomic::TLiteralInt(_)] => {
for a in &prop.ty.types {
ty.add_type(a.clone());
}
}
_ => return None, }
}
}
_ => return None,
}
}
if ty.is_empty() {
None
} else {
Some(ty)
}
}
fn extract_haystack_type(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<Type> {
match &expr.kind {
ExprKind::Array(elements) => {
let mut ty = Type::empty();
for item in elements.iter() {
match &item.value.kind {
ExprKind::String(s) => {
ty.add_type(Atomic::TLiteralString(std::sync::Arc::from(s.as_ref())))
}
ExprKind::Int(n) => ty.add_type(Atomic::TLiteralInt(*n)),
_ => return None, }
}
if ty.is_empty() {
None
} else {
Some(ty)
}
}
ExprKind::Variable(name) => {
let var_name = name.trim_start_matches('$');
haystack_type_from_array_type(&ctx.get_var(var_name))
}
ExprKind::PropertyAccess(_) | ExprKind::NullsafePropertyAccess(_) => {
let (obj, prop) = extract_prop_access(expr)?;
haystack_type_from_array_type(&resolve_prop_current_type(ctx, &obj, &prop, db, file))
}
ExprKind::StaticPropertyAccess(_) => {
let (fqcn, prop) = extract_static_prop_access(expr, ctx, db, file)?;
haystack_type_from_array_type(&resolve_static_prop_current_type(ctx, &fqcn, &prop, db))
}
ExprKind::Parenthesized(inner) => extract_haystack_type(inner, ctx, db, file),
_ => None,
}
}
fn narrow_to_haystack_values(current: &Type, haystack: &Type) -> Type {
let mut out = Type::empty();
for hay_atom in &haystack.types {
let lit_ty = Type::single(hay_atom.clone());
if lit_ty.is_subtype_structural(current) {
out.add_type(hay_atom.clone());
}
}
out
}
fn in_array_loose_narrowing_is_safe(current: &Type, haystack: &Type) -> bool {
fn all(ty: &Type, pred: fn(&Atomic) -> bool) -> bool {
!ty.types.is_empty() && ty.types.iter().all(pred)
}
(all(current, Atomic::is_int) && all(haystack, Atomic::is_int))
|| (all(current, Atomic::is_string) && all(haystack, Atomic::is_string))
}
#[cfg(test)]
mod tests {
use super::is_numeric_string;
#[test]
fn numeric_strings_are_recognized() {
assert!(is_numeric_string("42"));
assert!(is_numeric_string("-42"));
assert!(is_numeric_string("+42"));
assert!(is_numeric_string("3.14"));
assert!(is_numeric_string(".5"));
assert!(is_numeric_string("1e10"));
assert!(is_numeric_string(" 123 "));
}
#[test]
fn non_numeric_strings_are_rejected() {
assert!(!is_numeric_string(""));
assert!(!is_numeric_string(" "));
assert!(!is_numeric_string("hello"));
assert!(!is_numeric_string("0x1A"));
assert!(!is_numeric_string("12abc"));
}
#[test]
fn nan_and_infinity_keywords_are_not_numeric() {
assert!(!is_numeric_string("NAN"));
assert!(!is_numeric_string("nan"));
assert!(!is_numeric_string("INF"));
assert!(!is_numeric_string("-INF"));
assert!(!is_numeric_string("Infinity"));
assert!(!is_numeric_string("-Infinity"));
}
}