use mir_types::{Atomic, Type};
use php_ast::ast::BinaryOp;
use php_ast::owned::ExprKind;
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::super::core::{
apply_prop_narrowed, count_or_strlen_emptiness, extract_static_prop_access,
narrow_receiver_non_null_on_prop_match, resolve_prop_current_type,
resolve_static_prop_current_type, ScalarArgTarget,
};
use super::super::literals::{extract_int_literal, flip_comparison_op};
pub(crate) 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() {
let value = arg.value.as_ref()?;
return ScalarArgTarget::extract(value);
}
}
}
None
}
pub(crate) 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() {
let value = arg.value.as_ref()?;
return extract_static_prop_access(value, ctx, db, file);
}
}
}
None
}
pub(crate) 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() {
let value = arg.value.as_ref()?;
return ScalarArgTarget::extract(value);
}
}
}
None
}
pub(crate) 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() {
let value = arg.value.as_ref()?;
return extract_static_prop_access(value, ctx, db, file);
}
}
}
None
}
pub(crate) 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);
}
}
pub(crate) 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);
}
pub(crate) 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 drop_unsatisfiable_shapes(ty: Type, op: BinaryOp, n: i64, is_true: bool) -> Type {
let satisfiable = |atom: &Atomic| {
let Atomic::TKeyedArray {
properties,
is_open: false,
..
} = atom
else {
return true;
};
let hi = properties.len() as i64;
let lo = properties.values().filter(|p| !p.optional).count() as i64;
let op = if is_true { op } else { negate_comparison(op) };
match op {
BinaryOp::Identical | BinaryOp::Equal => lo <= n && n <= hi,
BinaryOp::NotIdentical | BinaryOp::NotEqual => !(lo == hi && lo == n),
BinaryOp::Less => lo < n,
BinaryOp::LessOrEqual => lo <= n,
BinaryOp::Greater => hi > n,
BinaryOp::GreaterOrEqual => hi >= n,
_ => true,
}
};
if ty.types.iter().all(satisfiable) {
return ty;
}
let mut kept = ty.clone();
kept.types.retain(|a| satisfiable(a));
if kept.is_empty() {
ty
} else {
kept
}
}
fn negate_comparison(op: BinaryOp) -> BinaryOp {
match op {
BinaryOp::Identical => BinaryOp::NotIdentical,
BinaryOp::NotIdentical => BinaryOp::Identical,
BinaryOp::Equal => BinaryOp::NotEqual,
BinaryOp::NotEqual => BinaryOp::Equal,
BinaryOp::Less => BinaryOp::GreaterOrEqual,
BinaryOp::GreaterOrEqual => BinaryOp::Less,
BinaryOp::LessOrEqual => BinaryOp::Greater,
BinaryOp::Greater => BinaryOp::LessOrEqual,
other => other,
}
}
pub(crate) 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 = drop_unsatisfiable_shapes(current.remove_null(), op, n, is_true);
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);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) 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 = drop_unsatisfiable_shapes(current.remove_null(), op, n, is_true);
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);
}
pub(crate) 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 = drop_unsatisfiable_shapes(current.remove_null(), op, n, is_true);
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);
}
pub(crate) fn narrow_array_count_condition(
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_call_on_left = extract_count_arg(left).is_some()
|| extract_count_static_prop_arg(left, ctx, db, file).is_some();
let (count_expr, count_cmp_op, count_lit) = if count_call_on_left {
(left, op, right)
} else {
(right, flip_comparison_op(op), 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);
}
}