use std::sync::Arc;
use indexmap::IndexMap;
use php_ast::owned::{Expr, ExprKind};
use php_ast::Span;
use mir_issues::{IssueKind, Severity};
use mir_types::atomic::ArrayKey;
use mir_types::{Atomic, Type};
use crate::expr::ExpressionAnalyzer;
use crate::flow_state::FlowState;
pub(crate) fn is_callable_array_pair(arg: &Type) -> bool {
arg.types.iter().any(|a| {
let Atomic::TKeyedArray { properties, .. } = a else {
return false;
};
if properties.len() != 2 {
return false;
}
let first = properties.get(&mir_types::atomic::ArrayKey::Int(0));
let second = properties.get(&mir_types::atomic::ArrayKey::Int(1));
let (Some(first), Some(second)) = (first, second) else {
return false;
};
let first_ok = first.ty.contains(|t| {
matches!(
t,
Atomic::TNamedObject { .. }
| Atomic::TObject
| Atomic::TSelf { .. }
| Atomic::TStaticObject { .. }
| Atomic::TClassString(_)
| Atomic::TMixed
)
});
let second_ok = second.ty.contains(|t| {
matches!(
t,
Atomic::TString | Atomic::TNonEmptyString | Atomic::TLiteralString(_)
)
});
first_ok && second_ok
})
}
pub(crate) fn check_array_map_callback(
ea: &mut ExpressionAnalyzer<'_>,
arg_types: &[Type],
arg_spans: &[Span],
) {
if arg_types.is_empty() || arg_spans.is_empty() {
return;
}
let callback_ty = &arg_types[0];
let callback_span = arg_spans[0];
if !super::callable::is_valid_callable_type(callback_ty) {
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: "array_map".to_string(),
expected: "callable".to_string(),
actual: callback_ty.to_string(),
},
Severity::Error,
callback_span,
);
return;
}
super::callable::record_callable_string_ref(ea, callback_ty, callback_span);
if arg_types.len() > 1 {
validate_callback_arity(ea, callback_ty, callback_span, arg_types.len() - 1);
}
}
fn validate_callback_arity(
ea: &mut ExpressionAnalyzer<'_>,
callback_ty: &Type,
callback_span: Span,
expected_arity: usize,
) {
if let Some(params) = super::callable::extract_callable_params(callback_ty, ea) {
let required_count = params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
if required_count > expected_arity {
let fn_name = callback_name_for_diagnostic(callback_ty);
ea.emit(
IssueKind::TooFewArguments {
fn_name,
expected: required_count,
actual: expected_arity,
},
Severity::Error,
callback_span,
);
}
}
}
const ARRAY_FILTER_USE_BOTH: i64 = 1; const ARRAY_FILTER_USE_KEY: i64 = 2;
pub(crate) fn check_array_filter_callback(
ea: &mut ExpressionAnalyzer<'_>,
arg_types: &[Type],
arg_spans: &[Span],
) {
if arg_types.len() < 2 || arg_spans.len() < 2 {
return;
}
let callback_ty = &arg_types[1];
let callback_span = arg_spans[1];
if !super::callable::is_valid_callable_type(callback_ty) {
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: "array_filter".to_string(),
expected: "callable".to_string(),
actual: callback_ty.to_string(),
},
Severity::Error,
callback_span,
);
return;
}
super::callable::record_callable_string_ref(ea, callback_ty, callback_span);
let expected_arity = if arg_types.len() > 2 {
match arg_types[2].types.first() {
Some(Atomic::TLiteralInt(ARRAY_FILTER_USE_BOTH)) => 2,
Some(Atomic::TLiteralInt(ARRAY_FILTER_USE_KEY)) => 1,
_ => 1,
}
} else {
1
};
if let Some(params) = super::callable::extract_callable_params(callback_ty, ea) {
let required_count = params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
if required_count > expected_arity {
let expected_plural = if expected_arity == 1 { "" } else { "s" };
let actual_plural = if required_count == 1 { "" } else { "s" };
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: "array_filter".to_string(),
expected: format!(
"callable accepting {} argument{}",
expected_arity, expected_plural
),
actual: format!(
"callable accepting {} argument{}",
required_count, actual_plural
),
},
Severity::Error,
callback_span,
);
}
}
}
fn callable_return_type(
union: &Type,
ea: &ExpressionAnalyzer<'_>,
ctx: &FlowState,
callback_expr: Option<&Expr>,
) -> Option<Type> {
for atomic in &union.types {
match atomic {
Atomic::TClosure { data } => return Some(data.return_type.clone()),
Atomic::TCallable {
return_type: Some(rt),
..
} => return Some((**rt).clone()),
Atomic::TCallable {
return_type: None, ..
} => {
if let Some(rt) = resolve_opaque_callback_via_callers(ea, ctx, callback_expr) {
return Some(rt);
}
}
Atomic::TLiteralString(fn_name) if !fn_name.is_empty() => {
let here = crate::db::Fqcn::from_str(ea.db, fn_name.as_ref());
if let Some(f) = crate::db::find_function(ea.db, here) {
if let Some(rt) = &f.return_type {
return Some((**rt).clone());
}
}
}
Atomic::TIntersection { parts } => {
for part in parts.iter() {
if let Some(rt) = callable_return_type(part, ea, ctx, callback_expr) {
return Some(rt);
}
}
}
_ => {}
}
}
None
}
fn resolve_opaque_callback_via_callers(
ea: &ExpressionAnalyzer<'_>,
ctx: &FlowState,
callback_expr: Option<&Expr>,
) -> Option<Type> {
let ExprKind::Variable(name) = &callback_expr?.kind else {
return None;
};
let var_name = name.trim_start_matches('$');
let fqn = ctx.current_function_fqn.as_ref()?;
let f = crate::db::find_function(ea.db, crate::db::Fqcn::from_str(ea.db, fqn))?;
let index = f.params.iter().position(|p| p.name.as_ref() == var_name)?;
let is_bare_callable = f.params[index].ty.as_ref().is_some_and(|t| {
t.types.len() == 1
&& matches!(
&t.types[0],
Atomic::TCallable {
return_type: None,
..
}
)
});
if !is_bare_callable {
return None;
}
let callee = super::opaque_callback::CalleeKey::Function(Arc::clone(fqn));
super::opaque_callback::opaque_callback_return_type(ea.db, &callee, index as u16)
}
pub(crate) fn array_fill_return_type(arg_types: &[Type]) -> Option<Type> {
let start = arg_types.first()?;
let count = arg_types.get(1)?;
let value = arg_types.get(2)?;
let count_is_positive = !count.types.is_empty()
&& count.types.iter().all(|a| match a {
Atomic::TLiteralInt(n) => *n >= 1,
Atomic::TPositiveInt => true,
Atomic::TIntRange { min, .. } => min.is_some_and(|m| m >= 1),
_ => false,
});
if !count_is_positive {
return None;
}
let start_is_zero = matches!(start.types.as_slice(), [Atomic::TLiteralInt(0)]);
if start_is_zero {
return Some(Type::single(Atomic::TNonEmptyList {
value: Box::new(value.clone()),
}));
}
let start_is_known_non_zero = !start.types.is_empty()
&& start.types.iter().all(|a| match a {
Atomic::TLiteralInt(n) => *n != 0,
Atomic::TPositiveInt | Atomic::TNegativeInt => true,
Atomic::TIntRange { min, max } => {
min.is_some_and(|m| m > 0) || max.is_some_and(|m| m < 0)
}
_ => false,
});
if start_is_known_non_zero {
return Some(Type::single(Atomic::TNonEmptyArray {
key: Box::new(Type::single(Atomic::TInt)),
value: Box::new(value.clone()),
}));
}
None
}
pub(crate) fn array_keys_return_type(arg_types: &[Type], return_ty: &Type) -> Type {
let Some(arr) = arg_types.first() else {
return return_ty.clone();
};
if !super::callable::is_non_empty_collection(arr) {
return return_ty.clone();
}
let mut result = Type::empty();
result.from_docblock = return_ty.from_docblock;
for atomic in &return_ty.types {
match atomic {
Atomic::TList { value } => {
result.add_type(Atomic::TNonEmptyList {
value: value.clone(),
});
}
other => result.add_type(other.clone()),
}
}
if result.is_empty() {
return_ty.clone()
} else {
result
}
}
pub(crate) fn array_reverse_return_type(arg_types: &[Type]) -> Option<Type> {
let arr = arg_types.first()?;
if arr.is_mixed() {
return None;
}
let (key, value) = crate::stmt::infer_foreach_types(arr);
if value.is_mixed() {
return None;
}
let preserve_keys = arg_types.get(1).is_some_and(|t| {
t.types
.iter()
.any(|a| matches!(a, Atomic::TTrue | Atomic::TBool))
&& !t
.types
.iter()
.any(|a| matches!(a, Atomic::TFalse | Atomic::TNull))
});
let key_is_int_only = !key.is_mixed() && key.types.iter().all(Atomic::is_int);
if key_is_int_only && !preserve_keys {
let atomic = if super::callable::is_non_empty_collection(arr) {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
return Some(Type::single(atomic));
}
if key.is_mixed() {
return None;
}
let atomic = if super::callable::is_non_empty_collection(arr) {
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
}
} else {
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
pub(crate) fn infer_array_map_return(
ea: &ExpressionAnalyzer<'_>,
arg_types: &[Type],
ctx: &FlowState,
callback_expr: Option<&Expr>,
) -> Option<Type> {
let callback = arg_types.first()?;
if callback.types.iter().any(|a| matches!(a, Atomic::TNull)) {
return None;
}
let value = callable_return_type(callback, ea, ctx, callback_expr)?;
if value
.types
.iter()
.any(|a| matches!(a, Atomic::TVoid | Atomic::TNever))
{
return None;
}
if arg_types.len() == 2 {
let source = &arg_types[1];
let src_is_list = !source.types.is_empty()
&& source.types.iter().all(|a| {
matches!(
a,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
});
let src_is_non_empty = !source.types.is_empty()
&& source.types.iter().all(|a| {
matches!(
a,
Atomic::TNonEmptyArray { .. } | Atomic::TNonEmptyList { .. }
) || matches!(a, Atomic::TKeyedArray { properties, .. } if !properties.is_empty())
});
let atom = match (src_is_list, src_is_non_empty) {
(true, true) => Atomic::TNonEmptyList {
value: Box::new(value),
},
(true, false) => Atomic::TList {
value: Box::new(value),
},
(false, true) => {
let (k, _) = crate::stmt::infer_foreach_types(source);
let key = if k.is_mixed() { Type::array_key() } else { k };
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
}
}
(false, false) => {
let (k, _) = crate::stmt::infer_foreach_types(source);
let key = if k.is_mixed() { Type::array_key() } else { k };
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
}
};
Some(Type::single(atom))
} else {
let src_is_non_empty = arg_types.get(1).is_some_and(|t| {
!t.types.is_empty()
&& t.types.iter().all(|a| {
matches!(
a,
Atomic::TNonEmptyArray { .. } | Atomic::TNonEmptyList { .. }
)
})
});
let key = Type::single(Atomic::TInt);
let atom = if src_is_non_empty {
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
}
} else {
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
};
Some(Type::single(atom))
}
}
pub(crate) fn infer_array_reduce_return(
ea: &ExpressionAnalyzer<'_>,
arg_types: &[Type],
ctx: &FlowState,
callback_expr: Option<&Expr>,
) -> Option<Type> {
let callback = arg_types.get(1)?;
let mut result = callable_return_type(callback, ea, ctx, callback_expr)?;
if result
.types
.iter()
.any(|a| matches!(a, Atomic::TVoid | Atomic::TNever))
{
return None;
}
match arg_types.get(2) {
Some(initial) => result.merge_with(initial),
None => result.merge_with(&Type::null()),
}
Some(result)
}
pub(crate) fn infer_array_filter_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (key, value) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() && value.is_mixed() {
return None;
}
Some(Type::single(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}))
}
pub(crate) fn array_slice_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let preserve_keys = arg_types.get(3).is_some_and(|t| {
t.types
.iter()
.any(|a| matches!(a, Atomic::TTrue | Atomic::TBool))
&& !t
.types
.iter()
.any(|a| matches!(a, Atomic::TFalse | Atomic::TNull))
});
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
let is_source_list = source.types.iter().all(|a| {
matches!(
a,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
});
if is_source_list && !preserve_keys {
return Some(Type::single(Atomic::TList {
value: Box::new(value),
}));
}
let (key, _) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() {
return None;
}
Some(Type::single(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}))
}
pub(crate) fn infer_array_values_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
let atomic = if super::callable::is_non_empty_collection(source) {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
pub(crate) fn infer_array_merge_return(arg_types: &[Type]) -> Option<Type> {
if arg_types.is_empty() {
return None;
}
let all_lists = arg_types.iter().all(|t| {
!t.types.is_empty()
&& t.types.iter().all(|a| {
matches!(
a,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
})
});
if !all_lists {
return None;
}
let mut value = Type::empty();
for arg in arg_types {
let (_, v) = crate::stmt::infer_foreach_types(arg);
value.merge_with(&v);
}
if value.is_empty() || value.is_mixed() {
return None;
}
let any_non_empty = arg_types
.iter()
.any(super::callable::is_non_empty_collection);
let atomic = if any_non_empty {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
pub(crate) fn array_merge_recursive_return_type(arg_types: &[Type]) -> Option<Type> {
infer_array_merge_return(arg_types)
}
pub(crate) fn array_unique_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (key, value) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() && value.is_mixed() {
return None;
}
let atomic = if super::callable::is_non_empty_collection(source) {
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
}
} else {
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
pub(crate) fn array_key_first_last_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() || !super::callable::is_non_empty_collection(source) {
return None;
}
let all_list = source
.types
.iter()
.all(|a| matches!(a, Atomic::TNonEmptyList { .. } | Atomic::TList { .. }));
if all_list {
Some(Type::single(Atomic::TInt))
} else {
let mut ty = Type::single(Atomic::TInt);
ty.add_type(Atomic::TString);
Some(ty)
}
}
pub(crate) fn array_pop_shift_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
if super::callable::is_non_empty_collection(source) {
Some(value)
} else {
let mut ty = value;
ty.add_type(Atomic::TNull);
Some(ty)
}
}
pub(crate) fn array_reset_end_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
if super::callable::is_non_empty_collection(source) {
Some(value)
} else {
let mut ty = value;
ty.add_type(Atomic::TFalse);
Some(ty)
}
}
pub(crate) fn array_current_next_prev_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
let mut ty = value;
ty.add_type(Atomic::TFalse);
Some(ty)
}
pub(crate) fn sort_byref_type(arr: &Type, reindex: bool) -> Type {
if arr.is_mixed() {
return arr.clone();
}
if !reindex {
return arr.clone();
}
let (_, value) = crate::stmt::infer_foreach_types(arr);
if value.is_mixed() {
return arr.clone();
}
let atom = if super::callable::is_non_empty_collection(arr) {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
Type::single(atom)
}
pub(crate) fn array_search_return_type(arg_types: &[Type]) -> Option<Type> {
let haystack = arg_types.get(1)?;
if haystack.is_mixed() {
return None;
}
let (key, _) = crate::stmt::infer_foreach_types(haystack);
if key.is_mixed() {
return None;
}
let mut result = key;
result.add_type(Atomic::TFalse);
Some(result)
}
pub(crate) fn array_key_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (key, _) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() {
return None;
}
let mut result = key;
result.add_type(Atomic::TNull);
Some(result)
}
pub(crate) fn array_fill_keys_return_type(arg_types: &[Type]) -> Option<Type> {
let keys_arr = arg_types.first()?;
let value_ty = arg_types.get(1)?;
if keys_arr.is_mixed() || value_ty.is_mixed() {
return None;
}
let (_, key_of_result) = crate::stmt::infer_foreach_types(keys_arr);
if key_of_result.is_mixed() {
return None;
}
let atom = if super::callable::is_non_empty_collection(keys_arr) {
Atomic::TNonEmptyArray {
key: Box::new(key_of_result),
value: Box::new(value_ty.clone()),
}
} else {
Atomic::TArray {
key: Box::new(key_of_result),
value: Box::new(value_ty.clone()),
}
};
Some(Type::single(atom))
}
pub(crate) fn array_chunk_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (key, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
let preserve_keys = arg_types.get(2).is_some_and(|t| {
t.types
.iter()
.any(|a| matches!(a, Atomic::TTrue | Atomic::TBool))
&& !t
.types
.iter()
.any(|a| matches!(a, Atomic::TFalse | Atomic::TNull))
});
let chunk_atom = if preserve_keys {
if key.is_mixed() {
return None;
}
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
let chunk_ty = Type::single(chunk_atom);
let outer_atom = if super::callable::is_non_empty_collection(source) {
Atomic::TNonEmptyList {
value: Box::new(chunk_ty),
}
} else {
Atomic::TList {
value: Box::new(chunk_ty),
}
};
Some(Type::single(outer_atom))
}
pub(crate) fn array_diff_intersect_like_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (key, value) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() && value.is_mixed() {
return None;
}
Some(Type::single(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}))
}
pub(crate) fn array_combine_return_type(arg_types: &[Type]) -> Option<Type> {
let keys_arr = arg_types.first()?;
let values_arr = arg_types.get(1)?;
let (_, keys_values) = crate::stmt::infer_foreach_types(keys_arr);
let key = crate::expr::helpers::coerce_array_key_type(&keys_values);
let (_, value) = crate::stmt::infer_foreach_types(values_arr);
if key.is_mixed() && value.is_mixed() {
return None;
}
let atomic = if super::callable::is_non_empty_collection(keys_arr) {
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
}
} else {
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
pub(crate) fn array_count_values_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() || value.types.is_empty() {
return None;
}
if !value.types.iter().all(|a| a.is_int() || a.is_string()) {
return None;
}
let key = crate::expr::helpers::coerce_array_key_type(&value);
let count = Type::single(Atomic::TIntRange {
min: Some(1),
max: None,
});
let atomic = if super::callable::is_non_empty_collection(source) {
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(count),
}
} else {
Atomic::TArray {
key: Box::new(key),
value: Box::new(count),
}
};
Some(Type::single(atomic))
}
pub(crate) fn array_change_key_case_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let is_shape = source.types.len() == 1 && matches!(source.types[0], Atomic::TKeyedArray { .. });
if !is_shape {
if source.types.iter().all(|a| {
matches!(
a,
Atomic::TArray { .. }
| Atomic::TList { .. }
| Atomic::TNonEmptyArray { .. }
| Atomic::TNonEmptyList { .. }
)
}) {
return Some(source.clone());
}
return None;
}
let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = &source.types[0]
else {
unreachable!("is_shape checked above");
};
let has_string_key = properties.keys().any(|k| matches!(k, ArrayKey::String(_)));
if !has_string_key {
return Some(source.clone());
}
let case_arg = arg_types.get(1);
let lower = match case_arg {
None => true,
Some(t) => match t.types.as_slice() {
[Atomic::TLiteralInt(0)] => true,
[Atomic::TLiteralInt(1)] => false,
_ => return None,
},
};
let mut new_properties = IndexMap::new();
for (k, prop) in properties.iter() {
let new_key = match k {
ArrayKey::String(s) => {
let folded: Arc<str> = if lower {
s.to_lowercase().into()
} else {
s.to_uppercase().into()
};
ArrayKey::String(folded)
}
ArrayKey::Int(_) => k.clone(),
};
new_properties.insert(new_key, prop.clone());
}
Some(Type::single(Atomic::TKeyedArray {
properties: Box::new(new_properties),
is_open: *is_open,
is_list: *is_list,
}))
}
pub(crate) fn array_splice_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, value) = crate::stmt::infer_foreach_types(source);
if value.is_mixed() {
return None;
}
let is_source_list = source.types.iter().all(|a| {
matches!(
a,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
});
if is_source_list {
return Some(Type::single(Atomic::TList {
value: Box::new(value),
}));
}
let (key, _) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() {
return None;
}
Some(Type::single(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}))
}
pub(crate) fn array_pad_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let is_source_list = !source.types.is_empty()
&& source.types.iter().all(|a| {
matches!(
a,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
});
if !is_source_list {
return None;
}
let (_, source_value) = crate::stmt::infer_foreach_types(source);
if source_value.is_mixed() {
return None;
}
let pad_value = arg_types.get(2)?.clone();
let mut value = source_value;
value.merge_with(&pad_value);
let length_forces_non_empty = arg_types
.get(1)
.is_some_and(|t| matches!(t.types.as_slice(), [Atomic::TLiteralInt(n)] if *n != 0));
let non_empty = super::callable::is_non_empty_collection(source) || length_forces_non_empty;
let atomic = if non_empty {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
pub(crate) fn array_column_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (_, row) = crate::stmt::infer_foreach_types(source);
if row.types.len() != 1 {
return None;
}
let Atomic::TKeyedArray { properties, .. } = &row.types[0] else {
return None;
};
let column_key_ty = arg_types.get(1)?;
let column_key = match column_key_ty.types.as_slice() {
[Atomic::TLiteralString(s)] => Some(ArrayKey::String(s.clone())),
[Atomic::TLiteralInt(i)] => Some(ArrayKey::Int(*i)),
[Atomic::TNull] => None,
_ => return None,
};
let (value, column_optional) = match &column_key {
Some(key) => {
let column_prop = properties.get(key)?;
(column_prop.ty.clone(), column_prop.optional)
}
None => (row.clone(), false),
};
let index_arg = arg_types.get(2);
let is_no_index = match index_arg {
None => true,
Some(t) => matches!(t.types.as_slice(), [Atomic::TNull]),
};
if is_no_index {
let non_empty = super::callable::is_non_empty_collection(source) && !column_optional;
let atomic = if non_empty {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
return Some(Type::single(atomic));
}
let index_key_ty = index_arg?;
let index_key = match index_key_ty.types.as_slice() {
[Atomic::TLiteralString(s)] => ArrayKey::String(s.clone()),
[Atomic::TLiteralInt(i)] => ArrayKey::Int(*i),
_ => return None,
};
let index_prop = properties.get(&index_key)?;
let key = crate::expr::helpers::coerce_array_key_type(&index_prop.ty);
let non_empty = super::callable::is_non_empty_collection(source)
&& !column_optional
&& !index_prop.optional;
let atomic = if non_empty {
Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
}
} else {
Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
fn callback_name_for_diagnostic(callback_ty: &Type) -> String {
if let Some(Atomic::TLiteralString(fn_name)) = callback_ty.types.first() {
fn_name.to_string()
} else {
"(closure)".to_string()
}
}
pub(crate) fn compact_return_type(ctx: &FlowState, args: &[php_ast::owned::Arg]) -> Option<Type> {
use mir_types::atomic::KeyedProperty;
let mut properties = IndexMap::new();
for arg in args {
if arg.unpack {
return None;
}
let Some(value) = &arg.value else {
return None;
};
let ExprKind::String(name) = &value.kind else {
return None;
};
let ty = ctx.get_var(name.as_ref());
let optional = ty.possibly_undefined;
properties.insert(
ArrayKey::String(std::sync::Arc::from(name.as_ref())),
KeyedProperty { ty, optional },
);
}
if properties.is_empty() {
return None;
}
Some(Type::single(Atomic::TKeyedArray {
properties: Box::new(properties),
is_open: false,
is_list: false,
}))
}
pub(crate) fn array_rand_return_type(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() {
return None;
}
let (key, _) = crate::stmt::infer_foreach_types(source);
if key.is_mixed() {
return None;
}
match arg_types.get(1) {
None => Some(key),
Some(t) => match t.types.as_slice() {
[Atomic::TLiteralInt(1)] => Some(key),
[Atomic::TLiteralInt(n)] if *n > 1 => Some(Type::single(Atomic::TNonEmptyList {
value: Box::new(key),
})),
_ => None,
},
}
}
pub(crate) fn array_push_unshift_byref_type(
arr: &Type,
push_types: &[Type],
inside_loop: bool,
) -> Type {
if arr.is_mixed() || push_types.is_empty() {
return arr.clone();
}
if !arr.types.is_empty()
&& arr
.types
.iter()
.all(|a| matches!(a, Atomic::TKeyedArray { properties, .. } if properties.is_empty()))
{
let mut current = arr.clone();
for pushed in push_types {
if pushed.is_mixed() {
return arr.clone();
}
current =
crate::expr::helpers::widen_array_as_list(¤t, pushed, inside_loop, None);
}
return current;
}
let (_, src_value) = crate::stmt::infer_foreach_types(arr);
let mut value = src_value;
for pushed in push_types {
if pushed.is_mixed() {
return arr.clone();
}
value.merge_with(pushed);
}
if value.is_empty() || value.is_mixed() {
return arr.clone();
}
let is_src_list = !arr.types.is_empty()
&& arr.types.iter().all(|a| {
matches!(
a,
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { is_list: true, .. }
)
});
if is_src_list {
return Type::single(Atomic::TNonEmptyList {
value: Box::new(value),
});
}
let (key, _) = crate::stmt::infer_foreach_types(arr);
if key.is_mixed() {
return arr.clone();
}
Type::single(Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
})
}