use php_ast::ast::{BinaryOp, UnaryPrefixOp};
use php_ast::owned::ExprKind;
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::core::{
apply_prop_narrowed, extract_static_prop_access, is_numeric_string,
narrow_receiver_non_null_on_prop_match, peel_parens, resolve_prop_current_type,
resolve_static_prop_current_type, set_narrowed, ScalarArgTarget, UnionNarrowExt,
};
pub(super) fn is_truthy_bool_literal(expr: &php_ast::owned::Expr) -> bool {
matches!(expr.kind, php_ast::owned::ExprKind::Bool(true))
}
pub(super) 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,
},
},
}
}
pub(super) 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,
}
}
pub(super) 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,
}
}
pub(super) 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)]
pub(super) 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);
}
pub(super) 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,
});
}
pub(super) 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
}
pub(super) 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,
})
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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(super) 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);
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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))
}
}
pub(super) 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);
}
pub(super) fn atom_safe_for_loose_int_narrowing(a: &Atomic, value: i64) -> bool {
a.is_int() || (matches!(a, Atomic::TNull) && value != 0)
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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
}
}