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 mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Name, Type};
use php_ast::owned::{ArrowFunctionExpr, ClosureExpr, ExprKind, Param};
use php_ast::Span;
use std::sync::Arc;
fn widen_byref_capture(ty: Type) -> Type {
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 param_name_span(source: &str, p: &Param) -> Span {
let Some(raw) = p.name.as_deref() else {
return p.span;
};
let bare = raw.trim_start_matches('$');
let range_start = p.span.start as usize;
let range_end = (p.span.end as usize).min(source.len());
let slice = &source[range_start..range_end];
let needle = format!("${bare}");
if let Some(rel) = slice.find(needle.as_str()) {
let start = p.span.start + rel as u32;
Span {
start,
end: start + needle.len() as u32,
}
} else {
p.span
}
}
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 refines = return_ty_hint
.as_ref()
.filter(|declared| {
is_string_family(declared)
&& is_string_family(&inferred_return)
&& !inferred_return.contains(|a| matches!(a, Atomic::TLiteralString(_)))
&& crate::subtype::is_subtype(db, &inferred_return, declared)
})
.is_some();
if refines {
inferred_return
} else {
return_ty_hint.unwrap_or(inferred_return)
}
}
fn is_string_family(ty: &Type) -> bool {
!ty.types.is_empty()
&& ty
.types
.iter()
.all(|a| a.is_string() || matches!(a, Atomic::TNull))
}
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.current_function_fqn.as_deref() {
crate::db::find_function(self.db, 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.current_function_fqn.as_deref())
.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.current_method_name.as_deref() {
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.current_function_fqn.as_deref() {
if let Some(function) =
crate::db::find_function(self.db, 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)
}
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,
);
}
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 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.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;
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,
);
}
}
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,
})
} 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,
);
}
}
closure_ctx.set_var(
name,
if use_var.by_ref {
widen_byref_capture(ctx.get_var(name))
} else {
ctx.get_var(name)
},
);
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
{
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 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(),
})
}),
}),
})
}
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,
);
}
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.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;
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");
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
{
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!("{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(),
})
})
},
}),
})
}
}