use php_ast::Span;
use mir_issues::{IssueKind, Severity};
use mir_types::atomic::FnParam;
use mir_types::{Atomic, Type};
use crate::expr::ExpressionAnalyzer;
#[derive(Clone)]
pub(crate) struct ParamInfo {
pub(crate) is_optional: bool,
pub(crate) is_variadic: bool,
}
pub(crate) fn extract_callable_params(
union: &Type,
ea: &ExpressionAnalyzer<'_>,
) -> Option<Vec<ParamInfo>> {
if union
.types
.iter()
.any(|a| matches!(a, Atomic::TCallable { params: None, .. }))
{
return None;
}
for atomic in &union.types {
match atomic {
Atomic::TClosure { params, .. } => {
return Some(
params
.iter()
.map(|p| ParamInfo {
is_optional: p.is_optional,
is_variadic: p.is_variadic,
})
.collect(),
);
}
Atomic::TLiteralString(fn_name) => {
if fn_name.is_empty() {
continue;
}
let here = crate::db::Fqcn::from_str(ea.db, fn_name.as_ref());
if let Some(f) = crate::db::find_function(ea.db, here) {
return Some(
f.params
.iter()
.map(|p| ParamInfo {
is_optional: p.is_optional,
is_variadic: p.is_variadic,
})
.collect(),
);
}
}
Atomic::TIntersection { parts } => {
for part in parts.iter() {
if let Some(params) = extract_callable_params(part, ea) {
return Some(params);
}
}
}
_ => {}
}
}
None
}
pub(crate) fn is_valid_callable_type(union: &Type) -> bool {
for atomic in &union.types {
match atomic {
Atomic::TClosure { .. }
| Atomic::TCallable { .. }
| Atomic::TString
| Atomic::TNonEmptyString
| Atomic::TLiteralString(_)
| Atomic::TNull => {
return true;
}
Atomic::TKeyedArray { is_list, .. } => {
if *is_list {
return is_callable_array_pair(union);
}
return true;
}
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TArray { .. }
| Atomic::TNonEmptyArray { .. } => {
return false;
}
_ => {
continue;
}
}
}
true
}
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 !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;
}
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) = extract_callable_params(callback_ty, ea) {
let required_count = params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
let has_variadic = params.iter().any(|p| p.is_variadic);
let max_params = params.len();
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,
);
} else if !has_variadic && max_params < expected_arity {
let fn_name = callback_name_for_diagnostic(callback_ty);
ea.emit(
IssueKind::TooManyArguments {
fn_name,
expected: max_params,
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 !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;
}
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) = extract_callable_params(callback_ty, ea) {
let required_count = params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
let has_variadic = params.iter().any(|p| p.is_variadic);
let max_params = params.len();
if required_count > expected_arity || (!has_variadic && max_params < expected_arity) {
let actual_count = if has_variadic {
required_count
} else {
max_params
};
let expected_plural = if expected_arity == 1 { "" } else { "s" };
let actual_plural = if actual_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{}",
actual_count, actual_plural
),
},
Severity::Error,
callback_span,
);
}
}
}
fn callable_return_type(union: &Type, ea: &ExpressionAnalyzer<'_>) -> Option<Type> {
for atomic in &union.types {
match atomic {
Atomic::TClosure { return_type, .. } => return Some((**return_type).clone()),
Atomic::TCallable {
return_type: Some(rt),
..
} => return Some((**rt).clone()),
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) {
return Some(rt);
}
}
}
_ => {}
}
}
None
}
fn is_non_empty_collection(ty: &Type) -> bool {
!ty.types.is_empty()
&& ty.types.iter().all(|a| match a {
Atomic::TNonEmptyArray { .. } | Atomic::TNonEmptyList { .. } => true,
Atomic::TKeyedArray { properties, .. } => properties.values().any(|p| !p.optional),
_ => false,
})
}
pub(crate) fn count_return_type(arg_types: &[Type]) -> Option<Type> {
if let Some(ty) = arg_types.first() {
if ty.types.len() == 1 {
if let Atomic::TKeyedArray {
properties,
is_open,
..
} = &ty.types[0]
{
if !is_open && properties.values().all(|p| !p.optional) {
return Some(Type::single(Atomic::TLiteralInt(properties.len() as i64)));
}
}
}
}
let min = match arg_types.first() {
Some(t) if is_non_empty_collection(t) => 1,
_ => 0,
};
Some(Type::single(Atomic::TIntRange {
min: Some(min),
max: None,
}))
}
pub(crate) fn strlen_return_type(arg_types: &[Type]) -> Type {
if let Some(ty) = arg_types.first() {
if ty.types.len() == 1 {
if let Atomic::TLiteralString(s) = &ty.types[0] {
return Type::single(Atomic::TLiteralInt(s.len() as i64));
}
}
}
let min = match arg_types.first() {
Some(t) if is_non_empty_string(t) => 1,
_ => 0,
};
Type::single(Atomic::TIntRange {
min: Some(min),
max: None,
})
}
fn is_non_empty_string(ty: &Type) -> bool {
!ty.types.is_empty()
&& ty.types.iter().all(|a| {
matches!(
a,
Atomic::TNonEmptyString
| Atomic::TClassString(_)
| Atomic::TInterfaceString
| Atomic::TEnumString
| Atomic::TTraitString
) || matches!(a, Atomic::TLiteralString(s) if !s.is_empty())
})
}
pub(crate) fn string_preserve_non_empty(arg_types: &[Type]) -> Option<Type> {
let arg = arg_types.first()?;
if is_non_empty_string(arg) {
Some(Type::single(Atomic::TNonEmptyString))
} else {
None
}
}
pub(crate) fn number_format_return_type() -> Type {
Type::single(Atomic::TNonEmptyString)
}
pub(crate) fn str_repeat_return_type(arg_types: &[Type]) -> Option<Type> {
let input = arg_types.first()?;
let count = arg_types.get(1)?;
let count_is_positive = count.types.iter().any(|a| match a {
Atomic::TLiteralInt(n) => *n >= 1,
Atomic::TPositiveInt => true,
Atomic::TIntRange { min, .. } => min.is_some_and(|m| m >= 1),
_ => false,
}) && 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 && is_non_empty_string(input) {
Some(Type::single(Atomic::TNonEmptyString))
} else {
None
}
}
pub(crate) fn array_fill_return_type(arg_types: &[Type]) -> Option<Type> {
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 {
Some(Type::single(Atomic::TNonEmptyList {
value: Box::new(value.clone()),
}))
} else {
None
}
}
pub(crate) fn implode_return_type(arg_types: &[Type]) -> Option<Type> {
let arr = if arg_types.len() == 1 {
arg_types.first()?
} else {
arg_types.get(1)?
};
if !is_non_empty_collection(arr) {
return None;
}
let all_elements_non_empty = arr.types.iter().all(|a| match a {
Atomic::TNonEmptyList { value } | Atomic::TList { value } => is_non_empty_string(value),
Atomic::TNonEmptyArray { value, .. } | Atomic::TArray { value, .. } => {
is_non_empty_string(value)
}
Atomic::TKeyedArray { properties, .. } => {
properties.values().all(|p| is_non_empty_string(&p.ty))
}
_ => false,
});
if all_elements_non_empty {
Some(Type::single(Atomic::TNonEmptyString))
} else {
None
}
}
pub(crate) fn str_split_return_type(arg_types: &[Type]) -> Option<Type> {
let s = arg_types.first()?;
if is_non_empty_string(s) {
Some(Type::single(Atomic::TNonEmptyList {
value: Box::new(Type::single(Atomic::TNonEmptyString)),
}))
} else {
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 !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 (_, value) = crate::stmt::infer_foreach_types(arr);
if value.is_mixed() {
return None;
}
let atomic = if is_non_empty_collection(arr) {
Atomic::TNonEmptyList {
value: Box::new(value),
}
} else {
Atomic::TList {
value: Box::new(value),
}
};
Some(Type::single(atomic))
}
fn sprintf_format_guarantees_non_empty(fmt: &str) -> bool {
let bytes = fmt.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
i += 1;
if i >= bytes.len() {
return true;
}
while i < bytes.len()
&& matches!(
bytes[i],
b'+' | b'-' | b' ' | b'0'..=b'9' | b'.' | b'\'' | b'('
)
{
i += 1;
}
if i >= bytes.len() {
return true;
}
match bytes[i] {
b'%' => return true,
b's' => {}
_ => return true,
}
i += 1;
} else {
return true;
}
}
false
}
pub(crate) fn explode_return_type(arg_types: &[Type], stub_return: &Type) -> Option<Type> {
let separator = arg_types.first()?;
let sep_non_empty = separator.types.iter().any(|a| {
matches!(a, Atomic::TNonEmptyString)
|| matches!(a, Atomic::TLiteralString(s) if !s.as_ref().is_empty())
});
if !sep_non_empty {
return None;
}
let mut result = Type::single(Atomic::TNonEmptyList {
value: Box::new(Type::single(Atomic::TString)),
});
if stub_return
.types
.iter()
.any(|a| matches!(a, Atomic::TFalse))
{
result.add_type(Atomic::TFalse);
}
Some(result)
}
pub(crate) fn sprintf_return_type(arg_types: &[Type]) -> Option<Type> {
let fmt_ty = arg_types.first()?;
if fmt_ty.types.len() != 1 {
return None;
}
let fmt = match &fmt_ty.types[0] {
Atomic::TLiteralString(s) => s.as_ref(),
_ => return None,
};
if sprintf_format_guarantees_non_empty(fmt) {
Some(Type::single(Atomic::TNonEmptyString))
} else {
None
}
}
pub(crate) fn abs_return_type(arg_types: &[Type]) -> Option<Type> {
let arg = arg_types.first()?;
let all_int = arg.types.iter().all(|a| {
matches!(
a,
Atomic::TInt
| Atomic::TLiteralInt(_)
| Atomic::TPositiveInt
| Atomic::TNonNegativeInt
| Atomic::TNegativeInt
| Atomic::TIntRange { .. }
)
});
if !all_int || arg.types.is_empty() {
return None;
}
let mut result = Type::empty();
for a in &arg.types {
let atom = match a {
Atomic::TPositiveInt | Atomic::TNonNegativeInt => a.clone(),
Atomic::TInt => Atomic::TNonNegativeInt,
Atomic::TLiteralInt(n) => {
let abs = if *n >= 0 {
*n
} else {
n.checked_neg().unwrap_or(i64::MAX)
};
Atomic::TLiteralInt(abs)
}
Atomic::TNegativeInt => Atomic::TPositiveInt,
Atomic::TIntRange { min, max } => {
let (lo, hi) = (*min, *max);
let lo_is_nn = lo.is_some_and(|m| m >= 0);
let hi_is_np = hi.is_some_and(|m| m <= 0);
let abs_bound = |v: Option<i64>| {
v.map(|n| {
if n >= 0 {
n
} else {
n.checked_neg().unwrap_or(i64::MAX)
}
})
};
if lo_is_nn {
a.clone()
} else if hi_is_np {
Atomic::TIntRange {
min: abs_bound(hi),
max: abs_bound(lo),
}
} else {
let new_max = match (abs_bound(lo), hi) {
(Some(a), Some(b)) => Some(a.max(b)),
_ => None, };
Atomic::TIntRange {
min: Some(0),
max: new_max,
}
}
}
_ => return None,
};
result.add_type(atom);
}
Some(result)
}
pub(crate) fn intdiv_return_type(arg_types: &[Type]) -> Option<Type> {
let (num1_ty, num2_ty) = (arg_types.first()?, arg_types.get(1)?);
let (n1_min, n1_max) = int_type_bounds(num1_ty)?;
let (n2_min, _n2_max) = int_type_bounds(num2_ty)?;
let dividend_nn = n1_min.is_some_and(|m| m >= 0);
let divisor_pos = n2_min.is_some_and(|m| m > 0);
if !dividend_nn || !divisor_pos {
return None;
}
let new_max = match (n1_max, n2_min) {
(Some(hi), Some(lo)) => hi.checked_div(lo),
_ => None,
};
let atom = match (Some(0i64), new_max) {
(Some(0), None) => Atomic::TNonNegativeInt,
(Some(1), None) => Atomic::TPositiveInt,
(min, max) => Atomic::TIntRange { min, max },
};
Some(Type::single(atom))
}
fn int_type_bounds(ty: &Type) -> Option<(Option<i64>, Option<i64>)> {
if ty.types.is_empty() {
return None;
}
let mut min: Option<i64> = Some(i64::MAX);
let mut max: Option<i64> = Some(i64::MIN);
for a in &ty.types {
let (lo, hi) = match a {
Atomic::TLiteralInt(n) => (Some(*n), Some(*n)),
Atomic::TIntRange { min, max } => (*min, *max),
Atomic::TPositiveInt => (Some(1), None),
Atomic::TNonNegativeInt => (Some(0), None),
Atomic::TNegativeInt => (None, Some(-1)),
Atomic::TInt => (None, None),
_ => return None,
};
min = match (min, lo) {
(Some(m), Some(l)) => Some(m.min(l)),
_ => None,
};
max = match (max, hi) {
(Some(m), Some(h)) => Some(m.max(h)),
_ => None,
};
}
Some((min, max))
}
pub(crate) fn min_return_type(arg_types: &[Type]) -> Option<Type> {
if arg_types.is_empty() {
return None;
}
let bounds: Vec<(Option<i64>, Option<i64>)> = arg_types
.iter()
.map(int_type_bounds)
.collect::<Option<_>>()?;
let result_min = bounds.iter().fold(None::<Option<i64>>, |acc, (lo, _)| {
Some(match (acc, lo) {
(None, v) => *v,
(Some(Some(a)), Some(b)) => Some(a.min(*b)),
_ => None,
})
})?;
let result_max = bounds.iter().fold(None::<Option<i64>>, |acc, (_, hi)| {
Some(match (acc, hi) {
(None, v) => *v,
(Some(Some(a)), Some(b)) => Some(a.min(*b)),
(Some(None), Some(b)) => Some(*b),
(Some(Some(a)), None) => Some(a),
_ => None,
})
})?;
Some(Type::single(make_int_range_atom(result_min, result_max)))
}
pub(crate) fn max_return_type(arg_types: &[Type]) -> Option<Type> {
if arg_types.is_empty() {
return None;
}
let bounds: Vec<(Option<i64>, Option<i64>)> = arg_types
.iter()
.map(int_type_bounds)
.collect::<Option<_>>()?;
let result_min = bounds.iter().fold(None::<Option<i64>>, |acc, (lo, _)| {
Some(match (acc, lo) {
(None, v) => *v,
(Some(Some(a)), Some(b)) => Some(a.max(*b)),
(Some(None), Some(b)) => Some(*b),
(Some(Some(a)), None) => Some(a),
_ => None,
})
})?;
let result_max = bounds.iter().fold(None::<Option<i64>>, |acc, (_, hi)| {
Some(match (acc, hi) {
(None, v) => *v,
(Some(Some(a)), Some(b)) => Some(a.max(*b)),
_ => None,
})
})?;
Some(Type::single(make_int_range_atom(result_min, result_max)))
}
fn make_int_range_atom(min: Option<i64>, max: Option<i64>) -> Atomic {
match (min, 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 },
}
}
pub(crate) fn rand_return_type(arg_types: &[Type]) -> Option<Type> {
let (min_ty, max_ty) = (arg_types.first()?, arg_types.get(1)?);
let extract_literal = |ty: &Type| {
if ty.types.len() == 1 {
if let Atomic::TLiteralInt(n) = ty.types[0] {
return Some(n);
}
}
None
};
let lo = extract_literal(min_ty)?;
let hi = extract_literal(max_ty)?;
if lo > hi {
return None; }
Some(Type::single(make_int_range_atom(Some(lo), Some(hi))))
}
fn array_key_type() -> Type {
let mut k = Type::single(Atomic::TInt);
k.add_type(Atomic::TString);
k
}
pub(crate) fn infer_array_map_return(
ea: &ExpressionAnalyzer<'_>,
arg_types: &[Type],
) -> 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)?;
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 { .. }));
let src_is_non_empty = !source.types.is_empty()
&& source.types.iter().all(|a| {
matches!(
a,
Atomic::TNonEmptyArray { .. } | Atomic::TNonEmptyList { .. }
)
});
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() { array_key_type() } 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() { array_key_type() } 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_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 { .. }));
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 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 { .. }))
});
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(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_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 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 range_return_type(arg_types: &[Type]) -> Option<Type> {
let start = arg_types.first()?;
let end = arg_types.get(1)?;
fn single_int_bound(t: &Type) -> Option<i64> {
if t.types.len() != 1 {
return None;
}
match &t.types[0] {
Atomic::TLiteralInt(n) => Some(*n),
Atomic::TIntRange {
min: Some(lo),
max: Some(hi),
} if lo == hi => Some(*lo),
_ => None,
}
}
let lo = single_int_bound(start)?;
let hi = single_int_bound(end)?;
let (range_min, range_max) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let elem = Atomic::TIntRange {
min: Some(range_min),
max: Some(range_max),
};
Some(Type::single(Atomic::TNonEmptyList {
value: Box::new(Type::single(elem)),
}))
}
pub(crate) fn array_key_first_last_return(arg_types: &[Type]) -> Option<Type> {
let source = arg_types.first()?;
if source.is_mixed() || !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 is_non_empty_collection(source) {
Some(value)
} else {
let mut ty = value;
ty.add_type(Atomic::TNull);
Some(ty)
}
}
pub(crate) fn callback_min_arity_spec(fn_name: &str) -> Option<(usize, usize)> {
match fn_name {
"array_reduce" => Some((1, 2)),
"usort" | "uasort" | "uksort" => Some((1, 2)),
"array_walk" | "array_walk_recursive" => Some((1, 1)),
_ => None,
}
}
pub(crate) fn check_min_arity_callback(
ea: &mut ExpressionAnalyzer<'_>,
fn_name: &str,
callback_idx: usize,
min_arity: usize,
arg_types: &[Type],
arg_spans: &[Span],
) {
if arg_types.len() <= callback_idx || arg_spans.len() <= callback_idx {
return;
}
let callback_ty = &arg_types[callback_idx];
let callback_span = arg_spans[callback_idx];
if !is_valid_callable_type(callback_ty) {
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: fn_name.to_string(),
expected: "callable".to_string(),
actual: callback_ty.to_string(),
},
Severity::Error,
callback_span,
);
return;
}
if let Some(params) = extract_callable_params(callback_ty, ea) {
let required_count = params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
if required_count < min_arity {
let expected_plural = if min_arity == 1 { "" } else { "s" };
let actual_plural = if required_count == 1 { "" } else { "s" };
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: fn_name.to_string(),
expected: format!(
"callable accepting at least {} argument{}",
min_arity, expected_plural
),
actual: format!(
"callable accepting {} argument{}",
required_count, actual_plural
),
},
Severity::Error,
callback_span,
);
}
}
}
pub(crate) fn check_typed_callable_arg(
ea: &mut ExpressionAnalyzer<'_>,
arg_ty: &Type,
expected_params: &[FnParam],
arg_span: Span,
) {
if let Some(actual_params) = extract_callable_params(arg_ty, ea) {
let expected_required = expected_params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
let actual_required = actual_params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count();
if actual_required > expected_required {
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: "typed_callable".to_string(),
expected: format!("callable with {} required parameter(s)", expected_required),
actual: format!("callable with {} required parameter(s)", actual_required),
},
Severity::Error,
arg_span,
);
}
}
}
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 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 preg_split_return_type(arg_types: &[Type]) -> Option<Type> {
let flags_ty = arg_types.get(3);
let flags_zero = match flags_ty {
None => true,
Some(t) => t.types.len() == 1 && matches!(t.types.first(), Some(Atomic::TLiteralInt(0))),
};
if !flags_zero {
return None;
}
let mut result = Type::single(Atomic::TNonEmptyList {
value: Box::new(Type::single(Atomic::TString)),
});
result.add_type(Atomic::TFalse);
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 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 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))
}
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 array_push_unshift_byref_type(arr: &Type, push_types: &[Type]) -> Type {
if arr.is_mixed() || push_types.is_empty() {
return arr.clone();
}
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 { .. }));
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),
})
}