use php_ast::owned::{ExprKind, FunctionCallExpr};
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::super::core::{
apply_prop_narrowed, extract_any_prop_access, extract_static_prop_access, extract_var_name,
narrow_receiver_non_null_on_prop_match, resolve_prop_current_type,
resolve_static_prop_current_type, UnionNarrowExt,
};
use super::super::literals::is_truthy_bool_literal;
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, }
}
}
Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
if value.types.is_empty()
|| !value
.types
.iter()
.all(|a| matches!(a, Atomic::TLiteralString(_) | Atomic::TLiteralInt(_)))
{
return None;
}
for a in &value.types {
ty.add_type(a.clone());
}
}
_ => return None,
}
}
if ty.is_empty() {
None
} else {
Some(ty)
}
}
pub(crate) 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_any_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,
}
}
pub(crate) 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
}
pub(crate) fn haystack_admits_null_loosely(haystack: &Type) -> bool {
haystack.types.iter().any(|a| {
matches!(a, Atomic::TLiteralInt(0))
|| matches!(a, Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0")
})
}
pub(crate) 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)
}
let current_sans_null;
let current = if current.contains(|a| matches!(a, Atomic::TNull)) {
if haystack_admits_null_loosely(haystack) {
return false;
}
current_sans_null = current.remove_null();
¤t_sans_null
} else {
current
};
(all(current, Atomic::is_int) && all(haystack, Atomic::is_int))
|| (all(current, Atomic::is_string) && all(haystack, Atomic::is_string))
}
pub(crate) fn strip_haystack_null(
haystack_expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) {
if let Some(var_name) = extract_var_name(haystack_expr) {
let current = ctx.get_var(&var_name);
if current.is_nullable() {
ctx.set_var(&var_name, current.remove_null());
}
} else if let Some((obj, prop)) = extract_any_prop_access(haystack_expr) {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
if current.is_nullable() {
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
}
} else if let Some((fqcn, prop)) = extract_static_prop_access(haystack_expr, ctx, db, file) {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
if current.is_nullable() {
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
}
pub(crate) fn narrow_in_array_condition(
ctx: &mut FlowState,
call: &FunctionCallExpr,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
if let Some(haystack_arg) = call.args.get(1) {
strip_haystack_null(&haystack_arg.value, ctx, db, file);
}
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 {
if strict || !haystack_admits_null_loosely(&haystack_ty) {
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);
}
}
}
}
}
}
}