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> {
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 non_negative_int() -> Type {
Type::single(Atomic::TIntRange {
min: Some(0),
max: None,
})
}
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;
}
let key = if arg_types.len() == 2 {
let (k, _) = crate::stmt::infer_foreach_types(&arg_types[1]);
if k.is_mixed() {
array_key_type()
} else {
k
}
} else {
Type::single(Atomic::TInt)
};
Some(Type::single(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
}))
}
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 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,
);
}
}
}
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()
}
}