use rustc_hash::FxHashMap;
use php_ast::Span;
use mir_codebase::storage::TemplateParam;
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Name, Type};
use crate::expr::ExpressionAnalyzer;
fn type_exists(ea: &ExpressionAnalyzer<'_>, fqcn: &str) -> bool {
crate::db::class_exists(ea.db, fqcn)
}
fn is_interface(ea: &ExpressionAnalyzer<'_>, fqcn: &str) -> bool {
crate::db::class_kind(ea.db, fqcn).is_some_and(|k| k.is_interface)
}
fn class_template_params(
ea: &ExpressionAnalyzer<'_>,
fqcn: &str,
) -> Vec<mir_codebase::storage::TemplateParam> {
crate::db::class_template_params(ea.db, fqcn)
.map(|tps| tps.to_vec())
.unwrap_or_default()
}
fn scalar_arg_fits_param(arg: &Type, param: &Type) -> bool {
arg.is_subtype_structural(param)
}
fn param_accepts_wider_than_arg(param: &Type, arg: &Type) -> bool {
param.is_subtype_structural(arg)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_one(
ea: &mut ExpressionAnalyzer<'_>,
fn_name: &str,
param_name: &str,
param_ty: &Type,
arg_ty: &Type,
arg_span: Span,
arg_idx: usize,
template_params: &[TemplateParam],
) {
for param_atomic in ¶m_ty.types {
if let Atomic::TCallable {
params: Some(expected_params),
..
} = param_atomic
{
super::super::callable::check_typed_callable_arg(ea, arg_ty, expected_params, arg_span);
}
}
let skip_validation =
matches!(fn_name, "call_user_func" | "call_user_func_array") && arg_idx == 0;
if !skip_validation {
validate_callable_argument(ea, param_ty, arg_ty, arg_span);
}
validate_class_string_argument(ea, param_ty, arg_ty, arg_span);
validate_callable_type(ea, param_ty, arg_ty, arg_span);
super::nullability::check_one(
ea,
fn_name,
param_name,
param_ty,
arg_ty,
arg_span,
template_params,
);
let param_accepts_false = param_ty.contains(|t| matches!(t, Atomic::TFalse | Atomic::TBool));
if !param_accepts_false
&& !param_ty.is_mixed()
&& !arg_ty.is_mixed()
&& !arg_ty.is_single()
&& arg_ty.contains(|t| matches!(t, Atomic::TFalse | Atomic::TBool))
{
let arg_without_false = arg_ty.remove_false();
let arg_core = arg_ty.core_type();
if !arg_core.types.is_empty()
&& (scalar_arg_fits_param(&arg_without_false, param_ty)
|| scalar_arg_fits_param(&arg_core, param_ty)
|| named_object_subtype(&arg_without_false, param_ty, ea)
|| named_object_subtype(&arg_core, param_ty, ea))
{
ea.emit(
IssueKind::PossiblyInvalidArgument {
param: param_name.to_string(),
fn_name: fn_name.to_string(),
expected: format!("{param_ty}"),
actual: format!("{arg_ty}"),
},
Severity::Info,
arg_span,
);
}
}
if arg_ty.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)))
&& param_ty.is_single()
&& param_ty.contains(|t| t.is_int())
{
ea.emit(
IssueKind::ImplicitFloatToIntCast {
from: arg_ty.to_string(),
},
Severity::Warning,
arg_span,
);
}
let arg_core = arg_ty.core_type();
if !scalar_arg_fits_param(arg_ty, param_ty)
&& !param_ty.is_mixed()
&& !arg_ty.is_mixed()
&& !named_object_subtype(arg_ty, param_ty, ea)
&& !super::param_contains_template_or_unknown(param_ty, arg_ty, ea, template_params)
&& !super::param_contains_template_or_unknown(arg_ty, arg_ty, ea, template_params)
&& !array_list_compatible(arg_ty, param_ty, ea)
&& !(arg_ty.is_single() && param_accepts_wider_than_arg(param_ty, arg_ty))
&& !(arg_ty.is_single() && param_accepts_wider_than_arg(¶m_ty.remove_null(), arg_ty))
&& !(arg_ty.is_single()
&& param_ty
.types
.iter()
.any(|p| param_accepts_wider_than_arg(&Type::single(p.clone()), arg_ty)))
&& !scalar_arg_fits_param(&arg_ty.remove_null(), param_ty)
&& (arg_ty.remove_false().types.is_empty()
|| !scalar_arg_fits_param(&arg_ty.remove_false(), param_ty))
&& (arg_core.types.is_empty() || !scalar_arg_fits_param(&arg_core, param_ty))
&& !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
&& (arg_ty.remove_false().types.is_empty()
|| !named_object_subtype(&arg_ty.remove_false(), param_ty, ea))
&& (arg_core.types.is_empty() || !named_object_subtype(&arg_core, param_ty, ea))
{
ea.emit(
IssueKind::InvalidArgument {
param: param_name.to_string(),
fn_name: fn_name.to_string(),
expected: format!("{param_ty}"),
actual: invalid_argument_actual_type(arg_ty, param_ty, ea),
},
Severity::Error,
arg_span,
);
}
}
fn invalid_argument_actual_type(
arg_ty: &Type,
param_ty: &Type,
ea: &ExpressionAnalyzer<'_>,
) -> String {
if let Some(projected) = project_generic_ancestor_type(arg_ty, param_ty, ea) {
return format!("{projected}");
}
format!("{arg_ty}")
}
fn project_generic_ancestor_type(
arg_ty: &Type,
param_ty: &Type,
ea: &ExpressionAnalyzer<'_>,
) -> Option<Type> {
if !arg_ty.is_single() {
return None;
}
let arg_fqcn = match arg_ty.types.first()? {
Atomic::TNamedObject { fqcn, type_params } => {
if !type_params.is_empty() {
return None;
}
fqcn
}
Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => fqcn,
_ => return None,
};
let resolved_arg = crate::db::resolve_name(ea.db, &ea.file, arg_fqcn.as_ref());
for param_atomic in ¶m_ty.types {
let (param_fqcn, param_type_params) = match param_atomic {
Atomic::TNamedObject { fqcn, type_params } => (fqcn, type_params),
_ => continue,
};
if param_type_params.is_empty() {
continue;
}
let resolved_param = crate::db::resolve_name(ea.db, &ea.file, param_fqcn.as_ref());
let ancestor_args = generic_ancestor_type_args(arg_fqcn.as_ref(), &resolved_param, ea)
.or_else(|| generic_ancestor_type_args(&resolved_arg, &resolved_param, ea))
.or_else(|| generic_ancestor_type_args(arg_fqcn.as_ref(), param_fqcn.as_ref(), ea))
.or_else(|| generic_ancestor_type_args(&resolved_arg, param_fqcn.as_ref(), ea))?;
if ancestor_args.is_empty() {
continue;
}
return Some(Type::single(Atomic::TNamedObject {
fqcn: *param_fqcn,
type_params: mir_types::union::vec_to_type_params(ancestor_args),
}));
}
None
}
fn named_object_subtype(arg: &Type, param: &Type, ea: &ExpressionAnalyzer<'_>) -> bool {
arg.types.iter().all(|a_atomic| {
let arg_fqcn: &Name = match a_atomic {
Atomic::TNamedObject { fqcn, .. } => fqcn,
Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
let is_trait =
crate::db::class_kind(ea.db, fqcn.as_ref()).is_some_and(|k| k.is_trait);
if is_trait {
return true;
}
fqcn
}
Atomic::TParent { fqcn } => fqcn,
Atomic::TNever => return true,
Atomic::TClosure { .. } => {
return param.types.iter().any(|p| match p {
Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
_ => false,
});
}
Atomic::TCallable { .. } => {
return param.types.iter().any(|p| match p {
Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
_ => false,
});
}
Atomic::TClassString(Some(arg_cls)) => {
return param.types.iter().any(|p| match p {
Atomic::TClassString(None) | Atomic::TString => true,
Atomic::TClassString(Some(param_cls)) => {
arg_cls == param_cls
|| crate::db::extends_or_implements(
ea.db,
arg_cls.as_ref(),
param_cls.as_ref(),
)
}
_ => false,
});
}
Atomic::TNull => {
return param.types.iter().any(|p| matches!(p, Atomic::TNull));
}
Atomic::TFalse => {
return param
.types
.iter()
.any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
}
Atomic::TIntersection { parts } => {
return param
.types
.iter()
.any(|p| matches!(p, Atomic::TObject | Atomic::TMixed))
|| parts
.iter()
.any(|part| named_object_subtype(part, param, ea));
}
_ => return false,
};
if param
.types
.iter()
.any(|p| matches!(p, Atomic::TCallable { .. }))
{
let resolved_arg = crate::db::resolve_name(ea.db, &ea.file, arg_fqcn.as_ref());
if crate::db::has_method_in_chain(ea.db, &resolved_arg, "__invoke")
|| crate::db::has_method_in_chain(ea.db, arg_fqcn.as_ref(), "__invoke")
{
return true;
}
}
param.types.iter().any(|p_atomic| {
if let Atomic::TIntersection { parts } = p_atomic {
return parts.iter().all(|part| {
part.types.iter().any(|part_atomic| {
let part_fqcn = match part_atomic {
Atomic::TNamedObject { fqcn, .. } => fqcn,
_ => return false,
};
let resolved_part =
crate::db::resolve_name(ea.db, &ea.file, part_fqcn.as_ref());
crate::db::extends_or_implements(ea.db, arg_fqcn.as_ref(), &resolved_part)
|| crate::db::extends_or_implements(
ea.db,
arg_fqcn.as_ref(),
part_fqcn.as_ref(),
)
})
});
}
let param_fqcn: &Name = match p_atomic {
Atomic::TNamedObject { fqcn, .. } => fqcn,
Atomic::TSelf { fqcn } => fqcn,
Atomic::TStaticObject { fqcn } => fqcn,
Atomic::TParent { fqcn } => fqcn,
_ => return false,
};
let resolved_param = crate::db::resolve_name(ea.db, &ea.file, param_fqcn.as_ref());
let resolved_arg = crate::db::resolve_name(ea.db, &ea.file, arg_fqcn.as_ref());
let is_same_class = resolved_param == resolved_arg
|| arg_fqcn.as_ref() == resolved_param.as_str()
|| resolved_arg == param_fqcn.as_ref();
if is_same_class {
let arg_type_params = match a_atomic {
Atomic::TNamedObject { type_params, .. } => &type_params[..],
_ => &[],
};
let param_type_params = match p_atomic {
Atomic::TNamedObject { type_params, .. } => &type_params[..],
_ => &[],
};
if !arg_type_params.is_empty() || !param_type_params.is_empty() {
let class_tps = class_template_params(ea, &resolved_param);
return generic_type_params_compatible(
arg_type_params,
param_type_params,
&class_tps,
ea,
);
}
return true;
}
let arg_extends_param =
crate::db::extends_or_implements(ea.db, arg_fqcn.as_ref(), &resolved_param)
|| crate::db::extends_or_implements(
ea.db,
arg_fqcn.as_ref(),
param_fqcn.as_ref(),
)
|| crate::db::extends_or_implements(ea.db, &resolved_arg, &resolved_param);
if arg_extends_param {
let param_type_params = match p_atomic {
Atomic::TNamedObject { type_params, .. } => &type_params[..],
_ => &[],
};
if !param_type_params.is_empty() {
let ancestor_args =
generic_ancestor_type_args(arg_fqcn.as_ref(), &resolved_param, ea)
.or_else(|| {
generic_ancestor_type_args(&resolved_arg, &resolved_param, ea)
})
.or_else(|| {
generic_ancestor_type_args(
arg_fqcn.as_ref(),
param_fqcn.as_ref(),
ea,
)
})
.or_else(|| {
generic_ancestor_type_args(&resolved_arg, param_fqcn.as_ref(), ea)
});
if let Some(arg_as_param_params) = ancestor_args {
let class_tps = class_template_params(ea, &resolved_param);
return generic_type_params_compatible(
&arg_as_param_params,
param_type_params,
&class_tps,
ea,
);
}
}
return true;
}
if crate::db::extends_or_implements(ea.db, param_fqcn.as_ref(), &resolved_arg)
|| crate::db::extends_or_implements(ea.db, param_fqcn.as_ref(), arg_fqcn.as_ref())
|| crate::db::extends_or_implements(ea.db, &resolved_param, &resolved_arg)
{
let param_type_params = match p_atomic {
Atomic::TNamedObject { type_params, .. } => &type_params[..],
_ => &[],
};
if param_type_params.is_empty() {
return true;
}
}
if !arg_fqcn.contains('\\') && !type_exists(ea, &resolved_arg) {
let target = arg_fqcn.as_ref();
for fqcn in crate::db::workspace_classes(ea.db).iter() {
let here = crate::db::Fqcn::from_str(ea.db, fqcn.as_ref());
let is_class =
crate::db::find_class_like(ea.db, here).is_some_and(|c| c.is_class());
if !is_class {
continue;
}
let short_name = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
if short_name == target
&& (crate::db::extends_or_implements(ea.db, fqcn.as_ref(), &resolved_param)
|| crate::db::extends_or_implements(
ea.db,
fqcn.as_ref(),
param_fqcn.as_ref(),
))
{
return true;
}
}
}
let iface_key = if is_interface(ea, arg_fqcn.as_ref()) {
Some(arg_fqcn.as_ref())
} else if is_interface(ea, resolved_arg.as_str()) {
Some(resolved_arg.as_str())
} else {
None
};
if let Some(iface_fqcn) = iface_key {
let class_fqcns: Vec<std::sync::Arc<str>> = crate::db::workspace_classes(ea.db)
.iter()
.filter(|fqcn| {
let here = crate::db::Fqcn::from_str(ea.db, fqcn.as_ref());
crate::db::find_class_like(ea.db, here).is_some_and(|c| c.is_class())
})
.cloned()
.collect();
let compatible = class_fqcns.iter().any(|cls_fqcn| {
crate::db::extends_or_implements(ea.db, cls_fqcn.as_ref(), iface_fqcn)
&& (crate::db::extends_or_implements(
ea.db,
cls_fqcn.as_ref(),
param_fqcn.as_ref(),
) || crate::db::extends_or_implements(
ea.db,
cls_fqcn.as_ref(),
&resolved_param,
))
});
if compatible {
return true;
}
}
if arg_fqcn.contains('\\')
&& !type_exists(ea, arg_fqcn.as_ref())
&& !type_exists(ea, &resolved_arg)
{
return true;
}
if param_fqcn.contains('\\')
&& !type_exists(ea, param_fqcn.as_ref())
&& !type_exists(ea, &resolved_param)
{
return true;
}
false
})
})
}
fn strict_named_object_subtype(arg: &Type, param: &Type, ea: &ExpressionAnalyzer<'_>) -> bool {
arg.types.iter().all(|a_atomic| {
let arg_fqcn: &Name = match a_atomic {
Atomic::TNamedObject { fqcn, .. } => fqcn,
Atomic::TNever => return true,
_ => return false,
};
param.types.iter().any(|p_atomic| {
let param_fqcn: &Name = match p_atomic {
Atomic::TNamedObject { fqcn, .. } => fqcn,
_ => return false,
};
let resolved_param = crate::db::resolve_name(ea.db, &ea.file, param_fqcn.as_ref());
let resolved_arg = crate::db::resolve_name(ea.db, &ea.file, arg_fqcn.as_ref());
resolved_param == resolved_arg
|| arg_fqcn.as_ref() == resolved_param.as_str()
|| resolved_arg == param_fqcn.as_ref()
|| crate::db::extends_or_implements(ea.db, arg_fqcn.as_ref(), &resolved_param)
|| crate::db::extends_or_implements(ea.db, arg_fqcn.as_ref(), param_fqcn.as_ref())
|| crate::db::extends_or_implements(ea.db, &resolved_arg, &resolved_param)
})
})
}
fn generic_type_params_compatible(
arg_params: &[Type],
param_params: &[Type],
template_params: &[mir_codebase::storage::TemplateParam],
ea: &ExpressionAnalyzer<'_>,
) -> bool {
if arg_params.len() != param_params.len() {
return true;
}
if arg_params.is_empty() {
return true;
}
for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
let variance = template_params
.get(i)
.map(|tp| tp.variance)
.unwrap_or(mir_types::Variance::Invariant);
let compatible = match variance {
mir_types::Variance::Covariant => {
scalar_arg_fits_param(arg_p, param_p)
|| param_p.is_mixed()
|| arg_p.is_mixed()
|| strict_named_object_subtype(arg_p, param_p, ea)
}
mir_types::Variance::Contravariant => {
scalar_arg_fits_param(param_p, arg_p)
|| arg_p.is_mixed()
|| param_p.is_mixed()
|| strict_named_object_subtype(param_p, arg_p, ea)
}
mir_types::Variance::Invariant => {
arg_p == param_p
|| arg_p.is_mixed()
|| param_p.is_mixed()
|| (scalar_arg_fits_param(arg_p, param_p)
&& scalar_arg_fits_param(param_p, arg_p))
}
};
if !compatible {
return false;
}
}
true
}
fn generic_ancestor_type_args(
child: &str,
ancestor: &str,
ea: &ExpressionAnalyzer<'_>,
) -> Option<Vec<Type>> {
let mut seen = std::collections::HashSet::new();
generic_ancestor_type_args_inner(child, ancestor, ea, &mut seen)
}
fn generic_ancestor_type_args_inner(
child: &str,
ancestor: &str,
ea: &ExpressionAnalyzer<'_>,
seen: &mut std::collections::HashSet<String>,
) -> Option<Vec<Type>> {
if child == ancestor {
return Some(vec![]);
}
if !seen.insert(child.to_string()) {
return None;
}
let here = crate::db::Fqcn::from_str(ea.db, child);
let cl = crate::db::find_class_like(ea.db, here)?;
let parent = cl.parent().cloned();
let extends_type_args: Vec<Type> = cl.extends_type_args().to_vec();
let implements_type_args = cl.implements_type_args();
for (iface, args) in implements_type_args.iter() {
if iface.as_ref() == ancestor {
return Some(args.to_vec());
}
}
let parent = parent?;
if parent.as_ref() == ancestor {
return Some(extends_type_args);
}
let parent_args = generic_ancestor_type_args_inner(parent.as_ref(), ancestor, ea, seen)?;
if parent_args.is_empty() {
return Some(parent_args);
}
let parent_template_params = class_template_params(ea, parent.as_ref());
let bindings: FxHashMap<Name, Type> = parent_template_params
.iter()
.zip(extends_type_args.iter())
.map(|(tp, ty)| (Name::from(tp.name.as_ref()), ty.clone()))
.collect();
Some(
parent_args
.into_iter()
.map(|ty| ty.substitute_templates(&bindings))
.collect(),
)
}
fn union_compatible(arg_ty: &Type, param_ty: &Type, ea: &ExpressionAnalyzer<'_>) -> bool {
arg_ty.types.iter().all(|av| {
let av_fqcn: &Name = match av {
Atomic::TNamedObject { fqcn, .. } => fqcn,
Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
fqcn
}
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => {
return param_ty.types.iter().any(|pv| {
let pv_val: &Type = match pv {
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => value,
_ => return false,
};
union_compatible(value, pv_val, ea)
});
}
Atomic::TKeyedArray { .. } => return true,
_ => return scalar_arg_fits_param(&Type::single(av.clone()), param_ty),
};
param_ty.types.iter().any(|pv| {
let pv_fqcn: &Name = match pv {
Atomic::TNamedObject { fqcn, .. } => fqcn,
Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => fqcn,
_ => return false,
};
if !pv_fqcn.contains('\\') && !type_exists(ea, pv_fqcn.as_ref()) {
return true;
}
let resolved_param = crate::db::resolve_name(ea.db, &ea.file, pv_fqcn.as_ref());
let resolved_arg = crate::db::resolve_name(ea.db, &ea.file, av_fqcn.as_ref());
resolved_param == resolved_arg
|| crate::db::extends_or_implements(ea.db, av_fqcn.as_ref(), &resolved_param)
|| crate::db::extends_or_implements(ea.db, &resolved_arg, &resolved_param)
|| crate::db::extends_or_implements(ea.db, pv_fqcn.as_ref(), &resolved_arg)
|| crate::db::extends_or_implements(ea.db, &resolved_param, &resolved_arg)
})
})
}
fn array_list_compatible(arg_ty: &Type, param_ty: &Type, ea: &ExpressionAnalyzer<'_>) -> bool {
arg_ty.types.iter().all(|a_atomic| {
let arg_value: &Type = match a_atomic {
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => value,
Atomic::TKeyedArray { .. } => return true,
_ => return false,
};
param_ty.types.iter().any(|p_atomic| {
let param_value: &Type = match p_atomic {
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => value,
_ => return false,
};
union_compatible(arg_value, param_value, ea)
})
})
}
fn validate_callable_argument(
ea: &mut ExpressionAnalyzer<'_>,
param_ty: &Type,
arg_ty: &Type,
arg_span: Span,
) {
if !param_ty.contains(|t| matches!(t, Atomic::TCallable { .. } | Atomic::TCallableString)) {
return;
}
if let Some(Atomic::TLiteralString(s)) = arg_ty.types.first() {
if let Some((class_name, method_name)) = s.split_once("::") {
let resolved_class = crate::db::resolve_name(ea.db, &ea.file, class_name);
if !crate::db::class_exists(ea.db, &resolved_class) {
ea.emit(
IssueKind::UndefinedClass {
name: resolved_class,
},
Severity::Error,
arg_span,
);
} else {
let here = crate::db::Fqcn::new(ea.db, Name::from(resolved_class.as_str()));
if crate::db::find_method_in_chain(ea.db, here, method_name).is_none() {
ea.emit(
IssueKind::UndefinedMethod {
class: resolved_class.clone(),
method: method_name.to_string(),
},
Severity::Error,
arg_span,
);
}
}
} else {
let here = crate::db::Fqcn::from_str(ea.db, s.as_ref());
if crate::db::find_function(ea.db, here).is_none() {
ea.emit(
IssueKind::UndefinedFunction {
name: s.to_string(),
},
Severity::Error,
arg_span,
);
}
}
}
}
fn validate_class_string_argument(
ea: &mut ExpressionAnalyzer<'_>,
param_ty: &Type,
arg_ty: &Type,
arg_span: Span,
) {
let has_class_string = param_ty
.types
.iter()
.any(|t| matches!(t, Atomic::TClassString(_)));
if !has_class_string {
return;
}
if let Some(Atomic::TLiteralString(s)) = arg_ty.types.first() {
let resolved = crate::db::resolve_name(ea.db, &ea.file, s.as_ref());
if !crate::db::class_exists(ea.db, &resolved) {
ea.emit(
IssueKind::UndefinedClass { name: resolved },
Severity::Error,
arg_span,
);
}
}
}
fn validate_callable_type(
ea: &mut ExpressionAnalyzer<'_>,
param_ty: &Type,
arg_ty: &Type,
arg_span: Span,
) {
let is_callable = param_ty.contains(|t| matches!(t, Atomic::TCallable { .. }));
if !is_callable {
return;
}
for atomic in &arg_ty.types {
if let Atomic::TKeyedArray { properties, .. } = atomic {
if properties.len() != 2 {
ea.emit(
IssueKind::InvalidArgument {
param: "callback".to_string(),
fn_name: "callable".to_string(),
expected: "callable (string or [object, \"method\"])".to_string(),
actual: arg_ty.to_string(),
},
Severity::Error,
arg_span,
);
continue;
}
let obj_prop = properties.values().next();
let method_prop = properties.values().nth(1);
if let (Some(obj_prop), Some(method_prop)) = (obj_prop, method_prop) {
if let Some(Atomic::TLiteralString(method_name)) = method_prop.ty.types.first() {
for obj_atomic in &obj_prop.ty.types {
if let Atomic::TNamedObject { fqcn, .. } = obj_atomic {
let resolved_class =
crate::db::resolve_name(ea.db, &ea.file, fqcn.as_ref());
let here =
crate::db::Fqcn::new(ea.db, Name::from(resolved_class.as_str()));
if crate::db::find_method_in_chain(ea.db, here, method_name).is_none() {
ea.emit(
IssueKind::UndefinedMethod {
class: resolved_class.clone(),
method: method_name.to_string(),
},
Severity::Error,
arg_span,
);
}
}
}
}
}
}
}
}