use std::sync::Arc;
use php_ast::Span;
use mir_codebase::storage::{FnParam, TemplateParam, Visibility};
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Name, Type};
use crate::expr::ExpressionAnalyzer;
mod counts;
mod nullability;
mod types;
pub(crate) struct ArgBinding {
pub(crate) param_idx: usize,
pub(crate) arg_ty: Type,
pub(crate) arg_span: Span,
pub(crate) arg_idx: usize,
}
pub struct CheckArgsParams<'a> {
pub fn_name: &'a str,
pub params: &'a [FnParam],
pub arg_types: &'a [Type],
pub arg_spans: &'a [Span],
pub arg_names: &'a [Option<String>],
pub arg_can_be_byref: &'a [bool],
pub call_span: Span,
pub has_spread: bool,
pub template_params: &'a [TemplateParam],
}
pub fn check_constructor_args(
ea: &mut ExpressionAnalyzer<'_>,
class_name: &str,
p: CheckArgsParams<'_>,
) {
let ctor_name = format!("{class_name}::__construct");
check_args(
ea,
CheckArgsParams {
fn_name: &ctor_name,
..p
},
);
}
pub fn spread_element_type(arr_ty: &Type) -> Type {
let mut result = Type::empty();
for atomic in arr_ty.types.iter() {
match atomic {
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => {
for t in value.types.iter() {
result.add_type(t.clone());
}
}
Atomic::TKeyedArray { properties, .. } => {
for (_key, prop) in properties.iter() {
for t in prop.ty.types.iter() {
result.add_type(t.clone());
}
}
}
_ => return Type::mixed(),
}
}
if result.types.is_empty() {
Type::mixed()
} else {
result
}
}
pub(crate) fn substitute_static_in_return(ret: Type, receiver_fqcn: &Arc<str>) -> Type {
let from_docblock = ret.from_docblock;
let types: Vec<Atomic> = ret
.types
.into_iter()
.map(|a| match a {
Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
fqcn: Name::from(receiver_fqcn.as_ref()),
type_params: mir_types::union::empty_type_params(),
},
other => other,
})
.collect();
let mut result = Type::from_vec(types);
result.from_docblock = from_docblock;
result
}
pub(crate) fn check_method_visibility(
ea: &mut ExpressionAnalyzer<'_>,
visibility: Visibility,
owner_fqcn: &Arc<str>,
method_name: &Arc<str>,
ctx: &crate::flow_state::FlowState,
span: Span,
) {
match visibility {
Visibility::Private => {
let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
let from_trait =
crate::db::class_kind(ea.db, owner_fqcn.as_ref()).is_some_and(|k| k.is_trait);
let allowed = caller_fqcn == owner_fqcn.as_ref()
|| (from_trait
&& crate::db::extends_or_implements(ea.db, caller_fqcn, owner_fqcn.as_ref()));
if !allowed {
ea.emit(
IssueKind::UndefinedMethod {
class: owner_fqcn.to_string(),
method: method_name.to_string(),
},
Severity::Error,
span,
);
}
}
Visibility::Protected => {
let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
if caller_fqcn.is_empty() {
ea.emit(
IssueKind::UndefinedMethod {
class: owner_fqcn.to_string(),
method: method_name.to_string(),
},
Severity::Error,
span,
);
} else {
let allowed = caller_fqcn == owner_fqcn.as_ref()
|| crate::db::extends_or_implements(ea.db, caller_fqcn, owner_fqcn.as_ref());
if !allowed {
ea.emit(
IssueKind::UndefinedMethod {
class: owner_fqcn.to_string(),
method: method_name.to_string(),
},
Severity::Error,
span,
);
}
}
}
Visibility::Public => {}
}
}
pub(crate) fn expr_can_be_passed_by_reference_owned(expr: &php_ast::owned::Expr) -> bool {
matches!(
expr.kind,
php_ast::owned::ExprKind::Variable(_)
| php_ast::owned::ExprKind::ArrayAccess(_)
| php_ast::owned::ExprKind::PropertyAccess(_)
| php_ast::owned::ExprKind::NullsafePropertyAccess(_)
| php_ast::owned::ExprKind::StaticPropertyAccess(_)
| php_ast::owned::ExprKind::StaticPropertyAccessDynamic { .. }
)
}
pub(crate) fn check_args(ea: &mut ExpressionAnalyzer<'_>, p: CheckArgsParams<'_>) {
let CheckArgsParams {
fn_name,
params,
arg_types,
arg_spans,
arg_names,
arg_can_be_byref,
call_span,
has_spread,
template_params,
} = p;
let bindings = counts::check_counts(
ea, fn_name, params, arg_types, arg_spans, arg_names, call_span, has_spread,
);
for ArgBinding {
param_idx,
arg_ty,
arg_span,
arg_idx,
} in &bindings
{
let param = ¶ms[*param_idx];
if param.is_byref && !arg_can_be_byref.get(*arg_idx).copied().unwrap_or(false) {
ea.emit(
IssueKind::InvalidPassByReference {
fn_name: fn_name.to_string(),
param: param.name.to_string(),
},
Severity::Error,
*arg_span,
);
}
if let Some(raw_param_ty) = ¶m.ty {
let param_ty_owned;
let param_ty: &Type = if param.is_variadic {
if let Some(elem_ty) = raw_param_ty.types.iter().find_map(|a| match a {
Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
Some(*value.clone())
}
_ => None,
}) {
param_ty_owned = elem_ty;
¶m_ty_owned
} else {
raw_param_ty
}
} else {
raw_param_ty
};
types::check_one(
ea,
fn_name,
¶m.name,
param_ty,
arg_ty,
*arg_span,
*arg_idx,
template_params,
);
}
}
}
fn param_contains_template_or_unknown(
param_ty: &Type,
arg_ty: &Type,
ea: &ExpressionAnalyzer<'_>,
template_params: &[TemplateParam],
) -> bool {
let template_names: std::collections::HashSet<&str> =
template_params.iter().map(|tp| tp.name.as_ref()).collect();
fn has_template_param(union: &Type, template_names: &std::collections::HashSet<&str>) -> bool {
union.types.iter().any(|atomic| match atomic {
Atomic::TTemplateParam { .. } => true,
Atomic::TNamedObject { fqcn, type_params } => {
if !fqcn.contains('\\') && template_names.contains(fqcn.as_ref()) {
return true;
}
type_params
.iter()
.any(|tp| has_template_param(tp, template_names))
}
Atomic::TClassString(Some(inner)) => {
!inner.contains('\\') && template_names.contains(inner.as_ref())
}
_ => false,
})
}
param_ty.types.iter().any(|atomic| match atomic {
Atomic::TTemplateParam { .. } => true,
Atomic::TNamedObject { fqcn, type_params } => {
if !fqcn.contains('\\') && template_names.contains(fqcn.as_ref()) {
return true;
}
if !fqcn.contains('\\') && !crate::db::class_exists(ea.db, fqcn.as_ref()) {
return true;
}
!type_params.is_empty() && has_template_param(param_ty, &template_names)
}
Atomic::TClassString(Some(inner)) => {
if !inner.contains('\\') && template_names.contains(inner.as_ref()) {
return true;
}
!inner.contains('\\') && !crate::db::class_exists(ea.db, inner.as_ref())
}
Atomic::TArray { key: _, value }
| Atomic::TList { value }
| Atomic::TNonEmptyArray { key: _, value }
| Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
Atomic::TTemplateParam { .. } => true,
Atomic::TNamedObject { fqcn, .. } => {
if !fqcn.contains('\\') && template_names.contains(fqcn.as_ref()) {
return true;
}
!fqcn.contains('\\') && !crate::db::class_exists(ea.db, fqcn.as_ref())
}
_ => false,
}),
Atomic::TIntersection { parts } => {
let has_template = parts
.iter()
.any(|part| has_template_param(part, &template_names));
if !has_template {
return false;
}
parts.iter().all(|part| {
if has_template_param(part, &template_names) {
return true; }
part.types.iter().all(|part_atomic| {
let part_fqcn = match part_atomic {
Atomic::TNamedObject { fqcn, .. } => fqcn,
_ => return true,
};
let arg_satisfies = |arg_fqcn: &Name| {
arg_fqcn == part_fqcn
|| crate::db::extends_or_implements(
ea.db,
arg_fqcn.as_ref(),
part_fqcn.as_ref(),
)
};
arg_ty.types.iter().any(|arg_atomic| match arg_atomic {
Atomic::TNamedObject { fqcn, .. } => arg_satisfies(fqcn),
Atomic::TIntersection { parts: arg_parts } => arg_parts
.iter()
.any(|ap| ap.types.iter().any(|a| matches!(a, Atomic::TNamedObject { fqcn, .. } if arg_satisfies(fqcn)))),
_ => false,
})
})
})
}
_ => false,
})
}