use php_ast::ast::{BinaryOp, UnaryPrefixOp};
use php_ast::owned::ExprKind;
use mir_types::Type;
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::core::{
apply_prop_narrowed, extract_class_name, extract_prop_access, extract_static_prop_access_parts,
extract_var_name, narrow_receiver_non_null_on_prop_match, peel_parens,
resolve_prop_current_type, resolve_static_prop_current_type, set_narrowed,
};
use super::instanceof_core::narrow_or_instanceof_union;
use super::narrow_from_condition;
use super::type_fn::{
narrow_from_type_fn, narrow_prop_from_type_fn, narrow_static_prop_from_type_fn,
};
#[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.as_ref()?)?;
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.as_ref()?)?;
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.as_ref()?,
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;
}
call.args.first()?.value.as_ref()
}
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))
}
pub(super) 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);
}
}
pub(super) 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));
}
}
}
}