use super::helpers::{
apply_doc_param_types, ast_params_to_fn_params_resolved, resolve_named_objects_in_union_native,
};
use super::ExpressionAnalyzer;
use crate::flow_state::FlowState;
use crate::stmt::{mir_check_matches, return_type_is_invalid, widen_for_check};
use crate::symbol::ReferenceKind;
use crate::util::param_name_span;
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Name, Type};
use php_ast::owned::{ArrowFunctionExpr, ClosureExpr, ExprKind};
use std::sync::Arc;
fn widen_byref_capture(ty: Type) -> Type {
if matches!(ty.types.as_slice(), [Atomic::TNull]) {
return Type::mixed();
}
let widened = widen_for_check(ty);
let mut out = Type::empty();
for atomic in widened.types {
let atomic = match atomic {
Atomic::TTrue | Atomic::TFalse => Atomic::TBool,
other => other,
};
out.add_type(atomic);
}
out
}
fn merge_byref_write(pre: Type, written: &Type) -> Type {
let written = &generalise_written_lists(written);
let writes_array = written
.types
.iter()
.any(|a| matches!(a, Atomic::TArray { .. } | Atomic::TKeyedArray { .. }));
let mut base = Type::empty();
for atomic in pre.types {
let is_empty_array =
matches!(&atomic, Atomic::TKeyedArray { properties, .. } if properties.is_empty());
if !(writes_array && is_empty_array) {
base.add_type(atomic);
}
}
let merged = Type::merge(&base, written);
let arrays = merged
.types
.iter()
.filter(|a| {
matches!(
a,
Atomic::TArray { .. }
| Atomic::TList { .. }
| Atomic::TNonEmptyArray { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { .. }
)
})
.count();
if arrays < 2 {
return merged;
}
let mut out = Type::empty();
for atomic in merged.types {
out.add_type(match atomic {
Atomic::TKeyedArray {
properties,
is_list,
..
} => Atomic::TKeyedArray {
properties,
is_open: true,
is_list,
},
other => other,
});
}
out
}
fn generalise_written_lists(written: &Type) -> Type {
let mut out = Type::empty();
for atomic in &written.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_list: true,
..
} if !properties.is_empty() => {
let mut value = Type::empty();
for prop in properties.values() {
value = Type::merge(&value, &prop.ty);
}
out.add_type(Atomic::TList {
value: Box::new(value),
});
}
other => out.add_type(other.clone()),
}
}
out
}
fn propagate_readonly_prop_refinements(
db: &dyn crate::db::MirDatabase,
ctx: &FlowState,
inner_ctx: &mut FlowState,
) {
let Some(self_fqcn) = ctx.self_fqcn.clone() else {
return;
};
let this_sym = mir_types::Name::from("this");
let here = crate::db::Fqcn::from_str(db, self_fqcn.as_ref());
for ((obj_var, prop), ty) in ctx.prop_refined.iter() {
if *obj_var != this_sym {
continue;
}
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop.as_str()) {
if p_def.is_readonly {
inner_ctx.set_prop_refined("this", prop.as_str(), (**ty).clone());
}
}
}
}
fn refined_closure_return(
db: &dyn crate::db::MirDatabase,
return_ty_hint: Option<Type>,
inferred_return: Type,
) -> Type {
let inferred_return = match &return_ty_hint {
Some(declared)
if is_bare_array(declared) && inferred_return.types.iter().any(is_record_shape) =>
{
widen_record_literals(&inferred_return)
}
_ => inferred_return,
};
let inferred_return = if return_ty_hint.is_some() {
widen_scalar_literals(inferred_return)
} else {
inferred_return
};
let refines = return_ty_hint
.as_ref()
.filter(|declared| {
let same_family = (is_string_family(declared) && is_string_family(&inferred_return))
|| (is_int_family(declared) && is_int_family(&inferred_return))
|| (is_bare_array(declared)
&& inferred_return
.types
.iter()
.all(|a| is_generic_array(a) || is_record_shape(a)));
same_family && crate::subtype::is_subtype(db, &inferred_return, declared)
})
.is_some();
if refines {
inferred_return
} else {
return_ty_hint.unwrap_or(inferred_return)
}
}
fn is_record_shape(a: &Atomic) -> bool {
matches!(a, Atomic::TKeyedArray { is_list: false, .. })
}
fn widen_record_literals(ty: &Type) -> Type {
let mut out = Type::empty();
for a in &ty.types {
match a {
Atomic::TKeyedArray {
properties,
is_open,
is_list: false,
} => {
let properties = properties
.iter()
.map(|(k, p)| {
let mut p = p.clone();
p.ty = widen_record_literals(&crate::stmt::widen_for_check(p.ty));
(k.clone(), p)
})
.collect();
out.add_type(Atomic::TKeyedArray {
properties: Box::new(properties),
is_open: *is_open,
is_list: false,
});
}
other => out.add_type(other.clone()),
}
}
out
}
fn is_string_family(ty: &Type) -> bool {
!ty.types.is_empty()
&& ty
.types
.iter()
.all(|a| a.is_string() || matches!(a, Atomic::TNull))
}
fn widen_scalar_literals(ty: Type) -> Type {
let int_bounds = ty
.types
.iter()
.filter_map(|a| match a {
Atomic::TLiteralInt(n) => Some(*n),
_ => None,
})
.fold(None, |acc: Option<(i64, i64)>, n| {
Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
});
let has_empty_string = ty
.types
.iter()
.any(|a| matches!(a, Atomic::TLiteralString(s) if s.is_empty()));
let mut out = Type::empty();
out.from_docblock = ty.from_docblock;
for a in ty.types {
out.add_type(match (a, int_bounds) {
(Atomic::TLiteralInt(_), Some((lo, _))) if lo > 0 => Atomic::TPositiveInt,
(Atomic::TLiteralInt(_), Some((lo, _))) if lo >= 0 => Atomic::TNonNegativeInt,
(Atomic::TLiteralInt(_), Some((_, hi))) if hi < 0 => Atomic::TNegativeInt,
(Atomic::TLiteralInt(_), _) => Atomic::TInt,
(Atomic::TLiteralString(_), _) if has_empty_string => Atomic::TString,
(Atomic::TLiteralString(_), _) => Atomic::TNonEmptyString,
(other, _) => other,
});
}
out
}
fn is_int_family(ty: &Type) -> bool {
!ty.types.is_empty()
&& ty
.types
.iter()
.all(|a| a.is_int() || matches!(a, Atomic::TNull))
}
fn is_bare_array(ty: &Type) -> bool {
matches!(
ty.types.as_slice(),
[Atomic::TArray { key, value }]
if (key.is_mixed() || key.is_array_key()) && value.is_mixed()
)
}
fn is_generic_array(a: &Atomic) -> bool {
a.is_array() && !matches!(a, Atomic::TKeyedArray { .. })
}
impl<'a> ExpressionAnalyzer<'a> {
fn expand_local_type_aliases_in_doc(
&self,
doc: &mut crate::parser::ParsedDocblock,
ctx: &FlowState,
) {
let aliases = if let Some(fqcn) = ctx.self_fqcn.as_deref() {
crate::db::find_class_like(self.db, crate::db::Fqcn::from_str(self.db, fqcn))
.map(|cl| cl.type_aliases().clone())
} else if let Some(fqn) = ctx.template_function().map(|f| &**f) {
crate::db::find_function_from(
self.db,
&self.file,
crate::db::Fqcn::from_str(self.db, fqn),
)
.map(|f| f.type_aliases.clone())
} else {
None
};
let Some(aliases) = aliases else { return };
if aliases.is_empty() {
return;
}
for (_, ty) in doc.params.iter_mut() {
*ty = crate::collector::expand_aliases_only(ty.clone(), &aliases);
}
if let Some(rt) = doc.return_type.take() {
doc.return_type = Some(crate::collector::expand_aliases_only(rt, &aliases));
}
}
fn own_doc_template_context(
&self,
ctx: &FlowState,
) -> (
rustc_hash::FxHashSet<String>,
Vec<mir_codebase::definitions::TemplateParam>,
String,
) {
let mut template_names: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
let mut template_params: Vec<mir_codebase::definitions::TemplateParam> = Vec::new();
let defining_entity = ctx
.self_fqcn
.as_deref()
.or(ctx.template_function().map(|f| &**f))
.unwrap_or_default()
.to_string();
if let Some(fqcn) = ctx.self_fqcn.as_deref() {
if let Some(class_like) =
crate::db::find_class_like(self.db, crate::db::Fqcn::from_str(self.db, fqcn))
{
template_names.extend(
class_like
.template_params()
.iter()
.map(|tp| tp.name.to_string()),
);
template_params.extend(class_like.template_params().iter().cloned());
if let Some(method_name) = ctx.template_method().map(|m| &**m) {
let method_name = crate::util::php_ident_lowercase(method_name);
if let Some(method) = class_like.own_methods().get(method_name.as_str()) {
template_names
.extend(method.template_params.iter().map(|tp| tp.name.to_string()));
template_params.extend(method.template_params.iter().cloned());
}
}
}
} else if let Some(fqn) = ctx.template_function().map(|f| &**f) {
if let Some(function) = crate::db::find_function_from(
self.db,
&self.file,
crate::db::Fqcn::from_str(self.db, fqn),
) {
template_names.extend(
function
.template_params
.iter()
.map(|tp| tp.name.to_string()),
);
template_params.extend(function.template_params.iter().cloned());
}
}
(template_names, template_params, defining_entity)
}
fn narrow_params_to_callback_hints(&mut self, params: &mut [mir_codebase::DeclaredParam]) {
let Some(hints) = self.callback_param_hints.take() else {
return;
};
for (param, hint) in params.iter_mut().zip(hints) {
let Some(hint) = hint.filter(|h| !h.is_mixed() && !h.is_never()) else {
continue;
};
if param.is_byref || param.is_variadic {
continue;
}
let Some(declared) = ¶m.ty else {
if !crate::call::has_unbound_template(&hint) {
param.ty = mir_codebase::wrap_param_type(Some(hint));
}
continue;
};
if declared.is_mixed()
|| !declared.remove_null().is_single()
|| declared.from_docblock
|| hint == **declared
|| !crate::subtype::is_subtype(self.db, &hint, declared)
{
continue;
}
param.ty = mir_codebase::wrap_param_type(Some(hint));
}
}
pub(super) fn analyze_closure(
&mut self,
c: &ClosureExpr,
expr_span: php_ast::Span,
ctx: &mut FlowState,
) -> Type {
for param in c.params.iter() {
if let Some(hint) = ¶m.type_hint {
self.check_type_hint(hint);
}
}
if let Some(hint) = &c.return_type {
self.check_type_hint(hint);
}
let mut leading_doc = crate::parser::find_preceding_docblock(self.source, expr_span.start)
.map(|doc| crate::parser::DocblockParser::parse(&doc));
if let Some(doc) = &mut leading_doc {
self.expand_local_type_aliases_in_doc(doc, ctx);
}
let mut params = ast_params_to_fn_params_resolved(
&c.params,
ctx.self_fqcn.as_deref(),
self.db,
&self.file,
);
let (template_names, template_params, defining_entity) = self.own_doc_template_context(ctx);
if let Some(doc) = &leading_doc {
apply_doc_param_types(
&mut params,
&c.params,
&doc.params,
self.db,
&self.file,
&template_names,
&template_params,
&defining_entity,
);
}
self.narrow_params_to_callback_hints(&mut params);
let return_ty_hint = c
.return_type
.as_ref()
.map(|h| crate::parser::type_from_hint_owned(h, ctx.self_fqcn.as_deref()))
.map(|u| resolve_named_objects_in_union_native(u, self.db, &self.file))
.or_else(|| {
leading_doc
.as_ref()
.and_then(|doc| doc.return_type.clone())
.map(|ty| {
crate::stmt::resolve_union_for_file_with_templates(
ty,
self.db,
&self.file,
&template_names,
&template_params,
&defining_entity,
)
})
});
if return_ty_hint.is_none() && self.mode == crate::expr::AnalysisMode::Full {
self.emit(
mir_issues::IssueKind::MissingClosureReturnType,
mir_issues::Severity::Info,
expr_span,
);
}
let mut closure_ctx = crate::flow_state::FlowState::for_function(
¶ms,
return_ty_hint.clone(),
Arc::from([]),
ctx.self_fqcn.clone(),
ctx.parent_fqcn.clone(),
ctx.static_fqcn.clone(),
ctx.strict_types,
c.is_static,
);
if std::mem::take(&mut ctx.rebound_closure_this) && !c.is_static {
closure_ctx.set_unknown_this();
} else if ctx.self_fqcn.is_none() && !c.is_static {
let this_sym = Name::from("this");
Arc::make_mut(&mut closure_ctx.vars).insert(
this_sym,
mir_codebase::definitions::wrap_var_type(Type::single(Atomic::TObject)),
);
Arc::make_mut(&mut closure_ctx.assigned_vars).insert(this_sym);
}
closure_ctx.template_param_names = Arc::clone(&ctx.template_param_names);
closure_ctx.inherit_template_scope(ctx);
closure_ctx.is_in_pure_fn = ctx.is_in_pure_fn;
closure_ctx.is_in_immutable_method = ctx.is_in_immutable_method;
closure_ctx.is_in_external_mutation_free_method = ctx.is_in_external_mutation_free_method;
closure_ctx.is_in_immutable_ctor = ctx.is_in_immutable_ctor;
propagate_readonly_prop_refinements(self.db, ctx, &mut closure_ctx);
for p in c.params.iter() {
if let Some(raw) = p.name.as_deref() {
let trimmed = raw.trim_start_matches('$');
let ty = closure_ctx.get_var(trimmed);
self.record_symbol(
param_name_span(self.source, p),
ReferenceKind::Variable(Arc::from(trimmed)),
ty,
);
}
}
let mut byref_captures: Vec<(&str, Type)> = Vec::new();
for use_var in c.use_vars.iter() {
let name = use_var.name.trim_start_matches('$');
if use_var.by_ref {
if !ctx.var_is_defined(name) {
let is_self_ref = ctx.self_ref_closure_hint == Some(Name::from(name));
ctx.set_var(
name,
if is_self_ref {
Type::single(mir_types::Atomic::TCallable {
params: None,
return_type: None,
is_pure: false,
})
} else {
Type::mixed()
},
);
}
} else if !ctx.var_is_defined(name) {
if ctx.var_possibly_defined(name) {
self.emit(
mir_issues::IssueKind::PossiblyUndefinedVariable {
name: name.to_string(),
},
mir_issues::Severity::Warning,
use_var.span,
);
} else {
self.emit(
mir_issues::IssueKind::UndefinedVariable {
name: name.to_string(),
},
mir_issues::Severity::Error,
use_var.span,
);
}
}
let captured_ty = if use_var.by_ref {
widen_byref_capture(ctx.get_var(name))
} else {
ctx.get_var(name)
};
self.record_symbol(
crate::symbol::variable_token_span(self.source, use_var.span, name),
ReferenceKind::Variable(Arc::from(name)),
captured_ty.clone(),
);
closure_ctx.set_var(name, captured_ty.clone());
if use_var.by_ref {
byref_captures.push((name, captured_ty));
}
if ctx.is_tainted(name) {
closure_ctx.taint_var(name);
}
ctx.read_vars.insert(mir_types::Name::from(name));
ctx.mark_consumed(name);
}
if closure_ctx.is_in_pure_fn
|| closure_ctx.is_in_immutable_method
|| closure_ctx.is_in_external_mutation_free_method
|| closure_ctx.is_in_immutable_ctor
{
let mut extended_param_names = (*closure_ctx.param_names).clone();
let mut extended_byref_param_names = (*closure_ctx.byref_param_names).clone();
for use_var in c.use_vars.iter() {
let name = use_var.name.trim_start_matches('$');
if ctx.param_names.contains(&Name::from(name)) {
extended_param_names.insert(Name::from(name));
}
if use_var.by_ref && ctx.byref_param_names.contains(&Name::from(name)) {
extended_byref_param_names.insert(Name::from(name));
}
}
closure_ctx.param_names = Arc::new(extended_param_names);
closure_ctx.byref_param_names = Arc::new(extended_byref_param_names);
}
let mut sa = crate::stmt::StatementsAnalyzer::new(
self.db,
self.file.clone(),
self.source,
self.source_map,
self.issues,
self.symbols.as_deref_mut(),
self.navigation_facts,
self.resolved_navigation_facts,
self.php_version,
self.mode,
);
sa.collect_symbols = self.collect_symbols;
sa.capture_symbol_types = self.capture_symbol_types;
sa.codebase_symbols_only = self.codebase_symbols_only;
sa.record_reference_locations = self.record_reference_locations;
sa.collect_navigation_facts = self.collect_navigation_facts;
sa.collect_resolved_navigation_facts = self.collect_resolved_navigation_facts;
sa.analyze_stmts(&c.body.stmts, &mut closure_ctx);
let inferred_return =
crate::body_analysis::merge_return_types(&sa.return_types, closure_ctx.diverges);
let inferred_return = if sa.yielded_types.is_empty() {
inferred_return
} else {
crate::body_analysis::build_generator_return_type(&sa.yielded_types, inferred_return)
};
for (name, captured_ty) in byref_captures {
let written_ty = closure_ctx.get_var(name);
let pre_ty = ctx.get_var(name);
if matches!(pre_ty.types.as_slice(), [Atomic::TNull]) {
ctx.set_var(name, Type::mixed());
} else if written_ty != captured_ty {
let merged = merge_byref_write(pre_ty, &written_ty);
ctx.set_var(name, merged);
}
}
for name in &closure_ctx.read_vars {
if ctx.var_is_defined(name) || ctx.var_possibly_defined(name) {
ctx.read_vars.insert(*name);
ctx.mark_consumed(name.as_str());
}
}
let return_ty = refined_closure_return(self.db, return_ty_hint, inferred_return);
let closure_params: Box<[mir_types::atomic::FnParam]> = params
.iter()
.map(|p| mir_types::atomic::FnParam {
name: Name::from(p.name.as_ref()),
ty: p
.ty
.as_ref()
.map(|arc| mir_types::SimpleType::from_union((**arc).clone())),
out_ty: None,
default: if p.has_default {
Some(mir_types::SimpleType::from_union(Type::mixed()))
} else {
None
},
is_variadic: p.is_variadic,
is_byref: p.is_byref,
is_optional: p.is_optional,
})
.collect();
Type::single(Atomic::TClosure {
data: Box::new(mir_types::atomic::ClosureData {
params: closure_params,
return_type: return_ty,
this_type: ctx.self_fqcn.clone().map(|f| {
Type::single(Atomic::TNamedObject {
fqcn: Name::from(f.as_ref()),
type_params: mir_types::union::empty_type_params(),
})
}),
is_pure: ctx.is_in_pure_fn,
is_static: c.is_static,
}),
})
}
pub(super) fn analyze_arrow_function(
&mut self,
af: &ArrowFunctionExpr,
expr_span: php_ast::Span,
ctx: &mut FlowState,
) -> Type {
for param in af.params.iter() {
if let Some(hint) = ¶m.type_hint {
self.check_type_hint(hint);
}
}
if let Some(hint) = &af.return_type {
self.check_type_hint(hint);
}
let mut leading_doc = crate::parser::find_preceding_docblock(self.source, expr_span.start)
.map(|doc| crate::parser::DocblockParser::parse(&doc));
if let Some(doc) = &mut leading_doc {
self.expand_local_type_aliases_in_doc(doc, ctx);
}
let mut params = ast_params_to_fn_params_resolved(
&af.params,
ctx.self_fqcn.as_deref(),
self.db,
&self.file,
);
let (template_names, template_params, defining_entity) = self.own_doc_template_context(ctx);
if let Some(doc) = &leading_doc {
apply_doc_param_types(
&mut params,
&af.params,
&doc.params,
self.db,
&self.file,
&template_names,
&template_params,
&defining_entity,
);
}
self.narrow_params_to_callback_hints(&mut params);
let return_ty_hint = af
.return_type
.as_ref()
.map(|h| crate::parser::type_from_hint_owned(h, ctx.self_fqcn.as_deref()))
.map(|u| resolve_named_objects_in_union_native(u, self.db, &self.file))
.or_else(|| {
leading_doc
.as_ref()
.and_then(|doc| doc.return_type.clone())
.map(|ty| {
crate::stmt::resolve_union_for_file_with_templates(
ty,
self.db,
&self.file,
&template_names,
&template_params,
&defining_entity,
)
})
});
let mut arrow_ctx = crate::flow_state::FlowState::for_function(
¶ms,
return_ty_hint.clone(),
Arc::from([]),
ctx.self_fqcn.clone(),
ctx.parent_fqcn.clone(),
ctx.static_fqcn.clone(),
ctx.strict_types,
af.is_static,
);
arrow_ctx.template_param_names = Arc::clone(&ctx.template_param_names);
arrow_ctx.inherit_template_scope(ctx);
arrow_ctx.is_in_pure_fn = ctx.is_in_pure_fn;
arrow_ctx.is_in_immutable_method = ctx.is_in_immutable_method;
arrow_ctx.is_in_external_mutation_free_method = ctx.is_in_external_mutation_free_method;
arrow_ctx.is_in_immutable_ctor = ctx.is_in_immutable_ctor;
propagate_readonly_prop_refinements(self.db, ctx, &mut arrow_ctx);
arrow_ctx.tainted_vars = ctx.tainted_vars.clone();
let this_sym = mir_types::Name::from("this");
if std::mem::take(&mut ctx.rebound_closure_this) && !af.is_static {
arrow_ctx.set_unknown_this();
}
for (name, ty) in ctx.vars.iter() {
if af.is_static && *name == this_sym {
continue;
}
if !arrow_ctx.vars.contains_key(name) {
std::sync::Arc::make_mut(&mut arrow_ctx.vars).insert(*name, ty.clone());
std::sync::Arc::make_mut(&mut arrow_ctx.assigned_vars).insert(*name);
}
}
if !af.is_static && !arrow_ctx.vars.contains_key(&this_sym) {
std::sync::Arc::make_mut(&mut arrow_ctx.vars).insert(
this_sym,
mir_codebase::definitions::wrap_var_type(Type::single(Atomic::TObject)),
);
std::sync::Arc::make_mut(&mut arrow_ctx.assigned_vars).insert(this_sym);
}
if arrow_ctx.is_in_pure_fn
|| arrow_ctx.is_in_immutable_method
|| arrow_ctx.is_in_external_mutation_free_method
|| arrow_ctx.is_in_immutable_ctor
{
let mut extended_param_names = (*arrow_ctx.param_names).clone();
extended_param_names.extend(ctx.param_names.iter().copied());
arrow_ctx.param_names = Arc::new(extended_param_names);
}
for p in af.params.iter() {
if let Some(raw) = p.name.as_deref() {
let trimmed = raw.trim_start_matches('$');
let ty = arrow_ctx.get_var(trimmed);
self.record_symbol(
param_name_span(self.source, p),
ReferenceKind::Variable(Arc::from(trimmed)),
ty,
);
}
}
let check_target = match &af.body.kind {
ExprKind::Parenthesized(inner) => inner.as_ref(),
_ => &af.body,
};
if let Some(doc) =
crate::parser::find_preceding_docblock(self.source, check_target.span.start)
{
let checks = crate::parser::DocblockParser::parse(&doc).mir_checks;
for (expr_text, expected_str) in checks {
let expected = crate::parser::docblock::parse_type_string(&expected_str);
let actual_raw = self.eval_check_expr(&expr_text, &arrow_ctx);
if !mir_check_matches(&expected, &actual_raw) {
self.emit(
IssueKind::TypeCheckMismatch {
var: expr_text,
expected: expected.to_string(),
actual: widen_for_check(actual_raw).to_string(),
},
Severity::Error,
check_target.span,
);
}
}
}
let inferred_return = self.analyze(&af.body, &mut arrow_ctx);
for name in &arrow_ctx.read_vars {
ctx.read_vars.insert(*name);
ctx.mark_consumed(name.as_str());
}
if let Some(declared) = &return_ty_hint {
let has_invalid = !declared.contains(|t| matches!(t, Atomic::TConditional { .. }))
&& ((declared.is_void()
&& !inferred_return.is_void()
&& !inferred_return.is_mixed())
|| return_type_is_invalid(
&inferred_return,
declared,
ctx.strict_types,
self.db,
&self.file,
));
let is_mixed_return = !has_invalid
&& !declared.is_void()
&& !declared.is_mixed()
&& inferred_return.is_mixed()
&& !declared.contains(|t| matches!(t, Atomic::TConditional { .. }));
if is_mixed_return {
let kind = IssueKind::MixedReturnStatement {
declared: format!("{declared}"),
};
let severity = kind.default_severity();
self.emit(kind, severity, check_target.span);
} else if has_invalid {
let kind = IssueKind::InvalidReturnType {
expected: format!("{declared}"),
actual: format!("{inferred_return}"),
};
let severity = kind.default_severity();
self.emit(kind, severity, check_target.span);
} else if !declared.is_void()
&& !declared.is_mixed()
&& !declared.contains(|t| matches!(t, Atomic::TNull))
&& !declared.contains(|t| matches!(t, Atomic::TConditional { .. }))
&& !declared.contains(|t| matches!(t, Atomic::TTemplateParam { .. }))
&& inferred_return.contains(|t| matches!(t, Atomic::TNull))
&& !inferred_return.remove_null().is_empty()
&& !return_type_is_invalid(
&inferred_return.remove_null(),
declared,
ctx.strict_types,
self.db,
&self.file,
)
{
let kind = IssueKind::NullableReturnStatement {
expected: format!("{declared}"),
actual: format!(
"{}",
crate::stmt::erase_templates_to_bounds(&inferred_return)
),
};
let severity = kind.default_severity();
self.emit(kind, severity, check_target.span);
}
}
let return_ty = refined_closure_return(self.db, return_ty_hint, inferred_return);
let closure_params: Box<[mir_types::atomic::FnParam]> = params
.iter()
.map(|p| mir_types::atomic::FnParam {
name: Name::from(p.name.as_ref()),
ty: p
.ty
.as_ref()
.map(|arc| mir_types::SimpleType::from_union((**arc).clone())),
out_ty: None,
default: if p.has_default {
Some(mir_types::SimpleType::from_union(Type::mixed()))
} else {
None
},
is_variadic: p.is_variadic,
is_byref: p.is_byref,
is_optional: p.is_optional,
})
.collect();
Type::single(Atomic::TClosure {
data: Box::new(mir_types::atomic::ClosureData {
params: closure_params,
return_type: return_ty,
this_type: if af.is_static {
None
} else {
ctx.self_fqcn.clone().map(|f| {
Type::single(Atomic::TNamedObject {
fqcn: Name::from(f.as_ref()),
type_params: mir_types::union::empty_type_params(),
})
})
},
is_pure: ctx.is_in_pure_fn,
is_static: af.is_static,
}),
})
}
}