use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::core::{
apply_prop_narrowed, is_numeric_string, narrow_receiver_non_null_on_prop_match,
resolve_prop_current_type, resolve_static_prop_current_type, set_narrowed, UnionNarrowExt,
};
use super::literals::narrow_string_to_non_empty;
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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);
}
pub(super) 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.as_ref()))
})
}
}
"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,
})
}
pub(super) 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
}