use std::sync::Arc;
use php_ast::Span;
use mir_issues::{IssueKind, Severity};
use mir_types::atomic::FnParam;
use mir_types::{Atomic, Type};
use crate::expr::ExpressionAnalyzer;
pub(crate) fn record_callable_string_ref(
ea: &ExpressionAnalyzer<'_>,
callback_ty: &Type,
callback_span: Span,
) {
for atomic in &callback_ty.types {
let Atomic::TLiteralString(name) = atomic else {
continue;
};
if name.is_empty() {
continue;
}
if let Some((class_name, method_name)) = name.as_ref().split_once("::") {
let resolved_class = crate::db::resolve_name(ea.db, &ea.file, class_name);
let here = crate::db::Fqcn::from_str(ea.db, &resolved_class);
if let Some((owner_fqcn, method)) =
crate::db::find_method_respecting_precedence(ea.db, here, method_name)
{
ea.record_ref(Arc::from(format!("cls:{resolved_class}")), callback_span);
ea.record_ref(
Arc::from(format!(
"meth:{owner_fqcn}::{}",
crate::util::php_ident_lowercase(&method.name)
)),
callback_span,
);
}
} else {
let fqn = name.as_ref().trim_start_matches('\\');
let here = crate::db::Fqcn::from_str(ea.db, fqn);
let canonical_fqn: Option<Arc<str>> =
crate::db::find_function(ea.db, here).map(|f| f.fqn.clone());
if let Some(canonical_fqn) = canonical_fqn {
ea.record_ref(Arc::from(format!("fn:{canonical_fqn}")), callback_span);
}
}
}
}
#[derive(Clone)]
pub(crate) struct ParamInfo {
pub(crate) is_optional: bool,
pub(crate) is_variadic: bool,
pub(crate) ty: Option<Type>,
}
fn extract_all_callable_candidates(
union: &Type,
ea: &ExpressionAnalyzer<'_>,
) -> Vec<Vec<ParamInfo>> {
let mut out = Vec::new();
for atomic in &union.types {
match atomic {
Atomic::TClosure { data } => {
out.push(
data.params
.iter()
.map(|p| ParamInfo {
is_optional: p.is_optional,
is_variadic: p.is_variadic,
ty: p.ty.as_ref().map(|t| t.to_union()),
})
.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) {
out.push(
f.params
.iter()
.map(|p| ParamInfo {
is_optional: p.is_optional,
is_variadic: p.is_variadic,
ty: p.ty.as_deref().cloned(),
})
.collect(),
);
}
}
Atomic::TCallable {
params: Some(params),
..
} => {
out.push(
params
.iter()
.map(|p| ParamInfo {
is_optional: p.is_optional,
is_variadic: p.is_variadic,
ty: p.ty.as_ref().map(|t| t.to_union()),
})
.collect(),
);
}
Atomic::TIntersection { parts } => {
for part in parts.iter() {
out.extend(extract_all_callable_candidates(part, ea));
}
}
_ => {}
}
}
out
}
fn extract_all_callable_return_types(union: &Type, ea: &ExpressionAnalyzer<'_>) -> Vec<Type> {
let mut out = Vec::new();
for atomic in &union.types {
match atomic {
Atomic::TClosure { data } => out.push(data.return_type.clone()),
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) {
if let Some(ret) = f.return_type.as_deref() {
out.push(ret.clone());
}
}
}
Atomic::TCallable {
return_type: Some(ret),
..
} => out.push((**ret).clone()),
Atomic::TIntersection { parts } => {
for part in parts.iter() {
out.extend(extract_all_callable_return_types(part, ea));
}
}
_ => {}
}
}
out
}
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;
}
extract_all_callable_candidates(union, ea)
.into_iter()
.next()
}
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 super::array_builtins::is_callable_array_pair(union);
}
return true;
}
Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TArray { .. }
| Atomic::TNonEmptyArray { .. } => {
return false;
}
_ => {
continue;
}
}
}
true
}
pub(super) 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 string_if_string_arg(arg_types: &[Type], idx: usize) -> Option<Type> {
let arg = arg_types.get(idx)?;
if is_non_empty_string(arg) {
Some(Type::single(Atomic::TNonEmptyString))
} else if !arg.types.is_empty() && arg.types.iter().all(|a| a.is_string()) {
Some(Type::single(Atomic::TString))
} 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 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
}
}
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))))
}
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 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;
}
record_callable_string_ref(ea, callback_ty, callback_span);
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,
);
}
}
}
fn contains_unresolvable_named_type(
ty: &Type,
ea: &ExpressionAnalyzer<'_>,
template_names: &rustc_hash::FxHashSet<&str>,
) -> bool {
ty.types
.iter()
.any(|a| atomic_contains_unresolvable_named_type(a, ea, template_names))
}
fn atomic_contains_unresolvable_named_type(
a: &Atomic,
ea: &ExpressionAnalyzer<'_>,
template_names: &rustc_hash::FxHashSet<&str>,
) -> bool {
match a {
Atomic::TNamedObject { fqcn, type_params } => {
(!fqcn.contains('\\')
&& (template_names.contains(fqcn.as_ref())
|| !crate::db::class_exists(ea.db, fqcn.as_ref())))
|| type_params
.iter()
.any(|tp| contains_unresolvable_named_type(tp, ea, template_names))
}
Atomic::TTemplateParam { .. } => true,
Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
contains_unresolvable_named_type(key, ea, template_names)
|| contains_unresolvable_named_type(value, ea, template_names)
}
Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
contains_unresolvable_named_type(value, ea, template_names)
}
Atomic::TKeyedArray { properties, .. } => properties
.values()
.any(|p| contains_unresolvable_named_type(&p.ty, ea, template_names)),
Atomic::TIntersection { parts } => parts
.iter()
.any(|p| contains_unresolvable_named_type(p, ea, template_names)),
Atomic::TCallable {
params,
return_type,
} => {
params.as_ref().is_some_and(|ps| {
ps.iter().any(|p| {
p.ty.as_ref().is_some_and(|t| {
contains_unresolvable_named_type(&t.to_union(), ea, template_names)
})
})
}) || return_type
.as_ref()
.is_some_and(|r| contains_unresolvable_named_type(r, ea, template_names))
}
Atomic::TClosure { data } => {
data.params.iter().any(|p| {
p.ty.as_ref().is_some_and(|t| {
contains_unresolvable_named_type(&t.to_union(), ea, template_names)
})
}) || contains_unresolvable_named_type(&data.return_type, ea, template_names)
}
_ => false,
}
}
fn is_int_or_float(t: &Type) -> bool {
!t.types.is_empty()
&& t.types.iter().all(|a| {
matches!(
a,
Atomic::TInt
| Atomic::TLiteralInt(_)
| Atomic::TIntRange { .. }
| Atomic::TPositiveInt
| Atomic::TNegativeInt
| Atomic::TNonNegativeInt
| Atomic::TFloat
| Atomic::TIntegralFloat
| Atomic::TLiteralFloat(..)
)
})
}
fn expected_fits_actual_param(expected: &Type, actual: &Type, ea: &ExpressionAnalyzer<'_>) -> bool {
if expected.is_mixed() || actual.is_mixed() {
return true;
}
if crate::subtype::is_subtype(ea.db, expected, actual) {
return true;
}
if ea.strict_types {
return false;
}
is_int_or_float(expected) && is_int_or_float(actual)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_typed_callable_arg(
ea: &mut ExpressionAnalyzer<'_>,
fn_name: &str,
param_name: &str,
arg_ty: &Type,
expected_params: &[FnParam],
expected_return: Option<&Type>,
arg_span: Span,
template_params: &[mir_codebase::definitions::TemplateParam],
) {
let template_names: rustc_hash::FxHashSet<&str> =
template_params.iter().map(|tp| tp.name.as_ref()).collect();
if arg_ty
.types
.iter()
.any(|a| matches!(a, Atomic::TCallable { params: None, .. }))
{
return;
}
let candidates = extract_all_callable_candidates(arg_ty, ea);
if candidates.is_empty() {
return;
}
let expected_required = expected_params.iter().filter(|p| !p.is_variadic).count();
let actual_required = candidates
.iter()
.map(|params| {
params
.iter()
.filter(|p| !p.is_optional && !p.is_variadic)
.count()
})
.max()
.unwrap_or(0);
if actual_required > expected_required {
ea.emit(
IssueKind::InvalidArgument {
param: param_name.to_string(),
fn_name: fn_name.to_string(),
expected: format!("callable with {} required parameter(s)", expected_required),
actual: format!("callable with {} required parameter(s)", actual_required),
},
Severity::Error,
arg_span,
);
return;
}
for params in &candidates {
for (i, expected) in expected_params.iter().enumerate() {
let Some(actual) = params.get(i) else {
break;
};
if actual.is_optional || actual.is_variadic {
continue;
}
let Some(expected_ty) = expected.ty.as_ref().map(|t| t.to_union()) else {
continue;
};
let Some(actual_ty) = actual.ty.as_ref() else {
continue;
};
if expected_ty.is_mixed() || actual_ty.is_mixed() {
continue;
}
if contains_unresolvable_named_type(&expected_ty, ea, &template_names)
|| contains_unresolvable_named_type(actual_ty, ea, &template_names)
{
continue;
}
if !expected_fits_actual_param(&expected_ty, actual_ty, ea) {
ea.emit(
IssueKind::InvalidArgument {
param: param_name.to_string(),
fn_name: fn_name.to_string(),
expected: format!(
"callable whose parameter #{} accepts {expected_ty}",
i + 1
),
actual: format!(
"callable whose parameter #{} only accepts {actual_ty}",
i + 1
),
},
Severity::Error,
arg_span,
);
return;
}
}
}
if let Some(expected_ret) = expected_return {
if !expected_ret.is_mixed()
&& !contains_unresolvable_named_type(expected_ret, ea, &template_names)
{
for actual_ret in extract_all_callable_return_types(arg_ty, ea) {
if actual_ret.is_mixed()
|| contains_unresolvable_named_type(&actual_ret, ea, &template_names)
{
continue;
}
if !expected_fits_actual_param(&actual_ret, expected_ret, ea) {
ea.emit(
IssueKind::InvalidArgument {
param: param_name.to_string(),
fn_name: fn_name.to_string(),
expected: format!("callable returning {expected_ret}"),
actual: format!("callable returning {actual_ret}"),
},
Severity::Error,
arg_span,
);
return;
}
}
}
}
}
pub(crate) fn filter_var_return_type(arg_types: &[Type]) -> Option<Type> {
if arg_types.len() > 2 {
return None;
}
let filter_arg = arg_types.get(1)?;
let [Atomic::TLiteralInt(filter)] = filter_arg.types.as_slice() else {
return None;
};
let mut ty = match filter {
257 => Type::single(Atomic::TInt), 259 => Type::single(Atomic::TFloat), 258 => return Some(Type::single(Atomic::TBool)),
272..=277 => Type::single(Atomic::TString),
_ => return None,
};
ty.add_type(Atomic::TFalse);
Some(ty)
}
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 result = Type::single(Atomic::TNonEmptyList {
value: Box::new(Type::single(Atomic::TString)),
});
Some(result)
}
pub(crate) fn preg_match_matches_type(flags: i64) -> Type {
Type::single(Atomic::TList {
value: Box::new(preg_match_leaf(flags)),
})
}
pub(crate) fn preg_match_all_matches_type(flags: i64) -> Type {
let inner = Type::single(Atomic::TList {
value: Box::new(preg_match_leaf(flags)),
});
Type::single(Atomic::TList {
value: Box::new(inner),
})
}
fn preg_match_leaf(flags: i64) -> Type {
const PREG_OFFSET_CAPTURE: i64 = 256;
if flags & PREG_OFFSET_CAPTURE != 0 {
let mut props = indexmap::IndexMap::new();
props.insert(
mir_types::atomic::ArrayKey::Int(0),
mir_types::atomic::KeyedProperty {
ty: Type::single(Atomic::TString),
optional: false,
},
);
props.insert(
mir_types::atomic::ArrayKey::Int(1),
mir_types::atomic::KeyedProperty {
ty: Type::single(Atomic::TInt),
optional: false,
},
);
Type::single(Atomic::TKeyedArray {
properties: Box::new(props),
is_open: false,
is_list: true,
})
} else {
Type::single(Atomic::TString)
}
}